From f65b56f669e98ec065e0a31cb1bb977c40134a87 Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Thu, 28 Sep 2023 15:17:57 -0700 Subject: [PATCH 001/472] Initial commit --- .../Features/IEndPointHealthFeature.cs | 12 + .../Features/IEndPointLoadFeature.cs | 11 + .../IServiceEndPointResolver.cs | 18 + .../IServiceEndPointResolverProvider.cs | 20 + .../IServiceEndPointSelector.cs | 23 ++ .../IServiceEndPointSelectorProvider.cs | 16 + .../Internal/ServiceEndPointImpl.cs | 24 ++ ...sions.ServiceDiscovery.Abstractions.csproj | 18 + .../ResolutionStatus.cs | 100 +++++ .../ResolutionStatusCode.cs | 40 ++ .../ServiceEndPoint.cs | 44 +++ .../ServiceEndPointCollection.cs | 87 +++++ .../ServiceEndPointCollectionSource.cs | 57 +++ .../ServiceEndPointResolverResult.cs | 30 ++ .../DnsServiceEndPointResolver.Log.cs | 40 ++ .../DnsServiceEndPointResolver.cs | 282 ++++++++++++++ .../DnsServiceEndPointResolverOptions.cs | 43 +++ .../DnsServiceEndPointResolverProvider.cs | 149 ++++++++ .../HostingExtensions.cs | 45 +++ ...oft.Extensions.ServiceDiscovery.Dns.csproj | 22 ++ ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 19 + ...ReverseProxyServiceCollectionExtensions.cs | 43 +++ .../ServiceDiscoveryDestinationResolver.cs | 93 +++++ ...viceDiscoveryForwarderHttpClientFactory.cs | 20 + .../ConfigurationServiceEndPointResolver.cs | 146 +++++++ ...igurationServiceEndPointResolverOptions.cs | 15 + ...gurationServiceEndPointResolverProvider.cs | 28 ++ .../HostingExtensions.cs | 83 ++++ .../Http/HttpClientBuilderExtensions.cs | 110 ++++++ .../Http/HttpServiceEndPointResolver.cs | 258 +++++++++++++ .../Http/ResolvingHttpClientHandler.cs | 45 +++ .../Http/ResolvingHttpDelegatingHandler.cs | 99 +++++ .../PassThroughServiceEndPointResolver.cs | 45 +++ .../Internal/ServiceNameParts.cs | 97 +++++ .../PickFirstServiceEndPointSelector.cs | 29 ++ ...ickFirstServiceEndPointSelectorProvider.cs | 18 + ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 +++ ...oChoicesServiceEndPointSelectorProvider.cs | 18 + .../RandomServiceEndPointSelector.cs | 29 ++ .../RandomServiceEndPointSelectorProvider.cs | 18 + .../RoundRobinServiceEndPointSelector.cs | 30 ++ ...undRobinServiceEndPointSelectorProvider.cs | 18 + ...crosoft.Extensions.ServiceDiscovery.csproj | 16 + .../ServiceEndPointResolver.cs | 358 ++++++++++++++++++ .../ServiceEndPointResolverFactory.cs | 56 +++ .../ServiceEndPointResolverOptions.cs | 22 ++ .../ServiceEndPointResolverRegistry.cs | 240 ++++++++++++ .../DnsServiceEndPointResolverTests.cs | 285 ++++++++++++++ ...tensions.ServiceDiscovery.Dns.Tests.csproj | 20 + ...nfigurationServiceEndPointResolverTests.cs | 135 +++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 19 + ...PassThroughServiceEndPointResolverTests.cs | 112 ++++++ .../ServiceEndPointResolverTests.cs | 192 ++++++++++ 53 files changed, 3847 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs new file mode 100644 index 00000000000..50276e05ecb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +public interface IEndPointHealthFeature +{ + // Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. + // Can be a no-op. + void ReportHealth(TimeSpan responseTime, Exception? exception); +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs new file mode 100644 index 00000000000..d58f23c7775 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +public interface IEndPointLoadFeature +{ + // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + public double CurrentLoad { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs new file mode 100644 index 00000000000..c228847c568 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Functionality for resolving endpoints for a service. +/// +public interface IServiceEndPointResolver : IAsyncDisposable +{ + /// + /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. + /// + /// The endpoint collection, which resolved endpoints will be added to. + /// The token to monitor for cancellation requests. + /// The resolution status. + ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..9ad9e3ae7b8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Creates instances. +/// +public interface IServiceEndPointResolverProvider +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the resolver for. + /// The resolver. + /// if the resolver was created, otherwise. + bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs new file mode 100644 index 00000000000..e2ffbd0421f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Selects endpoints from a collection of endpoints. +/// +public interface IServiceEndPointSelector +{ + /// + /// Sets the collection of endpoints which this instance will select from. + /// + /// The collection of endpoints to select from. + void SetEndPoints(ServiceEndPointCollection endPoints); + + /// + /// Selects an endpoints from the collection provided by the most recent call to . + /// + /// The context. + /// An endpoint. + ServiceEndPoint GetEndPoint(object? context); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..27f4ec4324a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Functionality for creating instances. +/// +public interface IServiceEndPointSelectorProvider +{ + /// + /// Creates an instance. + /// + /// A new instance. + IServiceEndPointSelector CreateSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs new file mode 100644 index 00000000000..0b54f5a19d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +internal sealed class ServiceEndPointImpl : ServiceEndPoint +{ + private readonly IFeatureCollection _features; + private readonly EndPoint _endPoint; + + public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) + { + _endPoint = endPoint; + _features = features ?? new FeatureCollection(); + } + + public override EndPoint EndPoint => _endPoint; + public override IFeatureCollection Features => _features; + + public override string? ToString() => _endPoint.ToString(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj new file mode 100644 index 00000000000..f89f1ba02ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs new file mode 100644 index 00000000000..152571bbb74 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Represents the status of an endpoint resolution operation. +/// +public readonly struct ResolutionStatus(ResolutionStatusCode statusCode, Exception? exception, string message) : IEquatable +{ + /// + /// Indicates that resolution was not performed. + /// + public static readonly ResolutionStatus None = new(ResolutionStatusCode.None, exception: null, message: ""); + + /// + /// Indicates that resolution is ongoing and has not yet completed. + /// + public static readonly ResolutionStatus Pending = new(ResolutionStatusCode.Pending, exception: null, message: "Pending"); + + /// + /// Indicates that resolution has completed successfully. + /// + public static readonly ResolutionStatus Success = new(ResolutionStatusCode.Success, exception: null, message: "Success"); + + /// + /// Indicates that resolution was cancelled. + /// + public static readonly ResolutionStatus Cancelled = new(ResolutionStatusCode.Cancelled, exception: null, message: "Cancelled"); + + /// + /// Indicates that resolution did not find a result for the service. + /// + public static ResolutionStatus CreateNotFound(string message) => new(ResolutionStatusCode.NotFound, exception: null, message: message); + + /// + /// Creates a status with a equal to with the provided exception. + /// + /// The resolution exception. + /// A new instance. + public static ResolutionStatus FromException(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + return new ResolutionStatus(ResolutionStatusCode.Error, exception, exception.Message); + } + + /// + /// Creates a status with a equal to with the provided exception. + /// + /// The resolution exception, if there was one. + /// A new instance. + public static ResolutionStatus FromPending(Exception? exception = null) + { + ArgumentNullException.ThrowIfNull(exception); + return new ResolutionStatus(ResolutionStatusCode.Pending, exception, exception.Message); + } + + /// + /// Gets the resolution status code. + /// + public ResolutionStatusCode StatusCode { get; } = statusCode; + + /// + /// Gets the resolution exception. + /// + + public Exception? Exception { get; } = exception; + + /// + /// Gets the resolution status message. + /// + public string Message { get; } = message; + + /// + /// Compares the provided operands, returning if they are equal and if they are not equal. + /// + public static bool operator ==(ResolutionStatus left, ResolutionStatus right) => left.Equals(right); + + /// + /// Compares the provided operands, returning if they are not equal and if they are equal. + /// + public static bool operator !=(ResolutionStatus left, ResolutionStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ResolutionStatus status && Equals(status); + + /// + public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && + EqualityComparer.Default.Equals(Exception, other.Exception) && + Message == other.Message; + + /// + public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message); + + public override string ToString() => Exception switch + { + not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]", + _ => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}]" + }; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs new file mode 100644 index 00000000000..7157eac758f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Status codes for . +/// +public enum ResolutionStatusCode +{ + /// + /// Resolution has not been performed. + /// + None = 0, + + /// + /// Resolution is pending completion. + /// + Pending = 1, + + /// + /// Resolution did not find any end points for the specified service. + /// + NotFound = 2, + + /// + /// Resolution was successful. + /// + Success = 3, + + /// + /// Resolution was canceled. + /// + Cancelled = 4, + + /// + /// Resolution failed. + /// + Error = 5, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs new file mode 100644 index 00000000000..9dc4675dade --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Represents an endpoint for a service. +/// +[DebuggerDisplay("{GetEndPointString(),nq}")] +public abstract class ServiceEndPoint +{ + /// + /// Gets the endpoint. + /// + public abstract EndPoint EndPoint { get; } + + /// + /// Gets the collection of endpoint features. + /// + public abstract IFeatureCollection Features { get; } + + /// + /// Creates a new . + /// + /// The endpoint being represented. + /// Features of the endpoint. + /// A newly initialized . + public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); + + /// + /// Gets a string representation of the . + /// + /// A string representation of the . + public virtual string GetEndPointString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + IPEndPoint ip => ip.ToString(), + _ => EndPoint.ToString()! + }; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs new file mode 100644 index 00000000000..57919f949c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Represents an immutable collection of service endpoints. +/// +[DebuggerDisplay("{ToString(),nq}")] +[DebuggerTypeProxy(nameof(ServiceEndPointCollectionDebuggerView))] +public class ServiceEndPointCollection : IReadOnlyList +{ + private readonly List? _endpoints; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The endpoints. + /// The change token. + /// The feature collection. + public ServiceEndPointCollection(string serviceName, List? endpoints, IChangeToken changeToken, IFeatureCollection features) + { + ArgumentNullException.ThrowIfNull(serviceName); + ArgumentNullException.ThrowIfNull(changeToken); + + _endpoints = endpoints; + Features = features; + ServiceName = serviceName; + ChangeToken = changeToken; + } + + /// + public ServiceEndPoint this[int index] => _endpoints?[index] ?? throw new ArgumentOutOfRangeException(nameof(index)); + + /// + /// Gets the service name. + /// + public string ServiceName { get; } + + /// + /// Gets the change token which indicates when this collection should be refreshed. + /// + public IChangeToken ChangeToken { get; } + + /// + /// Gets the feature collection. + /// + public IFeatureCollection Features { get; } + + /// + public int Count => _endpoints?.Count ?? 0; + + /// + public IEnumerator GetEnumerator() => _endpoints?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public override string ToString() + { + if (_endpoints is not { } eps) + { + return "[]"; + } + + return $"[{string.Join(", ", eps)}]"; + } + + private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value) + { + public string ServiceName => value.ServiceName; + + public IChangeToken ChangeToken => value.ChangeToken; + + public IFeatureCollection Features => value.Features; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public ServiceEndPoint[] EndPoints => value.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs new file mode 100644 index 00000000000..94f274a38e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// A mutable collection of service endpoints. +/// +public class ServiceEndPointCollectionSource(string serviceName, IFeatureCollection features) +{ + private readonly List _endPoints = new(); + private readonly List _changeTokens = new(); + + /// + /// Gets the service name. + /// + public string ServiceName { get; } = serviceName; + + /// + /// Adds a change token. + /// + /// The change token. + public void AddChangeToken(IChangeToken changeToken) + { + _changeTokens.Add(changeToken); + } + + /// + /// Gets the composite change token. + /// + /// The composite change token. + public IChangeToken GetChangeToken() => new CompositeChangeToken(_changeTokens); + + /// + /// Gets the feature collection. + /// + public IFeatureCollection Features { get; } = features; + + /// + /// Gets the endpoints. + /// + public IList EndPoints => _endPoints; + + /// + /// Creates a from the provided instance. + /// + /// The source collection. + /// The service endpoint collection. + public static ServiceEndPointCollection CreateServiceEndPointCollection(ServiceEndPointCollectionSource source) + { + return new ServiceEndPointCollection(source.ServiceName, source._endPoints, source.GetChangeToken(), source.Features); + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..9179ed2f113 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Represents the result of service endpoint resolution. +/// +/// The endpoint collection. +/// The status. +public sealed class ServiceEndPointResolverResult(ServiceEndPointCollection? endPoints, ResolutionStatus status) +{ + /// + /// Gets the status. + /// + public ResolutionStatus Status { get; } = status; + + /// + /// Gets a value indicating whether resolution completed successfully. + /// + [MemberNotNullWhen(true, nameof(EndPoints))] + public bool ResolvedSuccessfully => Status.StatusCode is ResolutionStatusCode.Success; + + /// + /// Gets the endpoints. + /// + public ServiceEndPointCollection? EndPoints { get; } = endPoints; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..6ace4ac983e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal partial class DnsServiceEndPointResolver +{ + private static partial class Log + { + [LoggerMessage(1, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using DNS SRV lookup for name '{RecordName}'.", EventName = "SrvQuery")] + public static partial void SrvQuery(ILogger logger, string serviceName, string recordName); + + [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] + public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); + + public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); + } + else if (logger.IsEnabled(LogLevel.Debug)) + { + DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); + } + } + + [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] + public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); + + [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] + public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); + + [LoggerMessage(4, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs new file mode 100644 index 00000000000..63d60d5318d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// A service end point resolver that uses DNS to resolve the service end points. +/// +internal sealed partial class DnsServiceEndPointResolver : IServiceEndPointResolver +{ + private readonly object _lock = new(); + private readonly string _serviceName; + private readonly Stopwatch _lastRefreshTimer = new(); + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly CancellationTokenSource _disposeCancellation = new(); + private readonly IDnsQuery _dnsClient; + private readonly TimeProvider _timeProvider; + private Task _resolveTask = Task.CompletedTask; + private ResolutionStatus _lastStatus; + private IChangeToken? _lastChangeToken; + private CancellationTokenSource _lastCollectionCancellation; + private List? _lastEndPointCollection; + private readonly string _addressRecordName; + private readonly string _srvRecordName; + private readonly int _defaultPort; + private TimeSpan _nextRefreshPeriod; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The name used to resolve the address of this service. + /// The name used to resolve this service's SRV record in DNS. + /// The default port to use for endpoints. + /// The options. + /// The logger. + /// The DNS client. + /// The time provider. + public DnsServiceEndPointResolver( + string serviceName, + string addressRecordName, + string srvRecordName, + int defaultPort, + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) + { + _serviceName = serviceName; + _options = options; + _logger = logger; + _lastEndPointCollection = null; + _addressRecordName = addressRecordName; + _srvRecordName = srvRecordName; + _defaultPort = defaultPort; + _dnsClient = dnsClient; + _nextRefreshPeriod = _options.CurrentValue.MinRetryPeriod; + _timeProvider = timeProvider; + var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(_options.CurrentValue.DefaultRefreshPeriod); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + } + + /// + public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. + if (endPoints.EndPoints.Count != 0) + { + return ResolutionStatus.None; + } + + if (ShouldRefresh()) + { + Task resolveTask; + lock (_lock) + { + if (_resolveTask.IsCompleted && ShouldRefresh()) + { + _resolveTask = ResolveAsyncInternal(); + } + + resolveTask = _resolveTask; + } + + await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + lock (_lock) + { + if (_lastEndPointCollection is { Count: > 0 } eps) + { + foreach (var ep in eps) + { + endPoints.EndPoints.Add(ep); + } + } + + if (_lastChangeToken is not null) + { + endPoints.AddChangeToken(_lastChangeToken); + } + + return _lastStatus; + } + } + + private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || _lastRefreshTimer.Elapsed >= _nextRefreshPeriod; + + private async Task ResolveAsyncInternal() + { + var endPoints = new List(); + var options = _options.CurrentValue; + var ttl = options.DefaultRefreshPeriod; + try + { + if (options.UseSrvQuery) + { + Log.SrvQuery(_logger, _serviceName, _srvRecordName); + var result = await _dnsClient.QueryAsync(_srvRecordName, QueryType.SRV).ConfigureAwait(false); + if (result.HasError) + { + SetException(CreateException(result.ErrorMessage), ttl); + return; + } + + var lookupMapping = new Dictionary(); + foreach (var record in result.Additionals) + { + ttl = MinTtl(record, ttl); + lookupMapping[record.DomainName] = record; + } + + var srvRecords = result.Answers.OfType(); + foreach (var record in srvRecords) + { + if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) + { + continue; + } + + ttl = MinTtl(record, ttl); + if (targetRecord is AddressRecord addressRecord) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + } + else if (targetRecord is CNameRecord canonicalNameRecord) + { + endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + } + } + } + else + { + Log.AddressQuery(_logger, _serviceName, _addressRecordName); + var addresses = await System.Net.Dns.GetHostAddressesAsync(_addressRecordName, _disposeCancellation.Token).ConfigureAwait(false); + foreach (var address in addresses) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, _defaultPort))); + } + + if (endPoints.Count == 0) + { + SetException(CreateException(), ttl); + return; + } + } + + SetResult(endPoints, ttl); + } + catch (Exception exception) + { + SetException(exception, ttl); + throw; + } + + static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + { + var candidate = TimeSpan.FromSeconds(record.TimeToLive); + return candidate < existing ? candidate : existing; + } + + InvalidOperationException CreateException(string? errorMessage = null) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS records were found for service {_serviceName}: {errorMessage}.", + _ => $"No DNS records were found for service {_serviceName}." + }; + var exception = new InvalidOperationException(msg); + return exception; + } + } + + private void SetException(Exception exception, TimeSpan validityPeriod) => SetResult(endPoints: null, exception, validityPeriod); + private void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); + private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + { + lock (_lock) + { + var options = _options.CurrentValue; + if (exception is not null) + { + if (_lastStatus.Exception is null) + { + _nextRefreshPeriod = options.MinRetryPeriod; + } + else + { + var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * options.RetryBackOffFactor)); + _nextRefreshPeriod = nextPeriod > options.MaxRetryPeriod ? options.MaxRetryPeriod : nextPeriod; + } + + if (_lastEndPointCollection is null) + { + // Since end points have never been resolved, use a pending status to indicate that they might appear + // soon and to retry for some period until they do. + _lastStatus = ResolutionStatus.FromPending(exception); + } + else + { + _lastStatus = ResolutionStatus.FromException(exception); + } + } + else + { + _lastRefreshTimer.Restart(); + _nextRefreshPeriod = options.DefaultRefreshPeriod; + _lastStatus = ResolutionStatus.Success; + } + + validityPeriod = validityPeriod > TimeSpan.Zero && validityPeriod < _nextRefreshPeriod ? validityPeriod : _nextRefreshPeriod; + _lastCollectionCancellation.Cancel(); + var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(validityPeriod); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + _lastEndPointCollection = endPoints; + } + + if (exception is null) + { + Debug.Assert(endPoints is not null); + Log.DiscoveredEndPoints(_logger, endPoints, _serviceName, validityPeriod); + } + else + { + Log.ResolutionFailed(_logger, exception, _serviceName); + } + } + + /// + public async ValueTask DisposeAsync() + { + _disposeCancellation.Cancel(); + + if (_resolveTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private CancellationTokenSource CreateCancellationTokenSource(TimeSpan validityPeriod) + { + if (validityPeriod <= TimeSpan.Zero) + { + // Do not invalidate on a timer, but invalidate on refresh. + return new CancellationTokenSource(); + } + else + { + return new CancellationTokenSource(validityPeriod, _timeProvider); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..45adebfea0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Options for configuring . +/// +public class DnsServiceEndPointResolverOptions +{ + /// + /// Gets or sets the default refresh period for endpoints resolved from DNS. + /// + public TimeSpan DefaultRefreshPeriod { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the initial period between retries. + /// + public TimeSpan MinRetryPeriod { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the maximum period between retries. + /// + public TimeSpan MaxRetryPeriod { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the retry period growth factor. + /// + public double RetryBackOffFactor { get; set; } = 2; + + /// + /// Gets or sets the default DNS namespace for services resolved via this provider. + /// + /// + /// If not specified, the provider will attempt to infer the namespace. + /// + public string? DnsNamespace { get; set; } + + /// + /// Gets or sets a value indicating whether to use DNS SRV queries to discover host addresses and ports. + /// + public bool UseSrvQuery { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..4a554d7c9e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using DnsClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides instances which resolve endpoints from DNS. +/// +internal sealed partial class DnsServiceEndPointResolverProvider : IServiceEndPointResolverProvider +{ + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly IDnsQuery _dnsClient; + private readonly TimeProvider _timeProvider; + private readonly string? _defaultNamespace; + + /// + /// Initializes a new instance. + /// + /// The options. + /// The logger. + /// The DNS client. + /// The time provider. + public DnsServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) + { + _options = options; + _logger = logger; + _dnsClient = dnsClient; + _timeProvider = timeProvider; + _defaultNamespace = options.CurrentValue.DnsNamespace ?? GetHostNamespace(); + } + + // RFC 2181 + // DNS hostnames can consist only of letters, digits, dots, and hyphens. + // They must begin with a letter. + // They must end with a letter or a digit. + // Individual segments (between dots) can be no longer than 63 characters. + [GeneratedRegex("^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$")] + private static partial Regex ValidDnsName(); + + // Adapted version of Tim Berners Lee's regex from the URI spec: https://stackoverflow.com/a/26766402 + // Adapted to parse the port into a group and discard groups which we do not care about. + [GeneratedRegex("^(?:([^:/?#]+)://)?([^/?#:]*)?(?::([\\d]+))?")] + private static partial Regex UriRegex(); + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md + // SRV records are available for headless services with named ports. + // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.svc.{zone}" + // We can fetch the namespace from /var/run/secrets/kubernetes.io/serviceaccount/namespace + // The protocol is assumed to be "tcp". + // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". + var dnsServiceName = serviceName; + var dnsNamespace = _defaultNamespace; + var portName = "default"; + var defaultPortNumber = 0; + + // Allow the service name to be expressed as either a URI or a plain DNS name. + var uri = UriRegex().Match(serviceName); + if (uri.Success) + { + if (uri.Groups[1].ValueSpan is { Length: > 0 } uriPortNameSpan) + { + // Override the port name if it was specified in the service name + portName = uriPortNameSpan.ToString(); + } + + if (int.TryParse(uri.Groups[3].ValueSpan, out var uriDefaultPort)) + { + // Override the default port if it was specified in the service name + defaultPortNumber = uriDefaultPort; + } + + // Since the service name was URI-formatted, we should extract the hostname part for resolution. + dnsServiceName = uri.Groups[2].Value; + } + else if (!ValidDnsName().IsMatch(serviceName)) + { + resolver = default; + return false; + } + + // If the DNS name is not qualified, and we have a qualifier, apply it. + if (!dnsServiceName.Contains('.') && dnsNamespace is not null) + { + dnsServiceName = $"{dnsServiceName}.{dnsNamespace}"; + } + + var srvRecordName = $"_{portName}._tcp.{dnsServiceName}"; + resolver = new DnsServiceEndPointResolver(serviceName, dnsServiceName, srvRecordName, defaultPortNumber, _options, _logger, _dnsClient, _timeProvider); + return true; + } + + private static string? GetHostNamespace() => ReadNamespaceFromKubernetesServiceAccount() ?? ReadQualifiedNamespaceFromResolvConf(); + + private static string? ReadNamespaceFromKubernetesServiceAccount() + { + if (OperatingSystem.IsLinux()) + { + // Read the namespace from the Kubernetes pod's service account. + var serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); + if (File.Exists(serviceAccountNamespacePath)) + { + return File.ReadAllText(serviceAccountNamespacePath).Trim(); + } + } + + return null; + } + + private static string? ReadQualifiedNamespaceFromResolvConf() + { + if (OperatingSystem.IsLinux()) + { + var resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); + if (File.Exists(resolveConfPath)) + { + var lines = File.ReadAllLines(resolveConfPath); + foreach (var line in lines) + { + if (line.StartsWith("search ")) + { + var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if (components.Length > 1) + { + return components[1]; + } + } + } + } + } + + return default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs new file mode 100644 index 00000000000..49351af386d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DnsClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Dns; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extensions for to add service discovery. +/// +public static class HostingExtensions +{ + /// + /// Adds DNS-based service discovery to the . + /// + /// The service collection. + /// The DNS service discovery configuration options. + /// The provided . + public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.Configure(options => options.UseSrvQuery = true); + return services.AddDnsServiceEndPointResolver(configureOptions); + } + + /// + /// Adds DNS-based service discovery to the . + /// + /// The service collection. + /// The DNS service discovery configuration options. + /// The provided . + public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.AddServiceDiscoveryCore(); + services.TryAddSingleton(); + services.AddSingleton(); + var options = services.AddOptions(); + configureOptions?.Invoke(options); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj new file mode 100644 index 00000000000..7f208a3f84a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -0,0 +1,22 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj new file mode 100644 index 00000000000..eaa6cebf61c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -0,0 +1,19 @@ + + + + $(NetCurrent) + enable + enable + true + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs new file mode 100644 index 00000000000..e52ff65ee2a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Yarp; +using Yarp.ReverseProxy.Forwarder; +using Yarp.ReverseProxy.ServiceDiscovery; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions for used to register the ReverseProxy's components. +/// +public static class ReverseProxyServiceCollectionExtensions +{ + /// + /// Provides a implementation which uses service discovery to resolve destinations. + /// + public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) + { + builder.Services.AddServiceDiscoveryCore(); + builder.Services.AddSingleton(); + return builder; + } + + /// + /// Adds the with service discovery support. + /// + public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) + { + return services.AddHttpForwarder().AddServiceDiscoveryForwarderFactory(); + } + + /// + /// Provides a implementation which uses service discovery to resolve service names. + /// + public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs new file mode 100644 index 00000000000..fbcef8c72ad --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; +using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.ServiceDiscovery; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp; + +/// +/// Implementation of which resolves destinations using service discovery. +/// +/// +/// Initializes a new instance. +/// +/// The endpoint resolver registry. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolverRegistry registry) : IDestinationResolver +{ + /// + public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) + { + Dictionary results = new(); + var tasks = new List, IChangeToken ChangeToken)>>(destinations.Count); + foreach (var (destinationId, destinationConfig) in destinations) + { + tasks.Add(ResolveHostAsync(destinationId, destinationConfig, cancellationToken)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + var changeTokens = new List(); + foreach (var task in tasks) + { + var (configs, changeToken) = await task.ConfigureAwait(false); + if (changeToken is not null) + { + changeTokens.Add(changeToken); + } + + foreach (var (name, config) in configs) + { + results[name] = config; + } + } + + return new ResolvedDestinationCollection(results, new CompositeChangeToken(changeTokens)); + } + + private async Task<(List<(string Name, DestinationConfig Config)>, IChangeToken ChangeToken)> ResolveHostAsync( + string originalName, + DestinationConfig originalConfig, + CancellationToken cancellationToken) + { + var originalUri = new Uri(originalConfig.Address); + var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; + var serviceName = originalUri.GetLeftPart(UriPartial.Authority); + + var endPoints = await registry.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); + var uriBuilder = new UriBuilder(originalUri); + var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; + var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; + foreach (var endPoint in endPoints) + { + var addressString = endPoint.GetEndPointString(); + Uri result; + if (!addressString.Contains("://")) + { + result = new Uri($"https://{addressString}"); + } + else + { + result = new Uri(addressString); + } + + uriBuilder.Host = result.Host; + uriBuilder.Port = result.Port; + var resolvedAddress = uriBuilder.Uri.ToString(); + var healthAddress = originalConfig.Health; + if (healthUriBuilder is not null) + { + healthUriBuilder.Host = result.Host; + healthUriBuilder.Port = result.Port; + healthAddress = healthUriBuilder.Uri.ToString(); + } + + var name = $"{originalName}[{addressString}]"; + var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + results.Add((name, config)); + } + + return (results, endPoints.ChangeToken); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs new file mode 100644 index 00000000000..0da7e8b55eb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Http; +using Yarp.ReverseProxy.Forwarder; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp; + +internal sealed class ServiceDiscoveryForwarderHttpClientFactory( + TimeProvider timeProvider, + IServiceEndPointSelectorProvider selectorProvider, + ServiceEndPointResolverFactory factory) : ForwarderHttpClientFactory +{ + protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) + { + var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry, handler); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs new file mode 100644 index 00000000000..4f511cbf37b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -0,0 +1,146 @@ +// 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; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// A service endpoint resolver that uses configuration to resolve endpoints. +/// +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver +{ + private readonly string _serviceName; + private readonly string? _endpointName; + private readonly IConfiguration _configuration; + private readonly IOptions _options; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The configuration. + /// The options. + public ConfigurationServiceEndPointResolver( + string serviceName, + IConfiguration configuration, + IOptions options) + { + if (ServiceNameParts.TryParse(serviceName, out var parts)) + { + _serviceName = parts.Host; + _endpointName = parts.EndPointName; + } + else + { + throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + } + + _configuration = configuration; + _options = options; + } + + /// + public ValueTask DisposeAsync() => default; + + /// + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); + + private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) + { + // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. + if (endPoints.EndPoints.Count != 0) + { + return ResolutionStatus.None; + } + + var root = _configuration; + var baseSectionName = _options.Value.SectionName; + if (baseSectionName is { Length: > 0 }) + { + root = root.GetSection(baseSectionName); + } + + // Get the corresponding config section. + var section = root.GetSection(_serviceName); + if (!section.Exists()) + { + return CreateNotFoundResponse(endPoints, baseSectionName); + } + + // Read the endpoint from the configuration. + // First check if there is a collection of sections + if (section.GetChildren().Any()) + { + var values = section.Get>(); + if (values is { Count: > 0 }) + { + // Use schemes if any of the URIs have a scheme set. + var uris = ParseServiceNameParts(values); + var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + foreach (var uri in uris) + { + // If either schemes are not in-use or the scheme matches, create an endpoint for this value + if (!useSchemes || SchemesMatch(_endpointName, uri)) + { + if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + } + } + } + } + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + { + if (SchemesMatch(_endpointName, uri)) + { + if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + } + } + + endPoints.AddChangeToken(section.GetReloadToken()); + return ResolutionStatus.Success; + + static bool SchemesMatch(string? scheme, ServiceNameParts parts) => + (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) + || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + } + + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + { + var configPath = new StringBuilder(); + if (baseSectionName is { Length: > 0 }) + { + configPath.Append(baseSectionName).Append(':'); + } + + configPath.Append(_serviceName); + endPoints.AddChangeToken(_configuration.GetReloadToken()); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + } + + private static List ParseServiceNameParts(List input) + { + var results = new List(input.Count); + for (var i = 0; i < input.Count; ++i) + { + if (ServiceNameParts.TryParse(input[i], out var value)) + { + results.Add(value); + } + } + + return results; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..a20d12d771c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Options for . +/// +public class ConfigurationServiceEndPointResolverOptions +{ + /// + /// The name of the configuration section which contains service endpoints. Defaults to "Services". + /// + public string? SectionName { get; set; } = "Services"; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..5c35b6161fd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// implementation that resolves services using . +/// +/// The configuration. +/// The options. +public class ConfigurationServiceEndPointResolverProvider( + IConfiguration configuration, + IOptions options) : IServiceEndPointResolverProvider +{ + private readonly IConfiguration _configuration = configuration; + private readonly IOptions _options = options; + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs new file mode 100644 index 00000000000..53e4c1f5bda --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for configuring service discovery. +/// +public static class HostingExtensions +{ + /// + /// Adds the core service discovery services and configures defaults. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) + { + return services.AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .AddPassThroughServiceEndPointResolver(); + } + + /// + /// Adds the core service discovery services. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) + { + services.AddOptions(); + services.AddLogging(); + services.TryAddSingleton(static sp => TimeProvider.System); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } + + /// + /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + { + return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + } + + /// + /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// + /// The delegate used to configure the provider. + /// The service collection. + /// The service collection. + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + var options = services.AddOptions(); + configureOptions?.Invoke(options); + return services; + } + + /// + /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs new file mode 100644 index 00000000000..7e9df30b770 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Http; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions for configuring with service discovery. +/// +public static class HttpClientBuilderExtensions +{ + /// + /// Adds service discovery to the . + /// + /// The builder. + /// The provider that creates selector instances. + /// The builder. + public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder, IServiceEndPointSelectorProvider selectorProvider) + { + var services = httpClientBuilder.Services; + services.AddServiceDiscoveryCore(); + httpClientBuilder.AddHttpMessageHandler(services => + { + var timeProvider = services.GetService() ?? TimeProvider.System; + var resolverProvider = services.GetRequiredService(); + var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry); + }); + + return httpClientBuilder; + } + + /// + /// Adds service discovery to the . + /// + /// The builder. + /// The builder. + public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) + { + var services = httpClientBuilder.Services; + services.AddServiceDiscoveryCore(); + httpClientBuilder.AddHttpMessageHandler(services => + { + var timeProvider = services.GetService() ?? TimeProvider.System; + + var selectorProvider = services.GetRequiredService(); + var resolverProvider = services.GetRequiredService(); + var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry); + }); + + // Configure the HttpClient to disable gRPC load balancing. + // This is done on all HttpClient instances but only impacts gRPC clients. + AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); + + return httpClientBuilder; + } + + private static void AddDisableGrpcLoadBalancingFilter(IServiceCollection services, string? name) + { + // A filter is used because it will always run last. This is important because the disable + // property needs to be added to all SocketsHttpHandler instances, including those specified + // with ConfigurePrimaryHttpMessageHandler. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.Configure(o => o.ClientNames.Add(name)); + } + + private sealed class DisableGrpcLoadBalancingFilterOptions + { + // Names of clients. A null value means it is applied globally to all clients. + public HashSet ClientNames { get; } = new HashSet(); + } + + private sealed class DisableGrpcLoadBalancingFilter : IHttpMessageHandlerBuilderFilter + { + private readonly DisableGrpcLoadBalancingFilterOptions _options; + private readonly bool _global; + + public DisableGrpcLoadBalancingFilter(IOptions options) + { + _options = options.Value; + _global = _options.ClientNames.Contains(null); + } + + public Action Configure(Action next) + { + return (builder) => + { + // Run other configuration first, we want to decorate. + next(builder); + if (_global || _options.ClientNames.Contains(builder.Name)) + { + if (builder.PrimaryHandler is SocketsHttpHandler socketsHttpHandler) + { + // gRPC knows about this property and uses it to check whether + // load balancing is disabled when the GrpcChannel is created. + socketsHttpHandler.Properties["__GrpcLoadBalancingDisabled"] = true; + } + } + }; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs new file mode 100644 index 00000000000..e9e80bc76bb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -0,0 +1,258 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// Resolves endpoints for HTTP requests. +/// +public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +{ + private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); + + private readonly object _lock = new(); + private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; + private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + + /// + /// Resolves and returns a service endpoint for the specified request. + /// + /// The request message. + /// The cancellation token. + /// The resolved service endpoint. + /// The request had no set or a suitable endpoint could not be found. + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (request.RequestUri is null) + { + throw new InvalidOperationException("Cannot resolve an endpoint for a request which has no RequestUri"); + } + + EnsureCleanupTimerStarted(); + + var key = request.RequestUri.GetLeftPart(UriPartial.Authority); + while (true) + { + var resolver = _resolvers.GetOrAdd( + key, + static (name, self) => self.CreateResolver(name), + this); + + var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + if (valid) + { + if (endPoint is null) + { + throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); + } + + return endPoint; + } + } + } + + private void EnsureCleanupTimerStarted() + { + if (_cleanupTimer is not null) + { + return; + } + + lock (_lock) + { + if (_cleanupTimer is not null) + { + return; + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + + foreach (var resolver in _resolvers) + { + await resolver.Value.DisposeAsync().ConfigureAwait(false); + } + + _resolvers.Clear(); + if (_cleanupTask is not null) + { + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private void CleanupResolvers() + { + lock (_lock) + { + if (_cleanupTask is { IsCompleted: true }) + { + _cleanupTask = CleanupResolversAsyncCore(); + } + } + } + + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) + { + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } + } + if (cleanupTasks is not null) + { + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); + } + } + + private ResolverEntry CreateResolver(string serviceName) + { + var resolver = _resolverProvider.CreateResolver(serviceName); + var selector = _selectorProvider.CreateSelector(); + var result = new ResolverEntry(resolver, selector); + resolver.Start(); + return result; + } + + private sealed class ResolverEntry : IAsyncDisposable + { + private readonly ServiceEndPointResolver _resolver; + private readonly IServiceEndPointSelector _selector; + private const ulong CountMask = unchecked((ulong)-1); + private const ulong RecentUseFlag = 1UL << 61; + private const ulong DisposingFlag = 1UL << 62; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public ResolverEntry(ServiceEndPointResolver resolver, IServiceEndPointSelector selector) + { + _resolver = resolver; + _selector = selector; + _resolver.OnEndPointsUpdated += result => + { + if (result.ResolvedSuccessfully) + { + _selector.SetEndPoints(result.EndPoints); + } + }; + } + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() + { + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); + + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } + + public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + { + try + { + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndPoint(context); + return (true, result); + } + else + { + return (false, default); + } + } + finally + { + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) + { + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); + } + + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } + } + + private async Task DisposeAsyncCore() + { + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs new file mode 100644 index 00000000000..1edf4cf4898 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// which resolves endpoints using service discovery. +/// +public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver) : HttpClientHandler +{ + private readonly HttpServiceEndPointResolver _resolver = resolver; + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var originalUri = request.RequestUri; + IEndPointHealthFeature? epHealth = null; + Exception? error = null; + var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + epHealth = result.Features.Get(); + } + + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + error = exception; + throw; + } + finally + { + epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + request.RequestUri = originalUri; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs new file mode 100644 index 00000000000..b8e0368c5e3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// HTTP message handler which resolves endpoints using service discovery. +/// +public class ResolvingHttpDelegatingHandler : DelegatingHandler +{ + private readonly HttpServiceEndPointResolver _resolver; + + /// + /// Initializes a new instance. + /// + /// The endpoint resolver. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver) + { + _resolver = resolver; + } + + /// + /// Initializes a new instance. + /// + /// The endpoint resolver. + /// The inner handler. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, HttpMessageHandler innerHandler) : base(innerHandler) + { + _resolver = resolver; + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var originalUri = request.RequestUri; + IEndPointHealthFeature? epHealth = null; + Exception? error = null; + var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = GetUriWithEndPoint(originalUri, result); + epHealth = result.Features.Get(); + } + + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + error = exception; + throw; + } + finally + { + epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + request.RequestUri = originalUri; + } + } + + internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint) + { + var endpoint = serviceEndPoint.EndPoint; + + string host; + int port; + switch (endpoint) + { + case IPEndPoint ip: + host = ip.Address.ToString(); + port = ip.Port; + break; + case DnsEndPoint dns: + host = dns.Host; + port = dns.Port; + break; + default: + throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + } + + var builder = new UriBuilder(uri) + { + Host = host, + }; + + // Default to the default port for the scheme. + if (port > 0) + { + builder.Port = port; + } + + return builder.Uri; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..15e566c5887 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +/// +/// Service endpoint resolver provider which passes through the provided value. +/// +internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +{ + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) + { + // Propagate the value through regardless, leaving it to the caller to interpret it. + endPoint = new DnsEndPoint(serviceName, 0); + } + + resolver = new PassThroughServiceEndPointResolver(endPoint); + return true; + } + + private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver + { + private readonly EndPoint _endPoint = endPoint; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + if (endPoints.EndPoints.Count != 0) + { + return new(ResolutionStatus.None); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs new file mode 100644 index 00000000000..8b2a03acd31 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal readonly struct ServiceNameParts +{ + public ServiceNameParts(string host, string? endPointName, int port) : this() + { + Host = host; + EndPointName = endPointName; + Port = port; + } + + public string? EndPointName { get; init; } + + public string Host { get; init; } + + public int Port { get; init; } + + public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) + { + if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) + { + parts = Create(uri, hasScheme: false); + return true; + } + + if (Uri.TryCreate(serviceName, default, out uri)) + { + parts = Create(uri, hasScheme: true); + return true; + } + + parts = default; + return false; + + static ServiceNameParts Create(Uri uri, bool hasScheme) + { + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + if (hasScheme) + { + endPointName = uri.Scheme; + } + } + + return new(host, endPointName, uri.Port); + } + } + + public static bool TryCreateEndPoint(ServiceNameParts parts, [NotNullWhen(true)] out EndPoint? endPoint) + { + if (IPAddress.TryParse(parts.Host, out var ip)) + { + endPoint = new IPEndPoint(ip, parts.Port); + } + else if (!string.IsNullOrEmpty(parts.Host)) + { + endPoint = new DnsEndPoint(parts.Host, parts.Port); + } + else + { + endPoint = null; + return false; + } + + return true; + } + + public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + { + if (TryParse(serviceName, out var parts)) + { + return TryCreateEndPoint(parts, out serviceEndPoint); + } + + serviceEndPoint = null; + return false; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs new file mode 100644 index 00000000000..9395896e520 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// A service endpoint selector which always returns the first endpoint in a collection. +/// +public class PickFirstServiceEndPointSelector : IServiceEndPointSelector +{ + private ServiceEndPointCollection? _endPoints; + + /// + public ServiceEndPoint GetEndPoint(object? context) + { + if (_endPoints is not { Count: > 0 } endPoints) + { + throw new InvalidOperationException("The endpoint collection contains no endpoints"); + } + + return endPoints[0]; + } + + /// + public void SetEndPoints(ServiceEndPointCollection endPoints) + { + _endPoints = endPoints; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..d3f657c9550 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Provides instances of . +/// +public class PickFirstServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider +{ + /// + /// Gets a shared instance of this class. + /// + public static PickFirstServiceEndPointSelectorProvider Instance { get; } = new(); + + /// + public IServiceEndPointSelector CreateSelector() => new PickFirstServiceEndPointSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs new file mode 100644 index 00000000000..e233dfb7b5c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Selects endpoints using the Power of Two Choices algorithm for distributed load balancing based on +/// the last-known load of the candidate endpoints. +/// +public class PowerOfTwoChoicesServiceEndPointSelector : IServiceEndPointSelector +{ + private ServiceEndPointCollection? _endPoints; + + /// + public void SetEndPoints(ServiceEndPointCollection endPoints) + { + _endPoints = endPoints; + } + + /// + public ServiceEndPoint GetEndPoint(object? context) + { + if (_endPoints is not { Count: > 0 } collection) + { + throw new InvalidOperationException("The endpoint collection contains no endpoints"); + } + + if (collection.Count == 1) + { + return collection[0]; + } + + var first = collection[Random.Shared.Next(collection.Count)]; + ServiceEndPoint second; + do + { + second = collection[Random.Shared.Next(collection.Count)]; + } while (ReferenceEquals(first, second)); + + // Note that this relies on fresh data to be effective. + if (first.Features.Get() is { } firstLoad + && second.Features.Get() is { } secondLoad) + { + return firstLoad.CurrentLoad < secondLoad.CurrentLoad ? first : second; + } + + // Degrade to random. + return first; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..00832bc7811 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Provides instances of . +/// +public class PowerOfTwoChoicesServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider +{ + /// + /// Gets a shared instance of this class. + /// + public static PowerOfTwoChoicesServiceEndPointSelectorProvider Instance { get; } = new(); + + /// + public IServiceEndPointSelector CreateSelector() => new PowerOfTwoChoicesServiceEndPointSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs new file mode 100644 index 00000000000..8e4bb2378d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// A service endpoint selector which returns random endpoints from the collection. +/// +public class RandomServiceEndPointSelector : IServiceEndPointSelector +{ + private ServiceEndPointCollection? _endPoints; + + /// + public void SetEndPoints(ServiceEndPointCollection endPoints) + { + _endPoints = endPoints; + } + + /// + public ServiceEndPoint GetEndPoint(object? context) + { + if (_endPoints is not { Count: > 0 } collection) + { + throw new InvalidOperationException("The endpoint collection contains no endpoints"); + } + + return collection[Random.Shared.Next(collection.Count)]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..ae74b4032bc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Provides instances of . +/// +public class RandomServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider +{ + /// + /// Gets a shared instance of this class. + /// + public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); + + /// + public IServiceEndPointSelector CreateSelector() => new RandomServiceEndPointSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs new file mode 100644 index 00000000000..5848c7d8f72 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. +/// +public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +{ + private uint _next; + private ServiceEndPointCollection? _endPoints; + + /// + public void SetEndPoints(ServiceEndPointCollection endPoints) + { + _endPoints = endPoints; + } + + /// + public ServiceEndPoint GetEndPoint(object? context) + { + if (_endPoints is not { Count: > 0 } collection) + { + throw new InvalidOperationException("The endpoint collection contains no endpoints"); + } + + return collection[(int)(Interlocked.Increment(ref _next) % collection.Count)]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..ed1e79d7416 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Provides instances of . +/// +public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider +{ + /// + /// Gets a shared instance of this class. + /// + public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); + + /// + public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj new file mode 100644 index 00000000000..916145964f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -0,0 +1,16 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs new file mode 100644 index 00000000000..3b7169d8854 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -0,0 +1,358 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Resolves endpoints for a specified service. +/// +public sealed class ServiceEndPointResolver( + IServiceEndPointResolver[] resolvers, + ILogger logger, + string serviceName, + TimeProvider timeProvider, + IOptions options) : IAsyncDisposable +{ + private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: true); + + private readonly object _lock = new(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly ServiceEndPointResolverOptions _options = options.Value; + private readonly IServiceEndPointResolver[] _resolvers = resolvers; + private readonly CancellationTokenSource _disposalCancellation = new(); + private ITimer? _pollingTimer; + private ServiceEndPointCollection? _cachedEndPoints; + private Task _refreshTask = Task.CompletedTask; + private volatile CacheStatus _cacheState; + + /// + /// Gets the service name. + /// + public string ServiceName { get; } = serviceName; + + /// + /// Gets or sets the action called when endpoints are updated. + /// + public Action? OnEndPointsUpdated { get; set; } + + /// + /// Starts the endpoint resolver. + /// + public void Start() + { + _ = RefreshAsync(force: false); + } + + /// + /// Returns a collection of resolved endpoints for the service. + /// + /// The cancellation token. + /// A collection of resolved endpoints for the service. + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + { + // If the cache is valid, return the cached value. + if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + { + return new ValueTask(cached); + } + + // Otherwise, ensure the cache is being refreshed + // Wait for the cache refresh to complete and return the cached value. + return GetEndPointsInternal(cancellationToken); + + async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + { + ServiceEndPointCollection? result; + do + { + await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); + result = _cachedEndPoints; + } while (result is null); + return result; + } + } + + // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation + private Task RefreshAsync(bool force) + { + lock (_lock) + { + // If the cache is invalid or needs invalidation, refresh the cache. + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + { + // Indicate that the cache is being updated and start a new refresh task. + _cacheState = CacheStatus.Refreshing; + + // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _refreshTask = RefreshAsyncInternal(); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + + return _refreshTask; + } + } + + private async Task RefreshAsyncInternal() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var cancellationToken = _disposalCancellation.Token; + Exception? error = null; + ServiceEndPointCollection? newEndPoints = null; + CacheStatus newCacheState; + ResolutionStatus status = ResolutionStatus.Success; + while (true) + { + try + { + var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); + status = ResolutionStatus.Success; + foreach (var resolver in _resolvers) + { + var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); + status = CombineStatus(status, resolverStatus); + } + + var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); + + var statusCode = status.StatusCode; + if (statusCode != ResolutionStatusCode.Success) + { + if (statusCode is ResolutionStatusCode.Pending) + { + // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. + await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); + continue; + } + else if (statusCode is ResolutionStatusCode.Cancelled) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception ?? new OperationCanceledException(); + break; + } + else if (statusCode is ResolutionStatusCode.Error) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception; + break; + } + } + + lock (_lock) + { + // Check if we need to poll for updates or if we can register for change notification callbacks. + if (endPoints.ChangeToken.ActiveChangeCallbacks) + { + // Initiate a background refresh, if necessary. + endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + else + { + SchedulePollingTimer(); + } + + // The cache is valid + newEndPoints = (ServiceEndPointCollection?)endPoints; + newCacheState = CacheStatus.Valid; + break; + } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); + status = CombineStatus(status, ResolutionStatus.FromException(exception)); + break; + } + } + + // If there was an error, the cache must be invalid. + Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + + lock (_lock) + { + if (newCacheState is CacheStatus.Valid) + { + Debug.Assert(newEndPoints is not null); + _cachedEndPoints = newEndPoints; + } + + _cacheState = newCacheState; + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + + if (error is not null) + { + _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + ExceptionDispatchInfo.Throw(error); + } + else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + { + _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + } + } + + private void SchedulePollingTimer() + { + lock (_lock) + { + if (_pollingTimer is null) + { + _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + } + else + { + _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + } + } + } + + private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) + { + if (existing.StatusCode > newStatus.StatusCode) + { + return existing; + } + + var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); + Exception? exception; + if (existing.Exception is not null && newStatus.Exception is not null) + { + List exceptions = new(); + AddExceptions(existing.Exception, exceptions); + AddExceptions(newStatus.Exception, exceptions); + exception = new AggregateException(exceptions); + } + else + { + exception = existing.Exception ?? newStatus.Exception; + } + + var message = code switch + { + ResolutionStatusCode.Error => exception!.Message ?? "Error", + _ => code.ToString(), + }; + + return new ResolutionStatus(code, exception, message); + + static void AddExceptions(Exception? exception, List exceptions) + { + if (exception is AggregateException ae) + { + exceptions.AddRange(ae.InnerExceptions); + } + else if (exception is not null) + { + exceptions.Add(exception); + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + + _disposalCancellation.Cancel(); + if (_refreshTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + foreach (var resolver in _resolvers) + { + await resolver.DisposeAsync().ConfigureAwait(false); + } + } + + private enum CacheStatus + { + Invalid, + Refreshing, + Valid + } + + private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) + { + if (changeToken.HasChanged) + { + return; + } + + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + IDisposable? changeTokenRegistration = null; + IDisposable? cancellationRegistration = null; + IDisposable? pollPeriodRegistration = null; + CancellationTokenSource? timerCancellation = null; + + try + { + // Either wait for a callback or poll externally. + if (changeToken.ActiveChangeCallbacks) + { + changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + else + { + timerCancellation = new(pollPeriod); + pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + + if (cancellationToken.CanBeCanceled) + { + cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + + await completion.Task.ConfigureAwait(false); + } + finally + { + changeTokenRegistration?.Dispose(); + cancellationRegistration?.Dispose(); + pollPeriodRegistration?.Dispose(); + timerCancellation?.Dispose(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs new file mode 100644 index 00000000000..6feb04f3350 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates instances. +/// +public class ServiceEndPointResolverFactory( + IEnumerable resolvers, + ILogger resolverLogger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers + .Where(r => r is not PassThroughServiceEndPointResolverProvider) + .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); + private readonly ILogger _resolverLogger = resolverLogger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a instance for the provided service name. + /// + public ServiceEndPointResolver CreateResolver(string serviceName) + { + ArgumentNullException.ThrowIfNull(serviceName); + + List? resolvers = null; + foreach (var factory in _resolverProviders) + { + if (factory.TryCreateResolver(serviceName, out var resolver)) + { + resolvers ??= new(); + resolvers.Add(resolver); + } + } + + if (resolvers is not { Count: > 0 }) + { + throw new InvalidOperationException("No resolver which supports the provided service name has been configured"); + } + + return new ServiceEndPointResolver( + resolvers: resolvers.ToArray(), + logger: _resolverLogger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..8a2b37a5048 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Options for . +/// +public sealed class ServiceEndPointResolverOptions +{ + /// + /// Gets or sets the period between polling resolvers which are in a pending state and do not support refresh notifications via . + /// + public TimeSpan PendingStatusRefreshPeriod { get; set; } = TimeSpan.FromSeconds(15); + + /// + /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// + public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs new file mode 100644 index 00000000000..8d039f2d74d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Resolves service names to collections of endpoints. +/// +/// The resolver factory. +/// The time provider. +public sealed class ServiceEndPointResolverRegistry(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) : IAsyncDisposable +{ + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolverRegistry)s!).CleanupResolvers(); + private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); + + private readonly object _lock = new(); + private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + private bool _disposed; + + /// + /// Resolves and returns service endpoints for the specified service. + /// + /// The service name. + /// The cancellation token. + /// The resolved service endpoints. + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(serviceName); + ObjectDisposedException.ThrowIf(_disposed, this); + + EnsureCleanupTimerStarted(); + + while (true) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var resolver = _resolvers.GetOrAdd( + serviceName, + static (name, self) => self.CreateResolver(name), + this); + + var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + if (valid) + { + if (result is null) + { + throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); + } + + return result; + } + } + } + + private void EnsureCleanupTimerStarted() + { + if (_cleanupTimer is not null) + { + return; + } + + lock (_lock) + { + if (_cleanupTimer is not null) + { + return; + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + _disposed = true; + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + + foreach (var resolver in _resolvers) + { + await resolver.Value.DisposeAsync().ConfigureAwait(false); + } + + _resolvers.Clear(); + if (_cleanupTask is not null) + { + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private void CleanupResolvers() + { + lock (_lock) + { + if (_cleanupTask is { IsCompleted: true }) + { + _cleanupTask = CleanupResolversAsyncCore(); + } + } + } + + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) + { + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } + } + if (cleanupTasks is not null) + { + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); + } + } + + private ResolverEntry CreateResolver(string serviceName) + { + var resolver = _resolverProvider.CreateResolver(serviceName); + resolver.Start(); + return new ResolverEntry(resolver); + } + + private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable + { + private readonly ServiceEndPointResolver _resolver = resolver; + private const ulong CountMask = unchecked((ulong)-1); + private const ulong RecentUseFlag = 1UL << 61; + private const ulong DisposingFlag = 1UL << 62; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() + { + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); + + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } + + public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + { + try + { + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endPoints); + } + else + { + return (false, default); + } + } + finally + { + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) + { + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); + } + + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } + } + + private async Task DisposeAsyncCore() + { + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..1b3a20fed19 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs @@ -0,0 +1,285 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +/// +/// Tests for and . +/// These also cover and by extension. +/// +public class DnsServiceEndPointResolverTests +{ + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } + + [Fact] + public async Task ResolveServiceEndPoint_Dns() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + } + }; + + return Task.FromResult(response); + } + }; + var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(3, initialResult.EndPoints.Count); + var eps = initialResult.EndPoints; + Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + } + } + + /// + /// Tests that when there are multiple resolvers registered, they are consulted in registration order and each provider only adds endpoints if the providers before it did not. + /// + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + } + }; + + return Task.FromResult(response); + } + }; + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "localhost:8080", + ["services:basket:1"] = "remotehost:9090", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var serviceCollection = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore(); + if (dnsFirst) + { + serviceCollection + .AddDnsSrvServiceEndPointResolver() + .AddConfigurationServiceEndPointResolver(); + } + else + { + serviceCollection + .AddConfigurationServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(); + } + var services = serviceCollection.BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.Null(initialResult.Status.Exception); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + + if (dnsFirst) + { + // We expect only the results from the DNS provider. + Assert.Equal(3, initialResult.EndPoints.Count); + var eps = initialResult.EndPoints; + Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + } + else + { + // We expect only the results from the Configuration provider. + Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + } + } + } + + public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; + public void SetValues(IEnumerable> values) + { + Data.Clear(); + foreach (var (key, value) in values) + { + Data[key] = value; + } + + OnReload(); + } + } + + /* + [Fact] + public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() + { + var oneEndPoint = new Dictionary + { + ["services:basket:http:0:host"] = "localhost", + ["services:basket:http:0:port"] = "8080", + }; + var bothEndPoints = new Dictionary(oneEndPoint) + { + ["services:basket:http:1:host"] = "remotehost", + ["services:basket:http:1:port"] = "9090", + }; + var configSource = new MyConfigurationProvider(); + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) + .AddServiceDiscovery() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var channel = Channel.CreateUnbounded(); + resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); + resolver.Start(); + var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.False(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); + Assert.Null(initialResult.EndPoints); + + // Update the config and check that it flows through the system. + configSource.SetValues(oneEndPoint); + + // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to + // cause an indefinite test hang. We expect the result to be published practically immediately, though. + _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + var firstEp = Assert.Single(oneEpResult); + Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); + + // Do it again to check that an updated (not cached) version is published. + configSource.SetValues(bothEndPoints); + var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + Assert.True(twoEpResult.ResolvedSuccessfully); + Assert.Equal(2, twoEpResult.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); + } + } + */ +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj new file mode 100644 index 00000000000..ba827640199 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..5b0ec89d7a1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for and . +/// These also cover and by extension. +/// +public class ConfigurationServiceEndPointResolverTests +{ + [Fact] + public async Task ResolveServiceEndPoint_Configuration_SingleResult() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket"] = "localhost:8080", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + ["services:basket:1"] = "http://remotehost:9090", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + ["services:basket:1"] = "http://remotehost:9090", + ["services:basket:2"] = "http://_grpc.localhost:2222", + ["services:basket:3"] = "grpc://remotehost:2222", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + } + } + + public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; + public void SetValues(IEnumerable> values) + { + Data.Clear(); + foreach (var (key, value) in values) + { + Data[key] = value; + } + + OnReload(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj new file mode 100644 index 00000000000..73bc9ec1eab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -0,0 +1,19 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..3ede6371deb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for . +/// These also cover and by extension. +/// +public class PassThroughServiceEndPointResolverTests +{ + [Fact] + public async Task ResolveServiceEndPoint_PassThrough() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Superseded() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + + // We expect the basket service to be resolved from Configuration, not the pass-through provider. + Assert.Single(initialResult.EndPoints); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Fallback() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + + // We expect the CATALOG service to be resolved from the pass-through provider. + Assert.Single(initialResult.EndPoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..7cbfbada0fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Threading.Channels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for and . +/// +public class ServiceEndPointResolverTests +{ + [Fact] + public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + } + + [Fact] + public void ResolveServiceEndPoint_NullServiceName_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateResolver(null!)); + } + + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + { + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + bool result; + (result, resolver) = createResolverDelegate(serviceName); + return result; + } + } + + private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver + { + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask DisposeAsync() => disposeAsync(); + } + + [Fact] + public async Task ResolveServiceEndPoint() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(resolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var initialEndPoints = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialEndPoints); + var sep = Assert.Single(initialEndPoints); + var ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + Assert.False(tcs.Task.IsCompleted); + + cts[0].Cancel(); + var resolverResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(resolverResult); + Assert.Equal(ResolutionStatus.Success, resolverResult.Status); + Assert.True(resolverResult.ResolvedSuccessfully); + Assert.Equal(2, resolverResult.EndPoints.Count); + var endpoints = resolverResult.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + endpoints.Sort((l, r) => l.Port - r.Port); + Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); + Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_ThrowOnReload() + { + var sem = new SemaphoreSlim(0); + var cts = new[] { new CancellationTokenSource() }; + var throwOnNextResolve = new[] { false }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: async (collection, ct) => + { + await sem.WaitAsync(ct).ConfigureAwait(false); + if (cts[0].IsCancellationRequested) + { + // Always be sure to have a fresh token. + cts[0] = new(); + } + + if (throwOnNextResolve[0]) + { + throwOnNextResolve[0] = false; + throw new InvalidOperationException("throwing"); + } + + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + return ResolutionStatus.Success; + }, + disposeAsync: () => default); + var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(resolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + sem.Release(1); + var initialEndPoints = await initialEndPointsTask; + Assert.NotNull(initialEndPoints); + Assert.Single(initialEndPoints); + + // Tell the resolver to throw on the next resolve call and then trigger a reload. + throwOnNextResolve[0] = true; + cts[0].Cancel(); + + var exception = await Assert.ThrowsAsync(async () => + { + var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + sem.Release(1); + await resolveTask.ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.Equal("throwing", exception.Message); + + var channel = Channel.CreateUnbounded(); + resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + + do + { + cts[0].Cancel(); + sem.Release(1); + var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + await resolveTask.ConfigureAwait(false); + var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + if (next.ResolvedSuccessfully) + { + break; + } + } while (true); + + var task = resolver.GetEndPointsAsync(CancellationToken.None); + sem.Release(1); + var endPoints = await task.ConfigureAwait(false); + Assert.NotSame(initialEndPoints, endPoints); + var sep = Assert.Single(endPoints); + var ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + } + } +} From 727e5ea2e04d900c9a1111b870d2d2d87a4cdd03 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 28 Sep 2023 15:35:57 -0700 Subject: [PATCH 002/472] False positive. **BYPASS_SECRET_SCANNING** From ec97849b6aea76ef3e2468825f5669fbc53aafe1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:24:00 -0700 Subject: [PATCH 003/472] Service Discovery: fail when no resolvers have been registered (#340) --- .../ServiceEndPointResolver.cs | 18 ++++++++++-- .../ServiceEndPointResolverFactory.cs | 2 +- .../ServiceEndPointResolverTests.cs | 29 ++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 3b7169d8854..a1ee606b688 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; @@ -49,6 +50,7 @@ public sealed class ServiceEndPointResolver( /// public void Start() { + ThrowIfNoResolvers(); _ = RefreshAsync(force: false); } @@ -59,6 +61,8 @@ public void Start() /// A collection of resolved endpoints for the service. public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { + ThrowIfNoResolvers(); + // If the cache is valid, return the cached value. if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) { @@ -138,7 +142,6 @@ private async Task RefreshAsyncInternal() } var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; if (statusCode != ResolutionStatusCode.Success) { @@ -181,7 +184,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = (ServiceEndPointCollection?)endPoints; + newEndPoints = endPoints; newCacheState = CacheStatus.Valid; break; } @@ -355,4 +358,15 @@ private static async Task WaitForPendingChangeToken(IChangeToken changeToken, Ti timerCancellation?.Dispose(); } } + + private void ThrowIfNoResolvers() + { + if (_resolvers.Length == 0) + { + ThrowNoResolversConfigured(); + } + } + + [DoesNotReturn] + private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 6feb04f3350..94696ceb413 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -43,7 +43,7 @@ public ServiceEndPointResolver CreateResolver(string serviceName) if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured"); + throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); } return new ServiceEndPointResolver( diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 7cbfbada0fa..aa91a41f887 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -6,6 +6,7 @@ using System.Threading.Channels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Xunit; @@ -24,7 +25,21 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .AddServiceDiscoveryCore() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + } + + [Fact] + public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = new ServiceEndPointResolver([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var exception = Assert.Throws(resolverFactory.Start); + Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); + Assert.Equal("No service endpoint resolvers are configured.", exception.Message); } [Fact] @@ -37,6 +52,18 @@ public void ResolveServiceEndPoint_NullServiceName_Throws() Assert.Throws(() => resolverFactory.CreateResolver(null!)); } + [Fact] + public async Task UseServiceDiscovery_NoResolvers_Throws() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")) + .UseServiceDiscovery(); + var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService().CreateClient("foo"); + var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); + Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + } + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider { public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) From 2bb9a6ee2308f0c240ce5e8222ab9784f3bbd359 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:14:42 -0700 Subject: [PATCH 004/472] Service Discovery: Refactor DNS & DNS SRV providers into subclasses (#343) * Service Discovery: Allow DNS and DNS SRV to be added independently --- .../DnsServiceEndPointResolver.cs | 280 ++---------------- ... => DnsServiceEndPointResolverBase.Log.cs} | 14 +- .../DnsServiceEndPointResolverBase.cs | 196 ++++++++++++ .../DnsServiceEndPointResolverOptions.cs | 13 - .../DnsServiceEndPointResolverProvider.cs | 138 +-------- .../DnsSrvServiceEndPointResolver.cs | 82 +++++ .../DnsSrvServiceEndPointResolverOptions.cs | 38 +++ .../DnsSrvServiceEndPointResolverProvider.cs | 150 ++++++++++ .../HostingExtensions.cs | 31 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 10 +- ... => DnsSrvServiceEndPointResolverTests.cs} | 12 +- 11 files changed, 545 insertions(+), 419 deletions(-) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.Log.cs => DnsServiceEndPointResolverBase.Log.cs} (73%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsServiceEndPointResolverTests.cs => DnsSrvServiceEndPointResolverTests.cs} (97%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 63d60d5318d..4b5783cd0d5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -1,282 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; -using DnsClient; -using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// A service end point resolver that uses DNS to resolve the service end points. -/// -internal sealed partial class DnsServiceEndPointResolver : IServiceEndPointResolver +internal sealed partial class DnsServiceEndPointResolver( + string serviceName, + string hostName, + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) { - private readonly object _lock = new(); - private readonly string _serviceName; - private readonly Stopwatch _lastRefreshTimer = new(); - private readonly IOptionsMonitor _options; - private readonly ILogger _logger; - private readonly CancellationTokenSource _disposeCancellation = new(); - private readonly IDnsQuery _dnsClient; - private readonly TimeProvider _timeProvider; - private Task _resolveTask = Task.CompletedTask; - private ResolutionStatus _lastStatus; - private IChangeToken? _lastChangeToken; - private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; - private readonly string _addressRecordName; - private readonly string _srvRecordName; - private readonly int _defaultPort; - private TimeSpan _nextRefreshPeriod; + protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; - /// - /// Initializes a new instance. - /// - /// The service name. - /// The name used to resolve the address of this service. - /// The name used to resolve this service's SRV record in DNS. - /// The default port to use for endpoints. - /// The options. - /// The logger. - /// The DNS client. - /// The time provider. - public DnsServiceEndPointResolver( - string serviceName, - string addressRecordName, - string srvRecordName, - int defaultPort, - IOptionsMonitor options, - ILogger logger, - IDnsQuery dnsClient, - TimeProvider timeProvider) - { - _serviceName = serviceName; - _options = options; - _logger = logger; - _lastEndPointCollection = null; - _addressRecordName = addressRecordName; - _srvRecordName = srvRecordName; - _defaultPort = defaultPort; - _dnsClient = dnsClient; - _nextRefreshPeriod = _options.CurrentValue.MinRetryPeriod; - _timeProvider = timeProvider; - var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(_options.CurrentValue.DefaultRefreshPeriod); - _lastChangeToken = new CancellationChangeToken(cancellation.Token); - } - - /// - public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) - { - // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. - if (endPoints.EndPoints.Count != 0) - { - return ResolutionStatus.None; - } - - if (ShouldRefresh()) - { - Task resolveTask; - lock (_lock) - { - if (_resolveTask.IsCompleted && ShouldRefresh()) - { - _resolveTask = ResolveAsyncInternal(); - } - - resolveTask = _resolveTask; - } - - await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - lock (_lock) - { - if (_lastEndPointCollection is { Count: > 0 } eps) - { - foreach (var ep in eps) - { - endPoints.EndPoints.Add(ep); - } - } - - if (_lastChangeToken is not null) - { - endPoints.AddChangeToken(_lastChangeToken); - } - - return _lastStatus; - } - } - - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || _lastRefreshTimer.Elapsed >= _nextRefreshPeriod; - - private async Task ResolveAsyncInternal() + protected override async Task ResolveAsyncCore() { var endPoints = new List(); - var options = _options.CurrentValue; - var ttl = options.DefaultRefreshPeriod; - try + var ttl = DefaultRefreshPeriod; + Log.AddressQuery(logger, ServiceName, hostName); + var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + foreach (var address in addresses) { - if (options.UseSrvQuery) - { - Log.SrvQuery(_logger, _serviceName, _srvRecordName); - var result = await _dnsClient.QueryAsync(_srvRecordName, QueryType.SRV).ConfigureAwait(false); - if (result.HasError) - { - SetException(CreateException(result.ErrorMessage), ttl); - return; - } - - var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals) - { - ttl = MinTtl(record, ttl); - lookupMapping[record.DomainName] = record; - } - - var srvRecords = result.Answers.OfType(); - foreach (var record in srvRecords) - { - if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) - { - continue; - } - - ttl = MinTtl(record, ttl); - if (targetRecord is AddressRecord addressRecord) - { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); - } - else if (targetRecord is CNameRecord canonicalNameRecord) - { - endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); - } - } - } - else - { - Log.AddressQuery(_logger, _serviceName, _addressRecordName); - var addresses = await System.Net.Dns.GetHostAddressesAsync(_addressRecordName, _disposeCancellation.Token).ConfigureAwait(false); - foreach (var address in addresses) - { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, _defaultPort))); - } - - if (endPoints.Count == 0) - { - SetException(CreateException(), ttl); - return; - } - } - - SetResult(endPoints, ttl); - } - catch (Exception exception) - { - SetException(exception, ttl); - throw; + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, 0))); } - static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + if (endPoints.Count == 0) { - var candidate = TimeSpan.FromSeconds(record.TimeToLive); - return candidate < existing ? candidate : existing; + SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).")); + return; } - InvalidOperationException CreateException(string? errorMessage = null) - { - var msg = errorMessage switch - { - { Length: > 0 } => $"No DNS records were found for service {_serviceName}: {errorMessage}.", - _ => $"No DNS records were found for service {_serviceName}." - }; - var exception = new InvalidOperationException(msg); - return exception; - } - } - - private void SetException(Exception exception, TimeSpan validityPeriod) => SetResult(endPoints: null, exception, validityPeriod); - private void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); - private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) - { - lock (_lock) - { - var options = _options.CurrentValue; - if (exception is not null) - { - if (_lastStatus.Exception is null) - { - _nextRefreshPeriod = options.MinRetryPeriod; - } - else - { - var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * options.RetryBackOffFactor)); - _nextRefreshPeriod = nextPeriod > options.MaxRetryPeriod ? options.MaxRetryPeriod : nextPeriod; - } - - if (_lastEndPointCollection is null) - { - // Since end points have never been resolved, use a pending status to indicate that they might appear - // soon and to retry for some period until they do. - _lastStatus = ResolutionStatus.FromPending(exception); - } - else - { - _lastStatus = ResolutionStatus.FromException(exception); - } - } - else - { - _lastRefreshTimer.Restart(); - _nextRefreshPeriod = options.DefaultRefreshPeriod; - _lastStatus = ResolutionStatus.Success; - } - - validityPeriod = validityPeriod > TimeSpan.Zero && validityPeriod < _nextRefreshPeriod ? validityPeriod : _nextRefreshPeriod; - _lastCollectionCancellation.Cancel(); - var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(validityPeriod); - _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; - } - - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, _serviceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, _serviceName); - } - } - - /// - public async ValueTask DisposeAsync() - { - _disposeCancellation.Cancel(); - - if (_resolveTask is { } task) - { - await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - } - } - - private CancellationTokenSource CreateCancellationTokenSource(TimeSpan validityPeriod) - { - if (validityPeriod <= TimeSpan.Zero) - { - // Do not invalidate on a timer, but invalidate on refresh. - return new CancellationTokenSource(); - } - else - { - return new CancellationTokenSource(validityPeriod, _timeProvider); - } + SetResult(endPoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 6ace4ac983e..81668041ae1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal partial class DnsServiceEndPointResolver +internal partial class DnsServiceEndPointResolverBase { - private static partial class Log + internal static partial class Log { [LoggerMessage(1, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using DNS SRV lookup for name '{RecordName}'.", EventName = "SrvQuery")] public static partial void SrvQuery(ILogger logger, string serviceName, string recordName); @@ -31,10 +31,16 @@ public static void DiscoveredEndPoints(ILogger logger, List end [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] + [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - [LoggerMessage(4, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] + [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + + [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); + + [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs new file mode 100644 index 00000000000..63f649531e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// A service end point resolver that uses DNS to resolve the service end points. +/// +internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointResolver +{ + private readonly object _lock = new(); + private readonly ILogger _logger; + private readonly CancellationTokenSource _disposeCancellation = new(); + private readonly TimeProvider _timeProvider; + private long _lastRefreshTimeStamp; + private Task _resolveTask = Task.CompletedTask; + private ResolutionStatus _lastStatus; + private CancellationChangeToken _lastChangeToken; + private CancellationTokenSource _lastCollectionCancellation; + private List? _lastEndPointCollection; + private TimeSpan _nextRefreshPeriod; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The logger. + /// The time provider. + public DnsServiceEndPointResolverBase( + string serviceName, + ILogger logger, + TimeProvider timeProvider) + { + ServiceName = serviceName; + _logger = logger; + _lastEndPointCollection = null; + _timeProvider = timeProvider; + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + } + + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); + + protected string ServiceName { get; } + + protected abstract double RetryBackOffFactor { get; } + protected abstract TimeSpan MinRetryPeriod { get; } + protected abstract TimeSpan MaxRetryPeriod { get; } + protected abstract TimeSpan DefaultRefreshPeriod { get; } + protected CancellationToken ShutdownToken => _disposeCancellation.Token; + + /// + public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. + if (endPoints.EndPoints.Count != 0) + { + return ResolutionStatus.None; + } + + if (ShouldRefresh()) + { + Task resolveTask; + lock (_lock) + { + if (_resolveTask.IsCompleted && ShouldRefresh()) + { + _resolveTask = ResolveAsyncInternal(); + } + + resolveTask = _resolveTask; + } + + await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + lock (_lock) + { + if (_lastEndPointCollection is { Count: > 0 } eps) + { + foreach (var ep in eps) + { + endPoints.EndPoints.Add(ep); + } + } + + endPoints.AddChangeToken(_lastChangeToken); + return _lastStatus; + } + } + + private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + + protected abstract Task ResolveAsyncCore(); + + private async Task ResolveAsyncInternal() + { + try + { + await ResolveAsyncCore().ConfigureAwait(false); + } + catch (Exception exception) + { + SetException(exception); + throw; + } + + } + + protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero); + protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); + private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + { + lock (_lock) + { + if (exception is not null) + { + _nextRefreshPeriod = GetRefreshPeriod(); + if (_lastEndPointCollection is null) + { + // Since end points have never been resolved, use a pending status to indicate that they might appear + // soon and to retry for some period until they do. + _lastStatus = ResolutionStatus.FromPending(exception); + } + else + { + _lastStatus = ResolutionStatus.FromException(exception); + } + } + else if (endPoints is not { Count: > 0 }) + { + _nextRefreshPeriod = GetRefreshPeriod(); + validityPeriod = TimeSpan.Zero; + _lastStatus = ResolutionStatus.Pending; + } + else + { + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + _nextRefreshPeriod = DefaultRefreshPeriod; + _lastStatus = ResolutionStatus.Success; + } + + if (validityPeriod <= TimeSpan.Zero) + { + validityPeriod = _nextRefreshPeriod; + } + else if (validityPeriod > _nextRefreshPeriod) + { + validityPeriod = _nextRefreshPeriod; + } + + _lastCollectionCancellation.Cancel(); + var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + _lastEndPointCollection = endPoints; + } + + if (exception is null) + { + Debug.Assert(endPoints is not null); + Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); + } + else + { + Log.ResolutionFailed(_logger, exception, ServiceName); + } + + TimeSpan GetRefreshPeriod() + { + if (_lastStatus.StatusCode is ResolutionStatusCode.Success) + { + return MinRetryPeriod; + } + + var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor)); + return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod; + } + } + + /// + public async ValueTask DisposeAsync() + { + _disposeCancellation.Cancel(); + + if (_resolveTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 45adebfea0b..fee7bf2a245 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -27,17 +27,4 @@ public class DnsServiceEndPointResolverOptions /// Gets or sets the retry period growth factor. /// public double RetryBackOffFactor { get; set; } = 2; - - /// - /// Gets or sets the default DNS namespace for services resolved via this provider. - /// - /// - /// If not specified, the provider will attempt to infer the namespace. - /// - public string? DnsNamespace { get; set; } - - /// - /// Gets or sets a value indicating whether to use DNS SRV queries to discover host addresses and ports. - /// - public bool UseSrvQuery { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 4a554d7c9e7..fc5f707e411 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -2,148 +2,38 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// /// Provides instances which resolve endpoints from DNS. /// -internal sealed partial class DnsServiceEndPointResolverProvider : IServiceEndPointResolverProvider +/// +/// Initializes a new instance. +/// +/// The options. +/// The logger. +/// The time provider. +internal sealed partial class DnsServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndPointResolverProvider { - private readonly IOptionsMonitor _options; - private readonly ILogger _logger; - private readonly IDnsQuery _dnsClient; - private readonly TimeProvider _timeProvider; - private readonly string? _defaultNamespace; - - /// - /// Initializes a new instance. - /// - /// The options. - /// The logger. - /// The DNS client. - /// The time provider. - public DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - IDnsQuery dnsClient, - TimeProvider timeProvider) - { - _options = options; - _logger = logger; - _dnsClient = dnsClient; - _timeProvider = timeProvider; - _defaultNamespace = options.CurrentValue.DnsNamespace ?? GetHostNamespace(); - } - - // RFC 2181 - // DNS hostnames can consist only of letters, digits, dots, and hyphens. - // They must begin with a letter. - // They must end with a letter or a digit. - // Individual segments (between dots) can be no longer than 63 characters. - [GeneratedRegex("^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$")] - private static partial Regex ValidDnsName(); - - // Adapted version of Tim Berners Lee's regex from the URI spec: https://stackoverflow.com/a/26766402 - // Adapted to parse the port into a group and discard groups which we do not care about. - [GeneratedRegex("^(?:([^:/?#]+)://)?([^/?#:]*)?(?::([\\d]+))?")] - private static partial Regex UriRegex(); - /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md - // SRV records are available for headless services with named ports. - // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.svc.{zone}" - // We can fetch the namespace from /var/run/secrets/kubernetes.io/serviceaccount/namespace - // The protocol is assumed to be "tcp". - // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". - var dnsServiceName = serviceName; - var dnsNamespace = _defaultNamespace; - var portName = "default"; - var defaultPortNumber = 0; - - // Allow the service name to be expressed as either a URI or a plain DNS name. - var uri = UriRegex().Match(serviceName); - if (uri.Success) - { - if (uri.Groups[1].ValueSpan is { Length: > 0 } uriPortNameSpan) - { - // Override the port name if it was specified in the service name - portName = uriPortNameSpan.ToString(); - } - - if (int.TryParse(uri.Groups[3].ValueSpan, out var uriDefaultPort)) - { - // Override the default port if it was specified in the service name - defaultPortNumber = uriDefaultPort; - } - - // Since the service name was URI-formatted, we should extract the hostname part for resolution. - dnsServiceName = uri.Groups[2].Value; - } - else if (!ValidDnsName().IsMatch(serviceName)) + if (!ServiceNameParts.TryParse(serviceName, out var parts)) { + DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; return false; } - // If the DNS name is not qualified, and we have a qualifier, apply it. - if (!dnsServiceName.Contains('.') && dnsNamespace is not null) - { - dnsServiceName = $"{dnsServiceName}.{dnsNamespace}"; - } - - var srvRecordName = $"_{portName}._tcp.{dnsServiceName}"; - resolver = new DnsServiceEndPointResolver(serviceName, dnsServiceName, srvRecordName, defaultPortNumber, _options, _logger, _dnsClient, _timeProvider); + resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider); return true; } - - private static string? GetHostNamespace() => ReadNamespaceFromKubernetesServiceAccount() ?? ReadQualifiedNamespaceFromResolvConf(); - - private static string? ReadNamespaceFromKubernetesServiceAccount() - { - if (OperatingSystem.IsLinux()) - { - // Read the namespace from the Kubernetes pod's service account. - var serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); - if (File.Exists(serviceAccountNamespacePath)) - { - return File.ReadAllText(serviceAccountNamespacePath).Trim(); - } - } - - return null; - } - - private static string? ReadQualifiedNamespaceFromResolvConf() - { - if (OperatingSystem.IsLinux()) - { - var resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); - if (File.Exists(resolveConfPath)) - { - var lines = File.ReadAllLines(resolveConfPath); - foreach (var line in lines) - { - if (line.StartsWith("search ")) - { - var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - - if (components.Length > 1) - { - return components[1]; - } - } - } - } - } - - return default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs new file mode 100644 index 00000000000..cb6043f94ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsSrvServiceEndPointResolver( + string serviceName, + string srvQuery, + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) +{ + protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + + protected override async Task ResolveAsyncCore() + { + var endPoints = new List(); + var ttl = DefaultRefreshPeriod; + Log.SrvQuery(logger, ServiceName, srvQuery); + var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); + if (result.HasError) + { + SetException(CreateException(srvQuery, result.ErrorMessage)); + return; + } + + var lookupMapping = new Dictionary(); + foreach (var record in result.Additionals) + { + ttl = MinTtl(record, ttl); + lookupMapping[record.DomainName] = record; + } + + var srvRecords = result.Answers.OfType(); + foreach (var record in srvRecords) + { + if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) + { + continue; + } + + ttl = MinTtl(record, ttl); + if (targetRecord is AddressRecord addressRecord) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + } + else if (targetRecord is CNameRecord canonicalNameRecord) + { + endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + } + } + + SetResult(endPoints, ttl); + + static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + { + var candidate = TimeSpan.FromSeconds(record.TimeToLive); + return candidate < existing ? candidate : existing; + } + + InvalidOperationException CreateException(string dnsName, string errorMessage) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", + _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + }; + return new InvalidOperationException(msg); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..83ccd9afdbe --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Options for configuring . +/// +public class DnsSrvServiceEndPointResolverOptions +{ + /// + /// Gets or sets the default refresh period for endpoints resolved from DNS. + /// + public TimeSpan DefaultRefreshPeriod { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the initial period between retries. + /// + public TimeSpan MinRetryPeriod { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the maximum period between retries. + /// + public TimeSpan MaxRetryPeriod { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the retry period growth factor. + /// + public double RetryBackOffFactor { get; set; } = 2; + + /// + /// Gets or sets the default DNS query suffix for services resolved via this provider. + /// + /// + /// If not specified, the provider will attempt to infer the namespace. + /// + public string? QuerySuffix { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..09d9ddbc556 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using DnsClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides instances which resolve endpoints from DNS using SRV queries. +/// +/// +/// Initializes a new instance. +/// +/// The options. +/// The logger. +/// The DNS client. +/// The time provider. +internal sealed partial class DnsSrvServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) : IServiceEndPointResolverProvider +{ + private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); + private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); + private static readonly string s_resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); + private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. + // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md + // SRV records are available for headless services with named ports. + // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.{suffix}" + // The suffix (after the service name) can be parsed from /etc/resolv.conf + // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". + // The protocol is assumed to be "tcp". + // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". + if (string.IsNullOrWhiteSpace(_querySuffix)) + { + DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); + resolver = default; + return false; + } + + if (!ServiceNameParts.TryParse(serviceName, out var parts)) + { + DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); + resolver = default; + return false; + } + + var portName = parts.EndPointName ?? "default"; + var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, options, logger, dnsClient, timeProvider); + return true; + } + + private static string? GetKubernetesHostDomain() + { + // Check that we are running in Kubernetes first. + if (!IsInKubernetesCluster()) + { + return null; + } + + if (!OperatingSystem.IsLinux()) + { + return null; + } + + var qualifiedNamespace = ReadQualifiedNamespaceFromResolvConf(); + if (!string.IsNullOrWhiteSpace(qualifiedNamespace)) + { + return qualifiedNamespace; + } + + var serviceAccountNamespace = ReadNamespaceFromKubernetesServiceAccount(); + if (!string.IsNullOrWhiteSpace(serviceAccountNamespace)) + { + // The zone is assumed to be "cluster.local" + return $"{serviceAccountNamespace}.svc.cluster.local"; + } + + return null; + } + + private static string? ReadNamespaceFromKubernetesServiceAccount() + { + // Read the namespace from the Kubernetes pod's service account. + if (File.Exists(s_serviceAccountNamespacePath)) + { + return File.ReadAllText(s_serviceAccountNamespacePath).Trim(); + } + + return null; + } + + private static string? ReadQualifiedNamespaceFromResolvConf() + { + if (!File.Exists(s_resolveConfPath)) + { + return default; + } + + // See https://manpages.debian.org/bookworm/manpages/resolv.conf.5.en.html#search for the format of /etc/resolv.conf's search option. + // In our case, we are interested in determining the domain name. + var lines = File.ReadAllLines(s_resolveConfPath); + foreach (var line in lines) + { + if (!line.StartsWith("search ")) + { + continue; + } + + var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (components.Length > 1) + { + return components[1]; + } + } + + return default; + } + + private static bool IsInKubernetesCluster() + { + var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); + var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT"); + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) + { + return false; + } + + var tokenPath = Path.Combine(s_serviceAccountPath, "token"); + if (!File.Exists(tokenPath)) + { + return false; + } + + var certPath = Path.Combine(s_serviceAccountPath, "ca.crt"); + return File.Exists(certPath); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs index 49351af386d..e385bde69a7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs @@ -4,7 +4,6 @@ using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Dns; @@ -16,30 +15,40 @@ namespace Microsoft.Extensions.Hosting; public static class HostingExtensions { /// - /// Adds DNS-based service discovery to the . + /// Adds DNS SRV service discovery to the . /// /// The service collection. - /// The DNS service discovery configuration options. + /// The DNS SRV service discovery configuration options. /// The provided . - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { - services.Configure(options => options.UseSrvQuery = true); - return services.AddDnsServiceEndPointResolver(configureOptions); + services.AddServiceDiscoveryCore(); + services.TryAddSingleton(); + services.AddSingleton(); + var options = services.AddOptions(); + options.Configure(o => configureOptions?.Invoke(o)); + return services; } /// - /// Adds DNS-based service discovery to the . + /// Adds DNS service discovery to the . /// /// The service collection. - /// The DNS service discovery configuration options. + /// The DNS SRV service discovery configuration options. /// The provided . - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); services.AddSingleton(); var options = services.AddOptions(); - configureOptions?.Invoke(options); + options.Configure(o => configureOptions?.Invoke(o)); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 7f208a3f84a..05b59e6dee6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,10 +1,14 @@ - + $(NetCurrent) true + + + + @@ -19,4 +23,8 @@ + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs similarity index 97% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 1b3a20fed19..558d7260f52 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . +/// Tests for and . /// These also cover and by extension. /// -public class DnsServiceEndPointResolverTests +public class DnsSrvServiceEndPointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -101,7 +101,7 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; @@ -170,15 +170,15 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .AddConfigurationServiceEndPointResolver(); } else { serviceCollection .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(); - } + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + }; var services = serviceCollection.BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; From ea510d7c34bf0fc727b38be749b0d0c7e9bb41dd Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:32:19 -0700 Subject: [PATCH 005/472] Add IHostNameFeature to propagate original host name to HttpClient (#460) --- .../Features/IHostNameFeature.cs | 16 ++++++++++++++ .../DnsServiceEndPointResolver.cs | 8 +++++-- .../DnsServiceEndPointResolverBase.cs | 4 ++-- .../DnsSrvServiceEndPointResolver.cs | 16 +++++++++++--- .../DnsSrvServiceEndPointResolverProvider.cs | 2 +- .../ConfigurationServiceEndPointResolver.cs | 16 +++++++++++--- .../Http/ResolvingHttpClientHandler.cs | 1 + .../Http/ResolvingHttpDelegatingHandler.cs | 1 + .../DnsSrvServiceEndPointResolverTests.cs | 7 +++++++ ...nfigurationServiceEndPointResolverTests.cs | 21 +++++++++++++++++++ 10 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs new file mode 100644 index 00000000000..fff3c3fa3f8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Exposes the host name of the end point. +/// +public interface IHostNameFeature +{ + /// + /// Gets the host name of the end point. + /// + public string HostName { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4b5783cd0d5..502ef7d04c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -13,13 +13,15 @@ internal sealed partial class DnsServiceEndPointResolver( string hostName, IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + string IHostNameFeature.HostName => hostName; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -28,7 +30,9 @@ protected override async Task ResolveAsyncCore() var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, 0))); + var endPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + endPoint.Features.Set(this); + endPoints.Add(endPoint); } if (endPoints.Count == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 63f649531e8..260cb99242a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; @@ -31,7 +31,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin /// The service name. /// The logger. /// The time provider. - public DnsServiceEndPointResolverBase( + protected DnsServiceEndPointResolverBase( string serviceName, ILogger logger, TimeProvider timeProvider) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index cb6043f94ac..5bf3065fe42 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -13,16 +13,19 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsSrvServiceEndPointResolver( string serviceName, string srvQuery, + string hostName, IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + string IHostNameFeature.HostName => hostName; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -53,11 +56,11 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } @@ -78,5 +81,12 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) }; return new InvalidOperationException(msg); } + + ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + return serviceEndPoint; + } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index 09d9ddbc556..e5a7e23710e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -58,7 +58,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi var portName = parts.EndPointName ?? "default"; var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, options, logger, dnsClient, timeProvider); + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4f511cbf37b..39792f2ca89 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; @@ -11,7 +12,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// A service endpoint resolver that uses configuration to resolve endpoints. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver, IHostNameFeature { private readonly string _serviceName; private readonly string? _endpointName; @@ -49,6 +50,8 @@ public ConfigurationServiceEndPointResolver( /// public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); + string IHostNameFeature.HostName => _serviceName; + private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) { // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. @@ -91,7 +94,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); } - endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } } @@ -105,7 +108,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); } - endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } @@ -117,6 +120,13 @@ static bool SchemesMatch(string? scheme, ServiceNameParts parts) => || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); } + private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + return serviceEndPoint; + } + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) { var configPath = new StringBuilder(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 1edf4cf4898..50a86722feb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -24,6 +24,7 @@ protected override async Task SendAsync(HttpRequestMessage { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index b8e0368c5e3..41331ef5215 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -44,6 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = GetUriWithEndPoint(originalUri, result); + request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 558d7260f52..847c9268bf5 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -210,6 +210,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); } + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index 5b0ec89d7a1..b19195d688e 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -43,6 +43,13 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() Assert.Equal(ResolutionStatus.Success, initialResult.Status); var ep = Assert.Single(initialResult.EndPoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } @@ -78,6 +85,13 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() Assert.Equal(2, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } @@ -115,6 +129,13 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.Equal(2, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } From 81b2daa265fd68e04bc66ea2c73a5b36c50bb7e2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:29:37 -0700 Subject: [PATCH 006/472] Fix race in ServiceEndPointResolver (#511) --- .../ServiceEndPointResolver.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index a1ee606b688..d9f847cd012 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -202,6 +202,20 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task + // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // that will have more overhead in the common case. + if (newCacheState is CacheStatus.Valid) + { + Interlocked.Exchange(ref _cachedEndPoints, null); + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + lock (_lock) { if (newCacheState is CacheStatus.Valid) @@ -213,11 +227,6 @@ private async Task RefreshAsyncInternal() _cacheState = newCacheState; } - if (OnEndPointsUpdated is { } callback) - { - callback(new(newEndPoints, status)); - } - if (error is not null) { _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); From aad81ea6db7a0e368442953de9defe1672888405 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 26 Oct 2023 20:09:18 -0700 Subject: [PATCH 007/472] Fix one place where service discovery isn't AOT compatible (#540) --- ...Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 1 + .../Configuration/ConfigurationServiceEndPointResolver.cs | 5 +++-- .../Microsoft.Extensions.ServiceDiscovery.csproj | 3 ++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index f89f1ba02ac..5073885c198 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -3,6 +3,7 @@ $(NetCurrent) true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 05b59e6dee6..519c340fe7b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -3,6 +3,7 @@ $(NetCurrent) true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index eaa6cebf61c..30fed3082d2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -5,6 +5,7 @@ enable enable true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 39792f2ca89..5bf8d750d37 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -76,9 +76,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Read the endpoint from the configuration. // First check if there is a collection of sections - if (section.GetChildren().Any()) + var children = section.GetChildren(); + if (children.Any()) { - var values = section.Get>(); + var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { // Use schemes if any of the URIs have a scheme set. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 916145964f2..6bad9c5c20b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,8 +1,9 @@ - + $(NetCurrent) true + true From d2b078182c0ad96eb3a7c93c0467994f824bd176 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:15:44 -0700 Subject: [PATCH 008/472] Service Discovery: make host name propagation opt-in (#548) * Service Discovery: make host name propagation opt-in * Review feedback --- .../DnsServiceEndPointResolver.cs | 10 ++++-- .../DnsServiceEndPointResolverOptions.cs | 7 ++++ .../DnsSrvServiceEndPointResolver.cs | 6 +++- .../DnsSrvServiceEndPointResolverOptions.cs | 7 ++++ .../ConfigurationServiceEndPointResolver.cs | 6 +++- ...igurationServiceEndPointResolverOptions.cs | 5 +++ .../HostingExtensions.cs | 10 +++--- .../DnsSrvServiceEndPointResolverTests.cs | 32 ++++++++++++++----- ...nfigurationServiceEndPointResolverTests.cs | 8 ++--- 9 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 502ef7d04c4..1d701fdd573 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -30,9 +30,13 @@ protected override async Task ResolveAsyncCore() var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var endPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - endPoint.Features.Set(this); - endPoints.Add(endPoint); + var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + + endPoints.Add(serviceEndPoint); } if (endPoints.Count == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index fee7bf2a245..37879b5f3e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// @@ -27,4 +29,9 @@ public class DnsServiceEndPointResolverOptions /// Gets or sets the retry period growth factor. /// public double RetryBackOffFactor { get; set; } = 2; + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 5bf3065fe42..01ae7de5315 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -85,7 +85,11 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + return serviceEndPoint; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 83ccd9afdbe..5bac96c6c0a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// @@ -35,4 +37,9 @@ public class DnsSrvServiceEndPointResolverOptions /// If not specified, the provider will attempt to infer the namespace. /// public string? QuerySuffix { get; set; } + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 5bf8d750d37..4060ad5ba9e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -124,7 +124,11 @@ static bool SchemesMatch(string? scheme, ServiceNameParts parts) => private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + return serviceEndPoint; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs index a20d12d771c..5e67a885ca9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -12,4 +12,9 @@ public class ConfigurationServiceEndPointResolverOptions /// The name of the configuration section which contains service endpoints. Defaults to "Services". /// public string? SectionName { get; set; } = "Services"; + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 53e4c1f5bda..f938e57e629 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -60,12 +59,15 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); services.AddSingleton(); - var options = services.AddOptions(); - configureOptions?.Invoke(options); + if (configureOptions is not null) + { + services.Configure(configureOptions); + } + return services; } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 847c9268bf5..7531eec0a48 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -120,6 +120,12 @@ public async Task ResolveServiceEndPoint_Dns() Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); } } @@ -170,7 +176,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndPointResolver(options => + { + options.QuerySuffix = ".ns"; + options.ApplyHostNameMetadata = _ => true; + }) .AddConfigurationServiceEndPointResolver(); } else @@ -202,6 +212,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } else { @@ -209,14 +226,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo Assert.Equal(2, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); - } - Assert.All(initialResult.EndPoints, ep => - { - var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); - }); + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); + } } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index b19195d688e..e695362dc5d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -47,8 +47,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() Assert.All(initialResult.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); + Assert.Null(hostNameFeature); }); } } @@ -68,7 +67,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; @@ -133,8 +132,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.All(initialResult.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); + Assert.Null(hostNameFeature); }); } } From a7b6f7bae06a9c78e299feebbe8835d4b3f0f746 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 2 Nov 2023 11:32:44 -0500 Subject: [PATCH 009/472] Last round of triple slash, I believe. (#661) * Last round of triple slash, I believe. * Revert Aspire.sln change * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs --------- Co-authored-by: David Fowler --- .../Features/IEndPointHealthFeature.cs | 10 ++++++++-- .../Features/IEndPointLoadFeature.cs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs index 50276e05ecb..63dc3e11a3a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -3,10 +3,16 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing. +/// public interface IEndPointHealthFeature { - // Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. - // Can be a no-op. + /// + /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op. + /// + /// The response time of the endpoint. + /// An optional exception that occurred while checking the endpoint's health. void ReportHealth(TimeSpan responseTime, Exception? exception); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs index d58f23c7775..2610f135945 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -3,9 +3,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that provides information about the current load of an endpoint. +/// public interface IEndPointLoadFeature { - // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + /// + /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). + /// public double CurrentLoad { get; } } From 92a239c128bbf1ddd4711f15d8e27f004b21aa08 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 2 Nov 2023 10:31:31 -0700 Subject: [PATCH 010/472] Last round of triple slash, I believe. (#661) (#663) * Last round of triple slash, I believe. * Revert Aspire.sln change * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs --------- Co-authored-by: David Pine --- .../Features/IEndPointHealthFeature.cs | 10 ++++++++-- .../Features/IEndPointLoadFeature.cs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs index 50276e05ecb..63dc3e11a3a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -3,10 +3,16 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing. +/// public interface IEndPointHealthFeature { - // Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. - // Can be a no-op. + /// + /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op. + /// + /// The response time of the endpoint. + /// An optional exception that occurred while checking the endpoint's health. void ReportHealth(TimeSpan responseTime, Exception? exception); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs index d58f23c7775..2610f135945 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -3,9 +3,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that provides information about the current load of an endpoint. +/// public interface IEndPointLoadFeature { - // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + /// + /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). + /// public double CurrentLoad { get; } } From 7fbb032c84508c87490aa1c9f07e5596eaad30f7 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:35:59 -0700 Subject: [PATCH 011/472] Add additional debug logs to Service Discovery (#672) * Add additional debug logs to Service Discovery * Log message tweak * Use display name instead of GetType().Name for resolver names. Remove nameof --- .../IServiceEndPointResolver.cs | 5 ++ .../Internal/ServiceEndPointImpl.cs | 2 +- .../DnsServiceEndPointResolver.cs | 4 + .../DnsServiceEndPointResolverBase.Log.cs | 27 +----- .../DnsServiceEndPointResolverBase.cs | 14 +--- .../DnsSrvServiceEndPointResolver.cs | 3 + ...onfigurationServiceEndPointResolver.Log.cs | 71 ++++++++++++++++ .../ConfigurationServiceEndPointResolver.cs | 84 ++++++++++++++----- ...gurationServiceEndPointResolverProvider.cs | 8 +- .../HostingExtensions.cs | 2 +- .../Internal/ServiceNameParts.cs | 17 +++- .../PassThroughServiceEndPointResolver.Log.cs | 15 ++++ .../PassThroughServiceEndPointResolver.cs | 32 +++++++ ...ThroughServiceEndPointResolverProvider.cs} | 26 ++---- .../ServiceEndPointResolver.Log.cs | 43 ++++++++++ .../ServiceEndPointResolver.cs | 10 ++- .../ServiceEndPointResolverFactory.Log.cs | 23 +++++ .../ServiceEndPointResolverFactory.cs | 15 ++-- ...PassThroughServiceEndPointResolverTests.cs | 2 +- .../ServiceEndPointResolverTests.cs | 6 +- 20 files changed, 314 insertions(+), 95 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Internal/PassThroughServiceEndPointResolver.cs => PassThrough/PassThroughServiceEndPointResolverProvider.cs} (51%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index c228847c568..3cbb9b6c491 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,6 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { + /// + /// Gets the diagnostic display name for this resolver. + /// + string DisplayName { get; } + /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index 0b54f5a19d0..b73635ecd57 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -20,5 +20,5 @@ public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = nul public override EndPoint EndPoint => _endPoint; public override IFeatureCollection Features => _features; - public override string? ToString() => _endPoint.ToString(); + public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 1d701fdd573..b0d30530c72 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -22,6 +22,9 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; + /// + public override string DisplayName => "DNS"; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -31,6 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 81668041ae1..c3384d20e19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -16,31 +15,13 @@ internal static partial class Log [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); - public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) - { - if (logger.IsEnabled(LogLevel.Trace)) - { - DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); - } - else if (logger.IsEnabled(LogLevel.Debug)) - { - DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); - } - } + [LoggerMessage(3, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] - public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - - [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] - public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - - [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] - public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); - - [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + [LoggerMessage(4, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); - [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + [LoggerMessage(5, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 260cb99242a..238ace927fa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,6 +44,8 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } + public abstract string DisplayName { get; } + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } @@ -61,6 +62,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. if (endPoints.EndPoints.Count != 0) { + Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return ResolutionStatus.None; } @@ -161,16 +163,6 @@ private void SetResult(List? endPoints, Exception? exception, T _lastEndPointCollection = endPoints; } - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, ServiceName); - } - TimeSpan GetRefreshPeriod() { if (_lastStatus.StatusCode is ResolutionStatusCode.Success) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 01ae7de5315..ce8ae159764 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -24,6 +24,8 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + public override string DisplayName => "DNS SRV"; + string IHostNameFeature.HostName => hostName; protected override async Task ResolveAsyncCore() @@ -85,6 +87,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..b7e43172740 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +internal sealed partial class ConfigurationServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); + + [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] + public static partial void MatchingEndPointNames(ILogger logger, string serviceName); + + [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")] + public static partial void IgnoringEndPointNames(ILogger logger, string serviceName); + + public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + if (matchEndPointNames) + { + MatchingEndPointNames(logger, serviceName); + } + else + { + IgnoringEndPointNames(logger, serviceName); + } + } + + [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + + [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] + internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + + [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] + internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); + public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + StringBuilder endpointValues = new(); + for (var i = 0; i < parsedValues.Count; i++) + { + if (endpointValues.Length > 0) + { + endpointValues.Append(", "); + } + + endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + } + + var configuredEndPoints = endpointValues.ToString(); + ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4060ad5ba9e..fd382487551 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -17,6 +18,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd private readonly string _serviceName; private readonly string? _endpointName; private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly IOptions _options; /// @@ -24,10 +26,12 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// The service name. /// The configuration. + /// The logger. /// The options. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, + ILogger logger, IOptions options) { if (ServiceNameParts.TryParse(serviceName, out var parts)) @@ -37,13 +41,17 @@ public ConfigurationServiceEndPointResolver( } else { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); } _configuration = configuration; + _logger = logger; _options = options; } + /// + public string DisplayName => "Configuration"; + /// public ValueTask DisposeAsync() => default; @@ -57,6 +65,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { + Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return ResolutionStatus.None; } @@ -69,9 +78,11 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Get the corresponding config section. var section = root.GetSection(_serviceName); + var configPath = GetConfigurationPath(baseSectionName); + Log.UsingConfigurationPath(_logger, configPath, _serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, baseSectionName); + return CreateNotFoundResponse(endPoints, configPath); } // Read the endpoint from the configuration. @@ -82,17 +93,21 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { - // Use schemes if any of the URIs have a scheme set. - var uris = ParseServiceNameParts(values); - var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - foreach (var uri in uris) + // Use endpoint names if any of the values have an endpoint name set. + var parsedValues = ParseServiceNameParts(values, configPath); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); + + var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + + foreach (var uri in parsedValues) { - // If either schemes are not in-use or the scheme matches, create an endpoint for this value - if (!useSchemes || SchemesMatch(_endpointName, uri)) + // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. + if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) { if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); @@ -100,30 +115,42 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) { - if (SchemesMatch(_endpointName, uri)) + if (EndPointNamesMatch(_endpointName, parsed)) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + } + + if (_logger.IsEnabled(LogLevel.Debug)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } + if (endPoints.EndPoints.Count == 0) + { + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + } + endPoints.AddChangeToken(section.GetReloadToken()); return ResolutionStatus.Success; - static bool SchemesMatch(string? scheme, ServiceNameParts parts) => - (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) - || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => + string.IsNullOrEmpty(parts.EndPointName) + || string.IsNullOrEmpty(endPointName) + || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); } private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); @@ -132,7 +159,14 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); + } + + private string GetConfigurationPath(string? baseSectionName) { var configPath = new StringBuilder(); if (baseSectionName is { Length: > 0 }) @@ -141,21 +175,29 @@ private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource } configPath.Append(_serviceName); - endPoints.AddChangeToken(_configuration.GetReloadToken()); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + return configPath.ToString(); } - private static List ParseServiceNameParts(List input) + private List ParseServiceNameParts(List input, string configPath) { var results = new List(input.Count); for (var i = 0; i < input.Count; ++i) { if (ServiceNameParts.TryParse(input[i], out var value)) { - results.Add(value); + if (!results.Contains(value)) + { + results.Add(value); + } + } + else + { + throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); } } return results; } + + public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 5c35b6161fd..affe3a655ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -12,17 +13,20 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// The configuration. /// The options. +/// The logger factory. public class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, - IOptions options) : IServiceEndPointResolverProvider + IOptions options, + ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider { private readonly IConfiguration _configuration = configuration; private readonly IOptions _options = options; + private readonly ILogger _logger = loggerFactory.CreateLogger(); /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index f938e57e629..94f806a4016 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 8b2a03acd31..0d310190a33 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal readonly struct ServiceNameParts +internal readonly struct ServiceNameParts : IEquatable { public ServiceNameParts(string host, string? endPointName, int port) : this() { @@ -21,6 +21,8 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public int Port { get; init; } + public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; + public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) { if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) @@ -93,5 +95,18 @@ public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out serviceEndPoint = null; return false; } + + public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); + + public bool Equals(ServiceNameParts other) => + EndPointName == other.EndPointName && + Host == other.Host && + Port == other.Port; + + public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right); + + public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..570eb5e4e47 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +internal sealed partial class PassThroughServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + internal static partial void UsingPassThrough(ILogger logger, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..4ee6b0dd32d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +/// +/// Service endpoint resolver which passes through the provided value. +/// +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +{ + public string DisplayName => "Pass-through"; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + if (endPoints.EndPoints.Count != 0) + { + return new(ResolutionStatus.None); + } + + Log.UsingPassThrough(logger, serviceName); + var ep = ServiceEndPoint.Create(endPoint); + ep.Features.Set(this); + endPoints.EndPoints.Add(ep); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs similarity index 51% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 15e566c5887..24028f24ee5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -3,14 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Internal; +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) @@ -21,25 +23,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(endPoint); + resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } - - private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver - { - private readonly EndPoint _endPoint = endPoint; - - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) - { - if (endPoints.EndPoints.Count != 0) - { - return new(ResolutionStatus.None); - } - - endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); - return new(ResolutionStatus.Success); - } - - public ValueTask DisposeAsync() => default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..274d471030d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public sealed partial class ServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] + public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + + [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] + public static partial void ResolutionPending(ILogger logger, string serviceName); + + [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] + public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); + + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + } + + static string GetEndPointString(ServiceEndPoint ep) + { + if (ep.Features.Get() is { } resolver) + { + return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + } + + return ep.GetEndPointString(); + } + } + + [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index d9f847cd012..b8356d56af5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves endpoints for a specified service. /// -public sealed class ServiceEndPointResolver( +public sealed partial class ServiceEndPointResolver( IServiceEndPointResolver[] resolvers, ILogger logger, string serviceName, @@ -135,6 +135,7 @@ private async Task RefreshAsyncInternal() { var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); status = ResolutionStatus.Success; + Log.ResolvingEndPoints(_logger, ServiceName); foreach (var resolver in _resolvers) { var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); @@ -148,6 +149,7 @@ private async Task RefreshAsyncInternal() if (statusCode is ResolutionStatusCode.Pending) { // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. + Log.ResolutionPending(_logger, ServiceName); await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); continue; } @@ -229,12 +231,12 @@ private async Task RefreshAsyncInternal() if (error is not null) { - _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + else if (newEndPoints is not null) { - _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs new file mode 100644 index 00000000000..23d4b03cd67 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public partial class ServiceEndPointResolverFactory +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.DisplayName))); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 94696ceb413..ce020178406 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -4,14 +4,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public class ServiceEndPointResolverFactory( +public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, ILogger resolverLogger, IOptions options, @@ -20,7 +20,7 @@ public class ServiceEndPointResolverFactory( private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _resolverLogger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; @@ -36,19 +36,20 @@ public ServiceEndPointResolver CreateResolver(string serviceName) { if (factory.TryCreateResolver(serviceName, out var resolver)) { - resolvers ??= new(); + resolvers ??= []; resolvers.Add(resolver); } } if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); + throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); } + Log.CreatingResolver(_logger, serviceName, resolvers); return new ServiceEndPointResolver( - resolvers: resolvers.ToArray(), - logger: _resolverLogger, + resolvers: [.. resolvers], + logger: _logger, serviceName: serviceName, timeProvider: _timeProvider, options: _options); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 3ede6371deb..9d3e6ac17e7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index aa91a41f887..c435f965fe6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -26,7 +26,7 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] @@ -61,7 +61,7 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider @@ -76,6 +76,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { + public string DisplayName => "Fake"; + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 2371e8b85940c086027af0b997ade21d0ae8f42f Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:03:18 -0700 Subject: [PATCH 012/472] Add additional debug logs to Service Discovery (#672) (#678) * Add additional debug logs to Service Discovery * Log message tweak * Use display name instead of GetType().Name for resolver names. Remove nameof --- .../IServiceEndPointResolver.cs | 5 ++ .../Internal/ServiceEndPointImpl.cs | 2 +- .../DnsServiceEndPointResolver.cs | 4 + .../DnsServiceEndPointResolverBase.Log.cs | 27 +----- .../DnsServiceEndPointResolverBase.cs | 14 +--- .../DnsSrvServiceEndPointResolver.cs | 3 + ...onfigurationServiceEndPointResolver.Log.cs | 71 ++++++++++++++++ .../ConfigurationServiceEndPointResolver.cs | 84 ++++++++++++++----- ...gurationServiceEndPointResolverProvider.cs | 8 +- .../HostingExtensions.cs | 2 +- .../Internal/ServiceNameParts.cs | 17 +++- .../PassThroughServiceEndPointResolver.Log.cs | 15 ++++ .../PassThroughServiceEndPointResolver.cs | 32 +++++++ ...ThroughServiceEndPointResolverProvider.cs} | 26 ++---- .../ServiceEndPointResolver.Log.cs | 43 ++++++++++ .../ServiceEndPointResolver.cs | 10 ++- .../ServiceEndPointResolverFactory.Log.cs | 23 +++++ .../ServiceEndPointResolverFactory.cs | 15 ++-- ...PassThroughServiceEndPointResolverTests.cs | 2 +- .../ServiceEndPointResolverTests.cs | 6 +- 20 files changed, 314 insertions(+), 95 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Internal/PassThroughServiceEndPointResolver.cs => PassThrough/PassThroughServiceEndPointResolverProvider.cs} (51%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index c228847c568..3cbb9b6c491 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,6 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { + /// + /// Gets the diagnostic display name for this resolver. + /// + string DisplayName { get; } + /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index 0b54f5a19d0..b73635ecd57 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -20,5 +20,5 @@ public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = nul public override EndPoint EndPoint => _endPoint; public override IFeatureCollection Features => _features; - public override string? ToString() => _endPoint.ToString(); + public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 1d701fdd573..b0d30530c72 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -22,6 +22,9 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; + /// + public override string DisplayName => "DNS"; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -31,6 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 81668041ae1..c3384d20e19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -16,31 +15,13 @@ internal static partial class Log [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); - public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) - { - if (logger.IsEnabled(LogLevel.Trace)) - { - DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); - } - else if (logger.IsEnabled(LogLevel.Debug)) - { - DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); - } - } + [LoggerMessage(3, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] - public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - - [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] - public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - - [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] - public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); - - [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + [LoggerMessage(4, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); - [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + [LoggerMessage(5, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 260cb99242a..238ace927fa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,6 +44,8 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } + public abstract string DisplayName { get; } + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } @@ -61,6 +62,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. if (endPoints.EndPoints.Count != 0) { + Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return ResolutionStatus.None; } @@ -161,16 +163,6 @@ private void SetResult(List? endPoints, Exception? exception, T _lastEndPointCollection = endPoints; } - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, ServiceName); - } - TimeSpan GetRefreshPeriod() { if (_lastStatus.StatusCode is ResolutionStatusCode.Success) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 01ae7de5315..ce8ae159764 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -24,6 +24,8 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + public override string DisplayName => "DNS SRV"; + string IHostNameFeature.HostName => hostName; protected override async Task ResolveAsyncCore() @@ -85,6 +87,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..b7e43172740 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +internal sealed partial class ConfigurationServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); + + [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] + public static partial void MatchingEndPointNames(ILogger logger, string serviceName); + + [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")] + public static partial void IgnoringEndPointNames(ILogger logger, string serviceName); + + public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + if (matchEndPointNames) + { + MatchingEndPointNames(logger, serviceName); + } + else + { + IgnoringEndPointNames(logger, serviceName); + } + } + + [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + + [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] + internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + + [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] + internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); + public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + StringBuilder endpointValues = new(); + for (var i = 0; i < parsedValues.Count; i++) + { + if (endpointValues.Length > 0) + { + endpointValues.Append(", "); + } + + endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + } + + var configuredEndPoints = endpointValues.ToString(); + ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4060ad5ba9e..fd382487551 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -17,6 +18,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd private readonly string _serviceName; private readonly string? _endpointName; private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly IOptions _options; /// @@ -24,10 +26,12 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// The service name. /// The configuration. + /// The logger. /// The options. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, + ILogger logger, IOptions options) { if (ServiceNameParts.TryParse(serviceName, out var parts)) @@ -37,13 +41,17 @@ public ConfigurationServiceEndPointResolver( } else { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); } _configuration = configuration; + _logger = logger; _options = options; } + /// + public string DisplayName => "Configuration"; + /// public ValueTask DisposeAsync() => default; @@ -57,6 +65,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { + Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return ResolutionStatus.None; } @@ -69,9 +78,11 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Get the corresponding config section. var section = root.GetSection(_serviceName); + var configPath = GetConfigurationPath(baseSectionName); + Log.UsingConfigurationPath(_logger, configPath, _serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, baseSectionName); + return CreateNotFoundResponse(endPoints, configPath); } // Read the endpoint from the configuration. @@ -82,17 +93,21 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { - // Use schemes if any of the URIs have a scheme set. - var uris = ParseServiceNameParts(values); - var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - foreach (var uri in uris) + // Use endpoint names if any of the values have an endpoint name set. + var parsedValues = ParseServiceNameParts(values, configPath); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); + + var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + + foreach (var uri in parsedValues) { - // If either schemes are not in-use or the scheme matches, create an endpoint for this value - if (!useSchemes || SchemesMatch(_endpointName, uri)) + // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. + if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) { if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); @@ -100,30 +115,42 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) { - if (SchemesMatch(_endpointName, uri)) + if (EndPointNamesMatch(_endpointName, parsed)) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + } + + if (_logger.IsEnabled(LogLevel.Debug)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } + if (endPoints.EndPoints.Count == 0) + { + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + } + endPoints.AddChangeToken(section.GetReloadToken()); return ResolutionStatus.Success; - static bool SchemesMatch(string? scheme, ServiceNameParts parts) => - (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) - || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => + string.IsNullOrEmpty(parts.EndPointName) + || string.IsNullOrEmpty(endPointName) + || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); } private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); @@ -132,7 +159,14 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); + } + + private string GetConfigurationPath(string? baseSectionName) { var configPath = new StringBuilder(); if (baseSectionName is { Length: > 0 }) @@ -141,21 +175,29 @@ private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource } configPath.Append(_serviceName); - endPoints.AddChangeToken(_configuration.GetReloadToken()); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + return configPath.ToString(); } - private static List ParseServiceNameParts(List input) + private List ParseServiceNameParts(List input, string configPath) { var results = new List(input.Count); for (var i = 0; i < input.Count; ++i) { if (ServiceNameParts.TryParse(input[i], out var value)) { - results.Add(value); + if (!results.Contains(value)) + { + results.Add(value); + } + } + else + { + throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); } } return results; } + + public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 5c35b6161fd..affe3a655ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -12,17 +13,20 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// The configuration. /// The options. +/// The logger factory. public class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, - IOptions options) : IServiceEndPointResolverProvider + IOptions options, + ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider { private readonly IConfiguration _configuration = configuration; private readonly IOptions _options = options; + private readonly ILogger _logger = loggerFactory.CreateLogger(); /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index f938e57e629..94f806a4016 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 8b2a03acd31..0d310190a33 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal readonly struct ServiceNameParts +internal readonly struct ServiceNameParts : IEquatable { public ServiceNameParts(string host, string? endPointName, int port) : this() { @@ -21,6 +21,8 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public int Port { get; init; } + public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; + public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) { if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) @@ -93,5 +95,18 @@ public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out serviceEndPoint = null; return false; } + + public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); + + public bool Equals(ServiceNameParts other) => + EndPointName == other.EndPointName && + Host == other.Host && + Port == other.Port; + + public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right); + + public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..570eb5e4e47 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +internal sealed partial class PassThroughServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + internal static partial void UsingPassThrough(ILogger logger, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..4ee6b0dd32d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +/// +/// Service endpoint resolver which passes through the provided value. +/// +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +{ + public string DisplayName => "Pass-through"; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + if (endPoints.EndPoints.Count != 0) + { + return new(ResolutionStatus.None); + } + + Log.UsingPassThrough(logger, serviceName); + var ep = ServiceEndPoint.Create(endPoint); + ep.Features.Set(this); + endPoints.EndPoints.Add(ep); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs similarity index 51% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 15e566c5887..24028f24ee5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -3,14 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Internal; +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) @@ -21,25 +23,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(endPoint); + resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } - - private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver - { - private readonly EndPoint _endPoint = endPoint; - - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) - { - if (endPoints.EndPoints.Count != 0) - { - return new(ResolutionStatus.None); - } - - endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); - return new(ResolutionStatus.Success); - } - - public ValueTask DisposeAsync() => default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..274d471030d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public sealed partial class ServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] + public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + + [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] + public static partial void ResolutionPending(ILogger logger, string serviceName); + + [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] + public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); + + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + } + + static string GetEndPointString(ServiceEndPoint ep) + { + if (ep.Features.Get() is { } resolver) + { + return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + } + + return ep.GetEndPointString(); + } + } + + [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index d9f847cd012..b8356d56af5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves endpoints for a specified service. /// -public sealed class ServiceEndPointResolver( +public sealed partial class ServiceEndPointResolver( IServiceEndPointResolver[] resolvers, ILogger logger, string serviceName, @@ -135,6 +135,7 @@ private async Task RefreshAsyncInternal() { var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); status = ResolutionStatus.Success; + Log.ResolvingEndPoints(_logger, ServiceName); foreach (var resolver in _resolvers) { var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); @@ -148,6 +149,7 @@ private async Task RefreshAsyncInternal() if (statusCode is ResolutionStatusCode.Pending) { // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. + Log.ResolutionPending(_logger, ServiceName); await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); continue; } @@ -229,12 +231,12 @@ private async Task RefreshAsyncInternal() if (error is not null) { - _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + else if (newEndPoints is not null) { - _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs new file mode 100644 index 00000000000..23d4b03cd67 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public partial class ServiceEndPointResolverFactory +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.DisplayName))); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 94696ceb413..ce020178406 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -4,14 +4,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public class ServiceEndPointResolverFactory( +public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, ILogger resolverLogger, IOptions options, @@ -20,7 +20,7 @@ public class ServiceEndPointResolverFactory( private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _resolverLogger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; @@ -36,19 +36,20 @@ public ServiceEndPointResolver CreateResolver(string serviceName) { if (factory.TryCreateResolver(serviceName, out var resolver)) { - resolvers ??= new(); + resolvers ??= []; resolvers.Add(resolver); } } if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); + throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); } + Log.CreatingResolver(_logger, serviceName, resolvers); return new ServiceEndPointResolver( - resolvers: resolvers.ToArray(), - logger: _resolverLogger, + resolvers: [.. resolvers], + logger: _logger, serviceName: serviceName, timeProvider: _timeProvider, options: _options); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 3ede6371deb..9d3e6ac17e7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index aa91a41f887..c435f965fe6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -26,7 +26,7 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] @@ -61,7 +61,7 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider @@ -76,6 +76,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { + public string DisplayName => "Fake"; + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 655a8a2a550dfea9c9926c2449a6566628ef34b3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 6 Nov 2023 15:35:15 +0800 Subject: [PATCH 013/472] Add package descriptions and icons. (#701) * Add package descriptions and icons. --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 5073885c198..295279cdefd 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 519c340fe7b..60ad214e883 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS SRV records. Useful for service resolution in orchestrators such as Kubernetes. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 30fed3082d2..6602d8ad88c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -6,6 +6,8 @@ enable true true + Provides extensions for service discovery for the YARP reverse proxy. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6bad9c5c20b..76862965b28 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient that enable service discovery based on configuration and dns. + $(SharedDir)dotnet-icon.png From db14b2701d044949a107a443ff1335b83d6cb34a Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 7 Nov 2023 09:03:10 +0800 Subject: [PATCH 014/472] [release/8.0-preview1] Add package descriptions and icons. (#701) (#702) * Add package descriptions and icons. (#701) * Add package descriptions and icons. * Update src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj Co-authored-by: Eric Erhardt * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> * Update src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> * Fix package icons. Use base arcade icon for ServiceDiscovery packages. Use the default Aspire icon for Aspire Hosting packages. --------- Co-authored-by: David Fowler Co-authored-by: Eric Erhardt Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 5073885c198..0ec16344e2a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 519c340fe7b..a3036bf028f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 30fed3082d2..729e5f9f405 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -6,6 +6,8 @@ enable true true + Provides extensions for service discovery for the YARP reverse proxy. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6bad9c5c20b..81a21ac841a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient that enable service discovery based on configuration. + true From fd54a80a12aec9fd30ca670d615bc62a9df8f41b Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 7 Nov 2023 23:40:40 +0800 Subject: [PATCH 015/472] Forward port package changes from release branch. (#722) * Forward port package changes from release branch. * Remove unused dotnet-icon. --------- Co-authored-by: Eric Erhardt --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ++-- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 295279cdefd..0ec16344e2a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -5,7 +5,7 @@ true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. - $(SharedDir)dotnet-icon.png + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 60ad214e883..a3036bf028f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,8 +4,8 @@ $(NetCurrent) true true - Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS SRV records. Useful for service resolution in orchestrators such as Kubernetes. - $(SharedDir)dotnet-icon.png + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 6602d8ad88c..729e5f9f405 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true true Provides extensions for service discovery for the YARP reverse proxy. - $(SharedDir)dotnet-icon.png + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 76862965b28..81a21ac841a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,8 +4,8 @@ $(NetCurrent) true true - Provides extensions to HttpClient that enable service discovery based on configuration and dns. - $(SharedDir)dotnet-icon.png + Provides extensions to HttpClient that enable service discovery based on configuration. + true From 381a5ed58b139e3683e2999f178690c646c54923 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 13 Nov 2023 18:11:15 -0600 Subject: [PATCH 016/472] Fix Trim warning in ServiceDiscovery (#768) When PublishTrimmed=true in an app that uses ServiceDiscovery, we get a warning that says: _ILLink : warning IL2105: Microsoft.Extensions.ServiceDiscovery.Abstractions.ServiceEndPointCollection: Type 'ServiceEndPointCollectionDebuggerView' was not found in the caller assembly nor in the base library. Type name strings used for dynamically accessing a type should be assembly qualified._ Fix this warning by using `typeof` instead of `nameof`. --- .../ServiceEndPointCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs index 57919f949c3..c9540f2e7a1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// Represents an immutable collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(nameof(ServiceEndPointCollectionDebuggerView))] +[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] public class ServiceEndPointCollection : IReadOnlyList { private readonly List? _endpoints; From 873440f97ec79433f8baa67bd9a37a5de76450c1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:02:58 -0800 Subject: [PATCH 017/472] READMEs for Service Discovery (#792) * READMEs for Service Discovery * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine --------- Co-authored-by: David Pine --- .../README.md | 7 + .../README.md | 65 +++++ .../README.md | 42 +++ .../README.md | 276 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md new file mode 100644 index 00000000000..c5cf6b9bc78 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -0,0 +1,7 @@ +# Microsoft.Extensions.ServiceDiscovery.Abstractions + +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md new file mode 100644 index 00000000000..d3fbe2a75e5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -0,0 +1,65 @@ +# Microsoft.Extensions.ServiceDiscovery.Dns + +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: + +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. + +- _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). + +## Resolving service endpoints with DNS + +The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. + +To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsServiceEndPointResolver(); +``` + +## Resolving service endpoints in Kubernetes with DNS SRV + +When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". + +```yml +apiVersion: v1 +kind: Service +metadata: + name: basket +spec: + selector: + name: basket-service + clusterIP: None + ports: + - name: default + port: 8080 + - name: dashboard + port: 8888 +``` + +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsSrvServiceEndPointResolver(); +``` + +The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. + +As in the previous example, add service discovery to an `HttpClient` for the basket service: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +``` + +Similarly, the "dashboard" endpoint can be targeted as follows: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md new file mode 100644 index 00000000000..a7175f0382c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md @@ -0,0 +1,42 @@ +# Microsoft.Extensions.ServiceDiscovery.Yarp + +The `Microsoft.Extensions.ServiceDiscovery.Yarp` library adds support for resolving endpoints for YARP clusters, by implementing a [YARP destination resolver](https://github.com/microsoft/reverse-proxy/blob/main/docs/docfx/articles/destination-resolvers.md). + +## Usage + +### Resolving YARP cluster destinations using Service Discovery + +The `IReverseProxyBuilder.AddServiceDiscoveryDestinationResolver()` extension method configures a [YARP destination resolver](https://github.com/microsoft/reverse-proxy/blob/main/docs/docfx/articles/destination-resolvers.md). To use this method, you must also configure YARP itself as described in the YARP documentation, and you must configure .NET Service Discovery via the _Microsoft.Extensions.ServiceDiscovery_ library. + +### Direct HTTP forwarding using Service Discovery Forwarding HTTP requests using `IHttpForwarder` + +YARP supports _direct forwarding_ of specific requests using the `IHttpForwarder` interface. This, too, can benefit from service discovery using the _Microsoft.Extensions.ServiceDiscovery_ library. To take advantage of service discovery when using YARP Direct Forwarding, use the `IServiceCollection.AddHttpForwarderWithServiceDiscovery` method. + +For example, consider the following .NET Aspire application: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Configure service discovery +builder.Services.AddServiceDiscovery(); + +// Add YARP Direct Forwarding with Service Discovery support +builder.Services.AddHttpForwarderWithServiceDiscovery(); + +// ... other configuration ... + +var app = builder.Build(); + +// ... other configuration ... + +// Map a Direct Forwarder which forwards requests to the resolved "catalogservice" endpoints +app.MapForwarder("/catalog/images/{id}", "http://catalogservice", "/api/v1/catalog/items/{id}/image"); + +app.Run(); +``` + +In the above example, the YARP Direct Forwarder will resolve the _catalogservice_ using service discovery, forwarding request sent to the `/catalog/images/{id}` endpoint to the destination path on the resolved endpoints. + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md new file mode 100644 index 00000000000..7ae0e4872c8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -0,0 +1,276 @@ +# Microsoft.Extensions.ServiceDiscovery + +The `Microsoft.Extensions.ServiceDiscovery` library is designed to simplify the integration of service discovery patterns in .NET applications. Service discovery is a key component of most distributed systems and microservices architectures. This library provides a straightforward way to resolve service names to endpoint addresses. + +In typical systems, service configuration changes over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. + +## How it works + +Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). + +Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. + +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. + +Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. + +### Change notifications + +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). + +### Extensibility using features + +Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: + +* `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). +* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. +* `IEndPointLoadFeature`: used to query estimated endpoint load. + +### Resolution order + +The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. + +## Getting Started + +### Installation + +To install the library, use the following NuGet command: + +```dotnetcli +dotnet add package Microsoft.Extensions.ServiceDiscovery +``` + +### Usage example + +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. + +```csharp +builder.Services.AddServiceDiscovery(); +``` + +Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: + +```csharp +builder.Services.AddHttpClient(c => +{ + c.BaseAddress = new("http://catalog")); +}).UseServiceDiscovery(); +``` + +Alternatively, you can add service discovery to all `HttpClient` instances by default: + +```csharp +builder.Services.ConfigureHttpClientDefaults(http => +{ + // Turn on service discovery by default + http.UseServiceDiscovery(); +}); +``` + +### Resolving service endpoints from configuration + +The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. +This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. + +Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: + +```json +{ + "Services": { + "catalog": [ + "localhost:8080", + "10.46.24.90:80", + ] + } +} +``` + +The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. +Each time the _catalog_ is resolved, one of these endpoints will be selected. + +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. + +### Configuration + +The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: + +* **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. + +* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. + +To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure(options => +{ + options.SectionName = "MyServiceEndpoints"; + + // Configure the logic for applying host name metadata + options.ApplyHostNameMetadata = endpoint => + { + // Your custom logic here. For example: + return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + }; +}); +``` + +This example demonstrates setting a custom section name for your service endpoints and providing a custom logic for applying host name metadata based on a condition. + +## Resolving service endpoints using platform-provided service discovery + +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. + +The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. + +The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. + +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. + +In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". + +## Load-balancing with endpoint selectors + +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://catalog")); + .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); +``` + +The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: + +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` + +Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. + +## Service discovery in .NET Aspire + +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. + +Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var catalog = builder.AddProject("catalog"); +var basket = builder.AddProject("basket"); + +var frontend = builder.AddProject("frontend") + .WithReference(basket) + .WithReference(catalog); +``` + +In the above example, the _frontend_ project references the _catalog_ project and the _basket_ project. The two `WithReference` calls instruct the .NET Aspire application to pass service discovery information for the referenced projects (_catalog_, and _basket_) into the _frontend_ project. + +## Named endpoints + +Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `http://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `http://_dashboard.basket` can be used to specify this endpoint, for example: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +In the above example, two `HttpClient`s are added: one for the core basket service and one for the basket service's dashboard. + +### Named endpoints using configuration + +With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": + +```json +{ + "Services": { + "basket": [ + "10.2.3.4:8080", /* the default endpoint, when resolving http://basket */ + "_dashboard.10.2.3.4:9999" /* the "dashboard" endpoint, resolved via http://_dashboard.basket */ + ] + } +} +``` + +### Named endpoints in .NET Aspire + +.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var basket = builder.AddProject("basket") + .WithServiceBinding(hostPort: 9999, scheme: "http", name: "admin"); + +var adminDashboard = builder.AddProject("admin-dashboard") + .WithReference(basket.GetEndPoint("admin")); + +var frontend = builder.AddProject("frontend") + .WithReference(basket); +``` + +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: + +```csharp + +// The preceding code is the same as in the above sample + +var frontend = builder.AddProject("frontend") + .WithReference(basket.GetEndpoint("http")); +``` + +### Named endpoints in Kubernetes using DNS SRV + +When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". + +```yml +apiVersion: v1 +kind: Service +metadata: + name: basket +spec: + selector: + name: basket-service + clusterIP: None + ports: + - name: default + port: 8080 + - name: dashboard + port: 8888 +``` + +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsSrvServiceEndPointResolver(); +``` + +The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. + +As in the previous example, add service discovery to an `HttpClient` for the basket service: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +``` + +Similarly, the "dashboard" endpoint can be targeted as follows: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +### Named endpoints in Azure Container Apps + +Named endpoints are not currently supported for services deployed to Azure Container Apps. + +## Feedback & contributing + +https://github.com/dotnet/aspire From aaf3fbf0bc78cb179b0aeb356efcd1b9da56c46a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 14 Nov 2023 15:43:08 -0600 Subject: [PATCH 018/472] Fix NuGet Package Icons (#817) --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 0ec16344e2a..edafcce1914 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -5,7 +5,7 @@ true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index a3036bf028f..7fed469c4d8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -5,7 +5,7 @@ true true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 729e5f9f405..c5145e06528 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true true Provides extensions for service discovery for the YARP reverse proxy. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 81a21ac841a..816a68bc78b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -5,7 +5,7 @@ true true Provides extensions to HttpClient that enable service discovery based on configuration. - true + $(DefaultDotnetIconFullPath) From 1525b005033e5722385969a274c4d40c31ae0198 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:29:39 -0800 Subject: [PATCH 019/472] Service Discovery: fix shutdown blocking indefinitely in some cases (#880) * Service Discovery: fix shutdown blocking indefinitely in some cases * Update src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs Co-authored-by: Eric Erhardt * Review feedback --------- Co-authored-by: Eric Erhardt --- .../Http/HttpServiceEndPointResolver.cs | 8 +- .../ServiceEndPointResolverRegistry.cs | 8 +- .../ServiceEndPointResolverTests.cs | 75 +++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index e9e80bc76bb..da7fee6dd47 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -122,7 +122,7 @@ private void CleanupResolvers() { lock (_lock) { - if (_cleanupTask is { IsCompleted: true }) + if (_cleanupTask is null or { IsCompleted: true }) { _cleanupTask = CleanupResolversAsyncCore(); } @@ -159,9 +159,9 @@ private sealed class ResolverEntry : IAsyncDisposable { private readonly ServiceEndPointResolver _resolver; private readonly IServiceEndPointSelector _selector; - private const ulong CountMask = unchecked((ulong)-1); - private const ulong RecentUseFlag = 1UL << 61; - private const ulong DisposingFlag = 1UL << 62; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs index 8d039f2d74d..fb71bb9b85c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs @@ -121,7 +121,7 @@ private void CleanupResolvers() { lock (_lock) { - if (_cleanupTask is { IsCompleted: true }) + if (_cleanupTask is null or { IsCompleted: true }) { _cleanupTask = CleanupResolversAsyncCore(); } @@ -155,9 +155,9 @@ private ResolverEntry CreateResolver(string serviceName) private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable { private readonly ServiceEndPointResolver _resolver = resolver; - private const ulong CountMask = unchecked((ulong)-1); - private const ulong RecentUseFlag = 1UL << 61; - private const ulong DisposingFlag = 1UL << 62; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index c435f965fe6..ff964eb28f6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Http; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; @@ -135,6 +136,80 @@ public async Task ResolveServiceEndPoint() } } + [Fact] + public async Task ResolveServiceEndPointOneShot() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(resolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolver = services.GetRequiredService(); + + Assert.NotNull(resolver); + var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialEndPoints); + var sep = Assert.Single(initialEndPoints); + var ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + await services.DisposeAsync().ConfigureAwait(false); + } + + [Fact] + public async Task ResolveHttpServiceEndPointOneShot() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(fakeResolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var selectorProvider = services.GetRequiredService(); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System); + + Assert.NotNull(resolver); + var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); + var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endPoint); + var ip = Assert.IsType(endPoint.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + await services.DisposeAsync().ConfigureAwait(false); + } + [Fact] public async Task ResolveServiceEndPoint_ThrowOnReload() { From b51c429ffbe64e673c9ed18343e84382221dbbef Mon Sep 17 00:00:00 2001 From: Arvin Kahbazi Date: Thu, 30 Nov 2023 22:38:48 +0330 Subject: [PATCH 020/472] Use ValueStopWatch (#1148) --- .../Http/ResolvingHttpClientHandler.cs | 6 +++--- .../Http/ResolvingHttpDelegatingHandler.cs | 6 +++--- .../Microsoft.Extensions.ServiceDiscovery.csproj | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 50a86722feb..52ff1d4494d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -19,7 +19,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + var responseDuration = ValueStopwatch.StartNew(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -39,7 +39,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 41331ef5215..8c0d67f41bd 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -39,7 +39,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + var responseDuration = ValueStopwatch.StartNew(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -59,7 +59,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 816a68bc78b..1fa156be1d4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,6 +10,7 @@ + From 7050e5a7593c3d643d28dc0ab294fa585c070ae2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:50:22 -0800 Subject: [PATCH 021/472] Make behavior of IHttpClientBuilder.UseServiceDiscovery overloads consistent (#1160) --- .../Http/HttpClientBuilderExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 7e9df30b770..156a08b7a6c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -34,6 +34,10 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt return new ResolvingHttpDelegatingHandler(registry); }); + // Configure the HttpClient to disable gRPC load balancing. + // This is done on all HttpClient instances but only impacts gRPC clients. + AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); + return httpClientBuilder; } From 50f8ab2f4dc89c88274dbd3710072966a4c02f46 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 22 Dec 2023 09:04:49 -0800 Subject: [PATCH 022/472] Rename WithServiceBinding to WithEndpoint (#1484) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 7ae0e4872c8..8bece9644ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -205,7 +205,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified var builder = DistributedApplication.CreateBuilder(args); var basket = builder.AddProject("basket") - .WithServiceBinding(hostPort: 9999, scheme: "http", name: "admin"); + .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") .WithReference(basket.GetEndPoint("admin")); From e9ee54076113f15f64076aa0bc8f8f55658ecddd Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 10 Jan 2024 20:01:24 +0200 Subject: [PATCH 023/472] Add comment to explain DisableGrpcLoadBalancingFilter (#1163) --- .../Http/HttpClientBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 156a08b7a6c..239db961e26 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -105,6 +105,7 @@ public Action Configure(Action Date: Tue, 16 Jan 2024 01:35:21 +0300 Subject: [PATCH 024/472] RoundRobinServiceEndPointSelectorProvider (#1661) Co-authored-by: Alexander Kucherov --- .../LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs index ed1e79d7416..40d9ce7845c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs @@ -11,7 +11,7 @@ public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelecto /// /// Gets a shared instance of this class. /// - public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); + public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new(); /// public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); From 6ad2ba6c173030fb3e6973f034a7a8f4193743e2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:02:14 -0800 Subject: [PATCH 025/472] Remove IServiceEndPointResolver.DisplayName property and use ToString() instead (#1761) --- .../IServiceEndPointResolver.cs | 5 ----- .../DnsServiceEndPointResolver.cs | 2 +- .../DnsServiceEndPointResolverBase.cs | 8 ++++++-- .../DnsSrvServiceEndPointResolver.cs | 5 ++++- .../Configuration/ConfigurationServiceEndPointResolver.cs | 3 --- .../PassThrough/PassThroughServiceEndPointResolver.cs | 4 ++-- .../ServiceEndPointResolver.Log.cs | 2 +- .../ServiceEndPointResolverFactory.Log.cs | 2 +- .../ServiceEndPointResolverTests.cs | 2 -- 9 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index 3cbb9b6c491..c228847c568 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,11 +8,6 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { - /// - /// Gets the diagnostic display name for this resolver. - /// - string DisplayName { get; } - /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index b0d30530c72..814c566a23e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -23,7 +23,7 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; /// - public override string DisplayName => "DNS"; + public override string ToString() => "DNS"; protected override async Task ResolveAsyncCore() { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 238ace927fa..ae4ba97c2a2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -44,16 +44,18 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } - public abstract string DisplayName { get; } - private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } protected abstract double RetryBackOffFactor { get; } + protected abstract TimeSpan MinRetryPeriod { get; } + protected abstract TimeSpan MaxRetryPeriod { get; } + protected abstract TimeSpan DefaultRefreshPeriod { get; } + protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// @@ -116,7 +118,9 @@ private async Task ResolveAsyncInternal() } protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero); + protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); + private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) { lock (_lock) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index ce8ae159764..65ec30f8f29 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -20,11 +20,14 @@ internal sealed partial class DnsSrvServiceEndPointResolver( TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; - public override string DisplayName => "DNS SRV"; + public override string ToString() => "DNS SRV"; string IHostNameFeature.HostName => hostName; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index fd382487551..89ebce80ed4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -49,9 +49,6 @@ public ConfigurationServiceEndPointResolver( _options = options; } - /// - public string DisplayName => "Configuration"; - /// public ValueTask DisposeAsync() => default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index 4ee6b0dd32d..02349c74593 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -12,8 +12,6 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver { - public string DisplayName => "Pass-through"; - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) { if (endPoints.EndPoints.Count != 0) @@ -29,4 +27,6 @@ public ValueTask ResolveAsync(ServiceEndPointCollectionSource } public ValueTask DisposeAsync() => default; + + public override string ToString() => "Pass-through"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs index 274d471030d..bbde620ac1e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -30,7 +30,7 @@ static string GetEndPointString(ServiceEndPoint ep) { if (ep.Features.Get() is { } resolver) { - return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + return $"{ep.GetEndPointString()} ({resolver})"; } return ep.GetEndPointString(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs index 23d4b03cd67..fdf08f4fa6e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -16,7 +16,7 @@ public static void CreatingResolver(ILogger logger, string serviceName, List r.DisplayName))); + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); } } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index ff964eb28f6..64972b37fb8 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -77,8 +77,6 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { - public string DisplayName => "Fake"; - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 43a49c3ef17bcea8b15c9f6d8968dc223d085704 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 13 Feb 2024 17:41:51 -0600 Subject: [PATCH 026/472] Enable XML doc error (#2191) * Enable XML doc error Fail the build when a public member is not documented. Since there are so many violations, for the current ones that weren't straight forward, I added a TODO for someone more familiar with the API to add the comments. * Add XML docs for new APIs --- .../ResolutionStatus.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs index 152571bbb74..04eec95dc63 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs @@ -92,6 +92,7 @@ public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && /// public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message); + /// public override string ToString() => Exception switch { not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]", From 8740979273a910a8115ba6255c42f77ab2d4a74b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 23 Feb 2024 06:43:45 -0800 Subject: [PATCH 027/472] Service Discovery: set port to 0 when scheme is not present, not -1 (#1885) --- .../Internal/ServiceNameParts.cs | 3 ++- ...PassThroughServiceEndPointResolverTests.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 0d310190a33..f9ab6ff6c2b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -46,6 +46,7 @@ static ServiceNameParts Create(Uri uri, bool hasScheme) var segmentSeparatorIndex = uriHost.IndexOf('.'); string host; string? endPointName = null; + var port = uri.Port > 0 ? uri.Port : 0; if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') { endPointName = uriHost[1..segmentSeparatorIndex]; @@ -62,7 +63,7 @@ static ServiceNameParts Create(Uri uri, bool hasScheme) } } - return new(host, endPointName, uri.Port); + return new(host, endPointName, port); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 9d3e6ac17e7..9325336f319 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -109,4 +109,26 @@ public async Task ResolveServiceEndPoint_Fallback() Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); } } + + // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. + [Fact] + public async Task ResolveServiceEndPoint_Fallback_NoScheme() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + + var resolver = services.GetRequiredService(); + var endPoints = await resolver.GetEndPointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); + } } From c85b3ef835e9b472e738d6a4d0b2228c5a1e82aa Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:04:14 -0800 Subject: [PATCH 028/472] Rename key Service Discovery types (#1877) * Service Discovery refactoring * Rename ServiceEndPointResolver to ServiceEndPointWatcher * Rename ServiceEndPointResolverRegistry to ServiceEndPointResolver * Rename IServiceEndPointResolver to IServiceEndPointProvider --- ...esolver.cs => IServiceEndPointProvider.cs} | 6 +- .../IServiceEndPointResolverProvider.cs | 6 +- .../DnsServiceEndPointResolver.cs | 2 +- .../DnsServiceEndPointResolverBase.Log.cs | 2 +- .../DnsServiceEndPointResolverBase.cs | 2 +- .../DnsServiceEndPointResolverProvider.cs | 4 +- .../DnsSrvServiceEndPointResolver.cs | 2 +- .../DnsSrvServiceEndPointResolverProvider.cs | 4 +- .../ServiceDiscoveryDestinationResolver.cs | 6 +- .../ConfigurationServiceEndPointResolver.cs | 4 +- ...gurationServiceEndPointResolverProvider.cs | 2 +- .../HostingExtensions.cs | 2 +- .../Http/HttpClientBuilderExtensions.cs | 1 - .../Http/HttpServiceEndPointResolver.cs | 10 +- .../PassThroughServiceEndPointResolver.cs | 4 +- ...sThroughServiceEndPointResolverProvider.cs | 2 +- .../ServiceEndPointResolver.cs | 441 ++++++------------ .../ServiceEndPointResolverFactory.Log.cs | 4 +- .../ServiceEndPointResolverFactory.cs | 14 +- .../ServiceEndPointResolverOptions.cs | 2 +- .../ServiceEndPointResolverRegistry.cs | 240 ---------- ...r.Log.cs => ServiceEndPointWatcher.Log.cs} | 4 +- .../ServiceEndPointWatcher.cs | 383 +++++++++++++++ .../DnsSrvServiceEndPointResolverTests.cs | 6 +- ...nfigurationServiceEndPointResolverTests.cs | 8 +- ...PassThroughServiceEndPointResolverTests.cs | 8 +- .../ServiceEndPointResolverTests.cs | 16 +- 27 files changed, 597 insertions(+), 588 deletions(-) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolver.cs => IServiceEndPointProvider.cs} (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.Log.cs => ServiceEndPointWatcher.Log.cs} (94%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index c228847c568..3b369a97850 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs @@ -4,12 +4,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Functionality for resolving endpoints for a service. +/// Provides details about a service's endpoints. /// -public interface IServiceEndPointResolver : IAsyncDisposable +public interface IServiceEndPointProvider : IAsyncDisposable { /// - /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. + /// Resolves the endpoints for the service. /// /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs index 9ad9e3ae7b8..51343a53697 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs @@ -6,15 +6,15 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Creates instances. +/// Creates instances. /// public interface IServiceEndPointResolverProvider { /// - /// Tries to create an instance for the specified . + /// Tries to create an instance for the specified . /// /// The service to create the resolver for. /// The resolver. /// if the resolver was created, otherwise. - bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver); + bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 814c566a23e..4a8350483eb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -34,7 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index c3384d20e19..cd664215aa9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndPointResolverBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index ae4ba97c2a2..516c8ed1f69 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// /// A service end point resolver that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointResolver +internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider { private readonly object _lock = new(); private readonly ILogger _logger; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index fc5f707e411..04eec3ac52c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Provides instances which resolve endpoints from DNS. +/// Provides instances which resolve endpoints from DNS. /// /// /// Initializes a new instance. @@ -24,7 +24,7 @@ internal sealed partial class DnsServiceEndPointResolverProvider( TimeProvider timeProvider) : IServiceEndPointResolverProvider { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { if (!ServiceNameParts.TryParse(serviceName, out var parts)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 65ec30f8f29..97a0d47d028 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -90,7 +90,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index e5a7e23710e..ced6e2c4a5f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Provides instances which resolve endpoints from DNS using SRV queries. +/// Provides instances which resolve endpoints from DNS using SRV queries. /// /// /// Initializes a new instance. @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index fbcef8c72ad..e35be5d629b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -13,8 +13,8 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// /// Initializes a new instance. /// -/// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolverRegistry registry) : IDestinationResolver +/// The endpoint resolver registry. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,7 +54,7 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var endPoints = await registry.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 89ebce80ed4..8ffb3de4039 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// A service endpoint resolver that uses configuration to resolve endpoints. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver, IHostNameFeature +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature { private readonly string _serviceName; private readonly string? _endpointName; @@ -147,7 +147,7 @@ static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index affe3a655ed..638c3e6d640 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -24,7 +24,7 @@ public class ConfigurationServiceEndPointResolverProvider( private readonly ILogger _logger = loggerFactory.CreateLogger(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 94f806a4016..5326d5a2935 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -39,7 +39,7 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services.TryAddSingleton(static sp => TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 239db961e26..0a507fb2f0b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -53,7 +53,6 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var selectorProvider = services.GetRequiredService(); var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index da7fee6dd47..a135a2b02ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -10,13 +10,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable { private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; + private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory; private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -148,7 +148,7 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateResolver(serviceName); + var resolver = _resolverFactory.CreateResolver(serviceName); var selector = _selectorProvider.CreateSelector(); var result = new ResolverEntry(resolver, selector); resolver.Start(); @@ -157,7 +157,7 @@ private ResolverEntry CreateResolver(string serviceName) private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointResolver _resolver; + private readonly ServiceEndPointWatcher _resolver; private readonly IServiceEndPointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; @@ -165,7 +165,7 @@ private sealed class ResolverEntry : IAsyncDisposable private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointResolver resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) { _resolver = resolver; _selector = selector; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index 02349c74593..ab0ea286b68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider { public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) { @@ -21,7 +21,7 @@ public ValueTask ResolveAsync(ServiceEndPointCollectionSource Log.UsingPassThrough(logger, serviceName); var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); + ep.Features.Set(this); endPoints.EndPoints.Add(ep); return new(ResolutionStatus.Success); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 24028f24ee5..32a64b8d350 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index b8356d56af5..965363dea9e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -1,383 +1,250 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.ExceptionServices; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Resolves endpoints for a specified service. +/// Resolves service names to collections of endpoints. /// -public sealed partial class ServiceEndPointResolver( - IServiceEndPointResolver[] resolvers, - ILogger logger, - string serviceName, - TimeProvider timeProvider, - IOptions options) : IAsyncDisposable +public sealed class ServiceEndPointResolver : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: true); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ILogger _logger = logger; - private readonly TimeProvider _timeProvider = timeProvider; - private readonly ServiceEndPointResolverOptions _options = options.Value; - private readonly IServiceEndPointResolver[] _resolvers = resolvers; - private readonly CancellationTokenSource _disposalCancellation = new(); - private ITimer? _pollingTimer; - private ServiceEndPointCollection? _cachedEndPoints; - private Task _refreshTask = Task.CompletedTask; - private volatile CacheStatus _cacheState; + private readonly ServiceEndPointResolverFactory _resolverProvider; + private readonly TimeProvider _timeProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + private bool _disposed; /// - /// Gets the service name. + /// Initializes a new instance of the class. /// - public string ServiceName { get; } = serviceName; - - /// - /// Gets or sets the action called when endpoints are updated. - /// - public Action? OnEndPointsUpdated { get; set; } - - /// - /// Starts the endpoint resolver. - /// - public void Start() + /// The resolver factory. + /// The time provider. + internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) { - ThrowIfNoResolvers(); - _ = RefreshAsync(force: false); + _resolverProvider = resolverProvider; + _timeProvider = timeProvider; } /// - /// Returns a collection of resolved endpoints for the service. + /// Resolves and returns service endpoints for the specified service. /// + /// The service name. /// The cancellation token. - /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + /// The resolved service endpoints. + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { - ThrowIfNoResolvers(); + ArgumentNullException.ThrowIfNull(serviceName); + ObjectDisposedException.ThrowIf(_disposed, this); - // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) - { - return new ValueTask(cached); - } + EnsureCleanupTimerStarted(); - // Otherwise, ensure the cache is being refreshed - // Wait for the cache refresh to complete and return the cached value. - return GetEndPointsInternal(cancellationToken); - - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + while (true) { - ServiceEndPointCollection? result; - do - { - await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; - } while (result is null); - return result; - } - } + ObjectDisposedException.ThrowIf(_disposed, this); + var resolver = _resolvers.GetOrAdd( + serviceName, + static (name, self) => self.CreateResolver(name), + this); - // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation - private Task RefreshAsync(bool force) - { - lock (_lock) - { - // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + if (valid) { - // Indicate that the cache is being updated and start a new refresh task. - _cacheState = CacheStatus.Refreshing; - - // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. - var restoreFlow = false; - try + if (result is null) { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _refreshTask = RefreshAsyncInternal(); + throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); } - finally - { - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } - } - } - return _refreshTask; + return result; + } } } - private async Task RefreshAsyncInternal() + private void EnsureCleanupTimerStarted() { - await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); - var cancellationToken = _disposalCancellation.Token; - Exception? error = null; - ServiceEndPointCollection? newEndPoints = null; - CacheStatus newCacheState; - ResolutionStatus status = ResolutionStatus.Success; - while (true) - { - try - { - var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); - status = ResolutionStatus.Success; - Log.ResolvingEndPoints(_logger, ServiceName); - foreach (var resolver in _resolvers) - { - var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); - status = CombineStatus(status, resolverStatus); - } - - var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; - if (statusCode != ResolutionStatusCode.Success) - { - if (statusCode is ResolutionStatusCode.Pending) - { - // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. - Log.ResolutionPending(_logger, ServiceName); - await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); - continue; - } - else if (statusCode is ResolutionStatusCode.Cancelled) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception ?? new OperationCanceledException(); - break; - } - else if (statusCode is ResolutionStatusCode.Error) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception; - break; - } - } - - lock (_lock) - { - // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) - { - // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } - } - else - { - SchedulePollingTimer(); - } - - // The cache is valid - newEndPoints = endPoints; - newCacheState = CacheStatus.Valid; - break; - } - } - catch (Exception exception) - { - error = exception; - newCacheState = CacheStatus.Invalid; - SchedulePollingTimer(); - status = CombineStatus(status, ResolutionStatus.FromException(exception)); - break; - } - } - - // If there was an error, the cache must be invalid. - Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, - // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but - // that will have more overhead in the common case. - if (newCacheState is CacheStatus.Valid) - { - Interlocked.Exchange(ref _cachedEndPoints, null); - } - - if (OnEndPointsUpdated is { } callback) + if (_cleanupTimer is not null) { - callback(new(newEndPoints, status)); + return; } lock (_lock) { - if (newCacheState is CacheStatus.Valid) + if (_cleanupTimer is not null) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + return; } - _cacheState = newCacheState; - } - - if (error is not null) - { - Log.ResolutionFailed(_logger, error, ServiceName); - ExceptionDispatchInfo.Throw(error); - } - else if (newEndPoints is not null) - { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); - } - } - - private void SchedulePollingTimer() - { - lock (_lock) - { - if (_pollingTimer is null) + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try { - _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = _timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); } - else + finally { - _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } } } } - private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) + /// + public async ValueTask DisposeAsync() { - if (existing.StatusCode > newStatus.StatusCode) + lock (_lock) { - return existing; + _disposed = true; + _cleanupTimer?.Dispose(); + _cleanupTimer = null; } - var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); - Exception? exception; - if (existing.Exception is not null && newStatus.Exception is not null) - { - List exceptions = new(); - AddExceptions(existing.Exception, exceptions); - AddExceptions(newStatus.Exception, exceptions); - exception = new AggregateException(exceptions); - } - else + foreach (var resolver in _resolvers) { - exception = existing.Exception ?? newStatus.Exception; + await resolver.Value.DisposeAsync().ConfigureAwait(false); } - var message = code switch + _resolvers.Clear(); + if (_cleanupTask is not null) { - ResolutionStatusCode.Error => exception!.Message ?? "Error", - _ => code.ToString(), - }; - - return new ResolutionStatus(code, exception, message); - - static void AddExceptions(Exception? exception, List exceptions) - { - if (exception is AggregateException ae) - { - exceptions.AddRange(ae.InnerExceptions); - } - else if (exception is not null) - { - exceptions.Add(exception); - } + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } } - /// - public async ValueTask DisposeAsync() + private void CleanupResolvers() { lock (_lock) { - if (_pollingTimer is { } timer) + if (_cleanupTask is null or { IsCompleted: true }) { - _pollingTimer = null; - timer.Dispose(); + _cleanupTask = CleanupResolversAsyncCore(); } } + } - _disposalCancellation.Cancel(); - if (_refreshTask is { } task) + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) { - await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } } - - foreach (var resolver in _resolvers) + if (cleanupTasks is not null) { - await resolver.DisposeAsync().ConfigureAwait(false); + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); } } - private enum CacheStatus + private ResolverEntry CreateResolver(string serviceName) { - Invalid, - Refreshing, - Valid + var resolver = _resolverProvider.CreateResolver(serviceName); + resolver.Start(); + return new ResolverEntry(resolver); } - private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) + private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable { - if (changeToken.HasChanged) + private readonly ServiceEndPointWatcher _resolver = resolver; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() { - return; - } + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); - TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - IDisposable? changeTokenRegistration = null; - IDisposable? cancellationRegistration = null; - IDisposable? pollPeriodRegistration = null; - CancellationTokenSource? timerCancellation = null; + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } - try + public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) { - // Either wait for a callback or poll externally. - if (changeToken.ActiveChangeCallbacks) + try { - changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endPoints); + } + else + { + return (false, default); + } } - else + finally { - timerCancellation = new(pollPeriod); - pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } } + } - if (cancellationToken.CanBeCanceled) + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) { - cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); } - await completion.Task.ConfigureAwait(false); - } - finally - { - changeTokenRegistration?.Dispose(); - cancellationRegistration?.Dispose(); - pollPeriodRegistration?.Dispose(); - timerCancellation?.Dispose(); + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } } - } - private void ThrowIfNoResolvers() - { - if (_resolvers.Length == 0) + private async Task DisposeAsyncCore() { - ThrowNoResolversConfigured(); + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } } } - - [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs index fdf08f4fa6e..d7835f26d08 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; -public partial class ServiceEndPointResolverFactory +partial class ServiceEndPointResolverFactory { private sealed partial class Log { [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) { if (logger.IsEnabled(LogLevel.Debug)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index ce020178406..c545c82e9e6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -9,29 +9,29 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Creates instances. +/// Creates service endpoint resolvers. /// public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, - ILogger resolverLogger, + ILogger resolverLogger, IOptions options, TimeProvider timeProvider) { private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _logger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; /// - /// Creates a instance for the provided service name. + /// Creates a service endpoint resolver for the provided service name. /// - public ServiceEndPointResolver CreateResolver(string serviceName) + public ServiceEndPointWatcher CreateResolver(string serviceName) { ArgumentNullException.ThrowIfNull(serviceName); - List? resolvers = null; + List? resolvers = null; foreach (var factory in _resolverProviders) { if (factory.TryCreateResolver(serviceName, out var resolver)) @@ -47,7 +47,7 @@ public ServiceEndPointResolver CreateResolver(string serviceName) } Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointResolver( + return new ServiceEndPointWatcher( resolvers: [.. resolvers], logger: _logger, serviceName: serviceName, diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs index 8a2b37a5048..415a2192c30 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Options for . +/// Options for service endpoint resolvers. /// public sealed class ServiceEndPointResolverOptions { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs deleted file mode 100644 index fb71bb9b85c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs +++ /dev/null @@ -1,240 +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.Collections.Concurrent; -using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Resolves service names to collections of endpoints. -/// -/// The resolver factory. -/// The time provider. -public sealed class ServiceEndPointResolverRegistry(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) : IAsyncDisposable -{ - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolverRegistry)s!).CleanupResolvers(); - private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); - - private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; - private readonly ConcurrentDictionary _resolvers = new(); - private ITimer? _cleanupTimer; - private Task? _cleanupTask; - private bool _disposed; - - /// - /// Resolves and returns service endpoints for the specified service. - /// - /// The service name. - /// The cancellation token. - /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(serviceName); - ObjectDisposedException.ThrowIf(_disposed, this); - - EnsureCleanupTimerStarted(); - - while (true) - { - ObjectDisposedException.ThrowIf(_disposed, this); - var resolver = _resolvers.GetOrAdd( - serviceName, - static (name, self) => self.CreateResolver(name), - this); - - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - if (valid) - { - if (result is null) - { - throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); - } - - return result; - } - } - } - - private void EnsureCleanupTimerStarted() - { - if (_cleanupTimer is not null) - { - return; - } - - lock (_lock) - { - if (_cleanupTimer is not null) - { - return; - } - - // Don't capture the current ExecutionContext and its AsyncLocals onto the timer - var restoreFlow = false; - try - { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); - } - finally - { - // Restore the current ExecutionContext - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } - } - } - } - - /// - public async ValueTask DisposeAsync() - { - lock (_lock) - { - _disposed = true; - _cleanupTimer?.Dispose(); - _cleanupTimer = null; - } - - foreach (var resolver in _resolvers) - { - await resolver.Value.DisposeAsync().ConfigureAwait(false); - } - - _resolvers.Clear(); - if (_cleanupTask is not null) - { - await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - } - } - - private void CleanupResolvers() - { - lock (_lock) - { - if (_cleanupTask is null or { IsCompleted: true }) - { - _cleanupTask = CleanupResolversAsyncCore(); - } - } - } - - private async Task CleanupResolversAsyncCore() - { - List? cleanupTasks = null; - foreach (var (name, resolver) in _resolvers) - { - if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) - { - cleanupTasks ??= new(); - cleanupTasks.Add(resolver.DisposeAsync().AsTask()); - } - } - if (cleanupTasks is not null) - { - await Task.WhenAll(cleanupTasks).ConfigureAwait(false); - } - } - - private ResolverEntry CreateResolver(string serviceName) - { - var resolver = _resolverProvider.CreateResolver(serviceName); - resolver.Start(); - return new ResolverEntry(resolver); - } - - private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable - { - private readonly ServiceEndPointResolver _resolver = resolver; - private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); - private const ulong RecentUseFlag = 1UL << 62; - private const ulong DisposingFlag = 1UL << 63; - private ulong _status; - private TaskCompletionSource? _onDisposed; - - public string ServiceName => _resolver.ServiceName; - - public bool CanExpire() - { - // Read the status, clearing the recent use flag in the process. - var status = Interlocked.And(ref _status, ~RecentUseFlag); - - // The instance can be expired if there are no concurrent callers and the recent use flag was not set. - return (status & (CountMask | RecentUseFlag)) == 0; - } - - public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) - { - try - { - var status = Interlocked.Increment(ref _status); - if ((status & DisposingFlag) == 0) - { - // If the resolver is valid, resolve. - // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); - } - else - { - return (false, default); - } - } - finally - { - // Set the recent use flag to prevent the instance from being disposed. - Interlocked.Or(ref _status, RecentUseFlag); - - // If we are the last concurrent request to complete and the Disposing flag has been set, - // dispose the resolver now. DisposeAsync was prevented by concurrent requests. - var status = Interlocked.Decrement(ref _status); - if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) - { - await DisposeAsyncCore().ConfigureAwait(false); - } - } - } - - public async ValueTask DisposeAsync() - { - if (_onDisposed is null) - { - Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); - } - - var status = Interlocked.Or(ref _status, DisposingFlag); - if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) - { - // If we are the one who flipped the Disposing flag and there are no concurrent requests, - // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. - await DisposeAsyncCore().ConfigureAwait(false); - } - else - { - await _onDisposed.Task.ConfigureAwait(false); - } - } - - private async Task DisposeAsyncCore() - { - try - { - await _resolver.DisposeAsync().ConfigureAwait(false); - } - finally - { - Debug.Assert(_onDisposed is not null); - _onDisposed.SetResult(); - } - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index bbde620ac1e..17864811062 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; -public sealed partial class ServiceEndPointResolver +partial class ServiceEndPointWatcher { private sealed partial class Log { @@ -28,7 +28,7 @@ public static void ResolutionSucceeded(ILogger logger, string serviceName, Servi static string GetEndPointString(ServiceEndPoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } resolver) { return $"{ep.GetEndPointString()} ({resolver})"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs new file mode 100644 index 00000000000..51e597ee89d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Watches for updates to the collection of resolved endpoints for a specified service. +/// +public sealed partial class ServiceEndPointWatcher( + IServiceEndPointProvider[] resolvers, + ILogger logger, + string serviceName, + TimeProvider timeProvider, + IOptions options) : IAsyncDisposable +{ + private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); + + private readonly object _lock = new(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly ServiceEndPointResolverOptions _options = options.Value; + private readonly IServiceEndPointProvider[] _resolvers = resolvers; + private readonly CancellationTokenSource _disposalCancellation = new(); + private ITimer? _pollingTimer; + private ServiceEndPointCollection? _cachedEndPoints; + private Task _refreshTask = Task.CompletedTask; + private volatile CacheStatus _cacheState; + + /// + /// Gets the service name. + /// + public string ServiceName { get; } = serviceName; + + /// + /// Gets or sets the action called when endpoints are updated. + /// + public Action? OnEndPointsUpdated { get; set; } + + /// + /// Starts the endpoint resolver. + /// + public void Start() + { + ThrowIfNoResolvers(); + _ = RefreshAsync(force: false); + } + + /// + /// Returns a collection of resolved endpoints for the service. + /// + /// The cancellation token. + /// A collection of resolved endpoints for the service. + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + { + ThrowIfNoResolvers(); + + // If the cache is valid, return the cached value. + if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + { + return new ValueTask(cached); + } + + // Otherwise, ensure the cache is being refreshed + // Wait for the cache refresh to complete and return the cached value. + return GetEndPointsInternal(cancellationToken); + + async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + { + ServiceEndPointCollection? result; + do + { + await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); + result = _cachedEndPoints; + } while (result is null); + return result; + } + } + + // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation + private Task RefreshAsync(bool force) + { + lock (_lock) + { + // If the cache is invalid or needs invalidation, refresh the cache. + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + { + // Indicate that the cache is being updated and start a new refresh task. + _cacheState = CacheStatus.Refreshing; + + // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _refreshTask = RefreshAsyncInternal(); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + + return _refreshTask; + } + } + + private async Task RefreshAsyncInternal() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var cancellationToken = _disposalCancellation.Token; + Exception? error = null; + ServiceEndPointCollection? newEndPoints = null; + CacheStatus newCacheState; + ResolutionStatus status = ResolutionStatus.Success; + while (true) + { + try + { + var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); + status = ResolutionStatus.Success; + Log.ResolvingEndPoints(_logger, ServiceName); + foreach (var resolver in _resolvers) + { + var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); + status = CombineStatus(status, resolverStatus); + } + + var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); + var statusCode = status.StatusCode; + if (statusCode != ResolutionStatusCode.Success) + { + if (statusCode is ResolutionStatusCode.Pending) + { + // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. + Log.ResolutionPending(_logger, ServiceName); + await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); + continue; + } + else if (statusCode is ResolutionStatusCode.Cancelled) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception ?? new OperationCanceledException(); + break; + } + else if (statusCode is ResolutionStatusCode.Error) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception; + break; + } + } + + lock (_lock) + { + // Check if we need to poll for updates or if we can register for change notification callbacks. + if (endPoints.ChangeToken.ActiveChangeCallbacks) + { + // Initiate a background refresh, if necessary. + endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + else + { + SchedulePollingTimer(); + } + + // The cache is valid + newEndPoints = endPoints; + newCacheState = CacheStatus.Valid; + break; + } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); + status = CombineStatus(status, ResolutionStatus.FromException(exception)); + break; + } + } + + // If there was an error, the cache must be invalid. + Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + + // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task + // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // that will have more overhead in the common case. + if (newCacheState is CacheStatus.Valid) + { + Interlocked.Exchange(ref _cachedEndPoints, null); + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + + lock (_lock) + { + if (newCacheState is CacheStatus.Valid) + { + Debug.Assert(newEndPoints is not null); + _cachedEndPoints = newEndPoints; + } + + _cacheState = newCacheState; + } + + if (error is not null) + { + Log.ResolutionFailed(_logger, error, ServiceName); + ExceptionDispatchInfo.Throw(error); + } + else if (newEndPoints is not null) + { + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + } + } + + private void SchedulePollingTimer() + { + lock (_lock) + { + if (_pollingTimer is null) + { + _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + } + else + { + _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + } + } + } + + private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) + { + if (existing.StatusCode > newStatus.StatusCode) + { + return existing; + } + + var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); + Exception? exception; + if (existing.Exception is not null && newStatus.Exception is not null) + { + List exceptions = new(); + AddExceptions(existing.Exception, exceptions); + AddExceptions(newStatus.Exception, exceptions); + exception = new AggregateException(exceptions); + } + else + { + exception = existing.Exception ?? newStatus.Exception; + } + + var message = code switch + { + ResolutionStatusCode.Error => exception!.Message ?? "Error", + _ => code.ToString(), + }; + + return new ResolutionStatus(code, exception, message); + + static void AddExceptions(Exception? exception, List exceptions) + { + if (exception is AggregateException ae) + { + exceptions.AddRange(ae.InnerExceptions); + } + else if (exception is not null) + { + exceptions.Add(exception); + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + + _disposalCancellation.Cancel(); + if (_refreshTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + foreach (var resolver in _resolvers) + { + await resolver.DisposeAsync().ConfigureAwait(false); + } + } + + private enum CacheStatus + { + Invalid, + Refreshing, + Valid + } + + private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) + { + if (changeToken.HasChanged) + { + return; + } + + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + IDisposable? changeTokenRegistration = null; + IDisposable? cancellationRegistration = null; + IDisposable? pollPeriodRegistration = null; + CancellationTokenSource? timerCancellation = null; + + try + { + // Either wait for a callback or poll externally. + if (changeToken.ActiveChangeCallbacks) + { + changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + else + { + timerCancellation = new(pollPeriod); + pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + + if (cancellationToken.CanBeCanceled) + { + cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + } + + await completion.Task.ConfigureAwait(false); + } + finally + { + changeTokenRegistration?.Dispose(); + cancellationRegistration?.Dispose(); + pollPeriodRegistration?.Dispose(); + timerCancellation?.Dispose(); + } + } + + private void ThrowIfNoResolvers() + { + if (_resolvers.Length == 0) + { + ThrowNoResolversConfigured(); + } + } + + [DoesNotReturn] + private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7531eec0a48..bd69969199d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class DnsSrvServiceEndPointResolverTests { @@ -104,7 +104,7 @@ public async Task ResolveServiceEndPoint_Dns() .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -191,7 +191,7 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo }; var services = serviceCollection.BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index e695362dc5d..66bc1ab8d1b 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class ConfigurationServiceEndPointResolverTests { @@ -30,7 +30,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -70,7 +70,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -114,7 +114,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 9325336f319..b29593265e9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for . -/// These also cover and by extension. +/// These also cover and by extension. /// public class PassThroughServiceEndPointResolverTests { @@ -26,7 +26,7 @@ public async Task ResolveServiceEndPoint_PassThrough() .AddPassThroughServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -58,7 +58,7 @@ public async Task ResolveServiceEndPoint_Superseded() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -92,7 +92,7 @@ public async Task ResolveServiceEndPoint_Fallback() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 64972b37fb8..0628bedbe73 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// public class ServiceEndPointResolverTests { @@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointResolver([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); var exception = Assert.Throws(resolverFactory.Start); Assert.Equal("No service endpoint resolvers are configured.", exception.Message); exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); @@ -65,9 +65,9 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider { - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(serviceName); @@ -75,7 +75,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi } } - private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver + private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider { public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); @@ -106,7 +106,7 @@ public async Task ResolveServiceEndPoint() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -157,7 +157,7 @@ public async Task ResolveServiceEndPointOneShot() .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); @@ -242,7 +242,7 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); From 07dd40bad112fb5b92c04e3b2bd7b24eebe16203 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:03:30 -0800 Subject: [PATCH 029/472] Fix PassThroughServiceEndPointResolverTests (#2451) --- .../PassThroughServiceEndPointResolverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index b29593265e9..696061949d9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -127,7 +127,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); var endPoints = await resolver.GetEndPointsAsync("catalog", default); Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); } From 2b01e48ecd7e26ec32c76478626b0a0e8c493d2b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:03:31 -0800 Subject: [PATCH 030/472] Service Discovery: allow multiple schemes to be specified in request (#2719) * Service Discovery: allow multiple schemes to be specified. Add endpoint name to configuration path. * Remove superfluous WriteServiceDiscoveryEnvironmentVariables method * Rename AllocatedEndpointAnnotation to AllocatedEndpoint and make it a property on EndpointAnnotation * Remove unnecessary members * Add endpoint from launch profile * Specify launch profile in test projects --- ...sions.ServiceDiscovery.Abstractions.csproj | 1 + .../UriEndPoint.cs | 30 +++ .../DnsServiceEndPointResolverProvider.cs | 14 +- .../DnsSrvServiceEndPointResolverProvider.cs | 15 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 4 - ...viceDiscoveryForwarderHttpClientFactory.cs | 6 +- ...onfigurationServiceEndPointResolver.Log.cs | 13 +- .../ConfigurationServiceEndPointResolver.cs | 225 +++++++++++------- ...igurationServiceEndPointResolverOptions.cs | 24 +- ...gurationServiceEndPointResolverProvider.cs | 15 +- .../HostingExtensions.cs | 5 + .../Http/HttpClientBuilderExtensions.cs | 6 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 94 +++++--- .../ServiceDiscoveryOptionsValidator.cs | 20 ++ .../Internal/ServiceNameParser.cs | 78 ++++++ .../Internal/ServiceNameParts.cs | 82 +------ ...crosoft.Extensions.ServiceDiscovery.csproj | 1 + ...sThroughServiceEndPointResolverProvider.cs | 42 +++- .../ServiceDiscoveryOptions.cs | 29 +++ .../DnsSrvServiceEndPointResolverTests.cs | 4 +- ...nfigurationServiceEndPointResolverTests.cs | 164 ++++++++++++- ...PassThroughServiceEndPointResolverTests.cs | 8 +- 23 files changed, 628 insertions(+), 258 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index edafcce1914..e98dc409e76 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -6,6 +6,7 @@ true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) + Microsoft.Extensions.ServiceDiscovery diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs new file mode 100644 index 00000000000..6d3132da880 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// An endpoint represented by a . +/// +/// The . +public sealed class UriEndPoint(Uri uri) : EndPoint +{ + /// + /// Gets the associated with this endpoint. + /// + public Uri Uri => uri; + + /// + public override bool Equals(object? obj) + { + return obj is UriEndPoint other && Uri.Equals(other.Uri); + } + + /// + public override int GetHashCode() => Uri.GetHashCode(); + + /// + public override string? ToString() => uri.ToString(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 04eec3ac52c..8f676327d5f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -9,24 +9,16 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The time provider. internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index ced6e2c4a5f..bf2f502c97a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -10,21 +10,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS using SRV queries. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The DNS client. -/// The time provider. internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -49,7 +40,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 7fed469c4d8..0d4c3cbeac2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index 0da7e8b55eb..d37e4f1407d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; using Yarp.ReverseProxy.Forwarder; @@ -10,11 +11,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; internal sealed class ServiceDiscoveryForwarderHttpClientFactory( TimeProvider timeProvider, IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory) : ForwarderHttpClientFactory + ServiceEndPointResolverFactory factory, + IOptions options) : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, handler); + return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index b7e43172740..5916951c69d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -12,7 +12,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] @@ -38,11 +38,11 @@ public static void EndPointNameMatchSelection(ILogger logger, string serviceName } } - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] - public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName); - [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] - internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] + internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); @@ -67,5 +67,8 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin var configuredEndPoints = endpointValues.ToString(); ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); } + + [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] + internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 8ffb3de4039..dae054c9883 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,12 +11,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// A service endpoint resolver that uses configuration to resolve endpoints. +/// A service endpoint resolver that uses configuration to resolve resolved. /// internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature { + private const string DefaultEndPointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IOptions _options; @@ -28,16 +30,19 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// The configuration. /// The logger. /// The options. + /// The service name parser. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, ILogger logger, - IOptions options) + IOptions options, + ServiceNameParser parser) { - if (ServiceNameParts.TryParse(serviceName, out var parts)) + if (parser.TryParse(serviceName, out var parts)) { _serviceName = parts.Host; _endpointName = parts.EndPointName; + _schemes = parts.Schemes; } else { @@ -59,141 +64,193 @@ public ConfigurationServiceEndPointResolver( private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) { - // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. + // Only add resolved to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return ResolutionStatus.None; } - var root = _configuration; - var baseSectionName = _options.Value.SectionName; - if (baseSectionName is { Length: > 0 }) - { - root = root.GetSection(baseSectionName); - } - // Get the corresponding config section. - var section = root.GetSection(_serviceName); - var configPath = GetConfigurationPath(baseSectionName); - Log.UsingConfigurationPath(_logger, configPath, _serviceName); + var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); } - // Read the endpoint from the configuration. - // First check if there is a collection of sections - var children = section.GetChildren(); - if (children.Any()) + endPoints.AddChangeToken(section.GetReloadToken()); + + // Find an appropriate configuration section based on the input. + IConfigurationSection? namedSection = null; + string endpointName; + if (string.IsNullOrWhiteSpace(_endpointName)) { - var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); - if (values is { Count: > 0 }) + if (_schemes.Length == 0) { - // Use endpoint names if any of the values have an endpoint name set. - var parsedValues = ParseServiceNameParts(values, configPath); - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); - - var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + // Use the section named "default". + endpointName = DefaultEndPointName; + namedSection = section.GetSection(endpointName); + } + else + { + // Set the ideal endpoint name for error messages. + endpointName = _schemes[0]; - foreach (var uri in parsedValues) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + foreach (var scheme in _schemes) { - // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. - if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) - { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); - } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); + endpointName = scheme; + namedSection = candidate; + break; } } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) + else + { + // Use the section corresponding to the endpoint name. + endpointName = _endpointName; + namedSection = section.GetSection(_endpointName); + } + + var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; + if (!namedSection.Exists()) + { + return CreateNotFoundResponse(endPoints, configPath); + } + + List resolved = []; + Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); + + // Account for both the single and multi-value cases. + if (!string.IsNullOrWhiteSpace(namedSection.Value)) + { + // Single value case. + if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) + { + return error; + } + } + else { - if (EndPointNamesMatch(_endpointName, parsed)) + // Multiple value case. + foreach (var child in namedSection.GetChildren()) { - if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); } - if (_logger.IsEnabled(LogLevel.Debug)) + if (!TryAddEndPoint(resolved, child, endpointName, out var error)) { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); + return error; } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } - if (endPoints.EndPoints.Count == 0) + // Filter the resolved endpoints to only include those which match the specified scheme. + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - Log.ConfigurationNotFound(_logger, _serviceName, configPath); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } + } } - endPoints.AddChangeToken(section.GetReloadToken()); - return ResolutionStatus.Success; - - static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => - string.IsNullOrEmpty(parts.EndPointName) - || string.IsNullOrEmpty(endPointName) - || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); - } + var added = 0; + foreach (var ep in resolved) + { + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++added; + endPoints.EndPoints.Add(ep); + } + } + else + { + ++added; + endPoints.EndPoints.Add(ep); + } + } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) - { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + if (added == 0) { - serviceEndPoint.Features.Set(this); + return CreateNotFoundResponse(endPoints, configPath); } - return serviceEndPoint; - } + return ResolutionStatus.Success; - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); } - private string GetConfigurationPath(string? baseSectionName) + private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) { - var configPath = new StringBuilder(); - if (baseSectionName is { Length: > 0 }) + var value = section.Value; + if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - configPath.Append(baseSectionName).Append(':'); + error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); + return false; } - configPath.Append(_serviceName); - return configPath.ToString(); + endPoints.Add(CreateEndPoint(endPoint)); + error = default; + return true; } - private List ParseServiceNameParts(List input, string configPath) + private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) { - var results = new List(input.Count); - for (var i = 0; i < input.Count; ++i) + if (value.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri)) { - if (ServiceNameParts.TryParse(input[i], out var value)) + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(uri.Host, out var ip)) { - if (!results.Contains(value)) - { - results.Add(value); - } + endPoint = new IPEndPoint(ip, port); } else { - throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); + endPoint = new DnsEndPoint(uri.Host, port); } } + else if (Uri.TryCreate(value, default, out uri)) + { + endPoint = new UriEndPoint(uri); + } + else + { + endPoint = null; + return false; + } + + return true; + } + + private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + + return serviceEndPoint; + } - return results; + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'."); } public override string ToString() => "Configuration"; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs index 5e67a885ca9..c83589eb268 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -1,20 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Options; + namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// Options for . /// -public class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndPointResolverOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". /// - public string? SectionName { get; set; } = "Services"; + public string SectionName { get; set; } = "Services"; /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// public Func ApplyHostNameMetadata { get; set; } = _ => false; } + +internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + { + if (string.IsNullOrWhiteSpace(options.SectionName)) + { + return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); + } + + if (options.ApplyHostNameMetadata is null) + { + return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 638c3e6d640..472205f12f9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,28 +5,23 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// implementation that resolves services using . /// -/// The configuration. -/// The options. -/// The logger factory. -public class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider + ILogger logger, + ServiceNameParser parser) : IServiceEndPointResolverProvider { - private readonly IConfiguration _configuration = configuration; - private readonly IOptions _options = options; - private readonly ILogger _logger = loggerFactory.CreateLogger(); - /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 5326d5a2935..a4ba9b63b31 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -4,8 +4,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; @@ -36,6 +38,8 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection { services.AddOptions(); services.AddLogging(); + services.TryAddSingleton(); + services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(static sp => TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); @@ -63,6 +67,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS { services.AddServiceDiscoveryCore(); services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 0a507fb2f0b..c1c833de89f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -31,7 +31,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var timeProvider = services.GetService() ?? TimeProvider.System; var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. @@ -56,7 +57,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var selectorProvider = services.GetRequiredService(); var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 52ff1d4494d..ba32139f6ab 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -9,9 +10,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver) : HttpClientHandler +public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly ServiceDiscoveryOptions _options = options.Value; /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -23,7 +25,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 8c0d67f41bd..2ab3c8dc3ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -3,6 +3,7 @@ using System.Net; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -13,24 +14,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; public class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; + private readonly ServiceDiscoveryOptions _options; /// /// Initializes a new instance. /// /// The endpoint resolver. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver) + /// The service discovery options. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) { _resolver = resolver; + _options = options.Value; } /// /// Initializes a new instance. /// /// The endpoint resolver. + /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; + _options = options.Value; } /// @@ -43,7 +49,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result); + request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } @@ -64,37 +70,71 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint) + internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) { var endpoint = serviceEndPoint.EndPoint; - - string host; - int port; - switch (endpoint) + UriBuilder result; + if (endpoint is UriEndPoint { Uri: { } ep }) { - case IPEndPoint ip: - host = ip.Address.ToString(); - port = ip.Port; - break; - case DnsEndPoint dns: - host = dns.Host; - port = dns.Port; - break; - default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); - } + result = new UriBuilder(uri) + { + Scheme = ep.Scheme, + Host = ep.Host, + }; - var builder = new UriBuilder(uri) - { - Host = host, - }; + if (ep.Port > 0) + { + result.Port = ep.Port; + } - // Default to the default port for the scheme. - if (port > 0) + if (ep.AbsolutePath.Length > 1) + { + result.Path = $"{ep.AbsolutePath.TrimEnd('/')}/{uri.AbsolutePath.TrimStart('/')}"; + } + } + else { - builder.Port = port; + string host; + int port; + switch (endpoint) + { + case IPEndPoint ip: + host = ip.Address.ToString(); + port = ip.Port; + break; + case DnsEndPoint dns: + host = dns.Host; + port = dns.Port; + break; + default: + throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + } + + result = new UriBuilder(uri) + { + Host = host, + }; + + // Default to the default port for the scheme. + if (port > 0) + { + result.Port = port; + } + + if (uri.Scheme.IndexOf('+') > 0) + { + var scheme = uri.Scheme.Split('+')[0]; + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + result.Scheme = scheme; + } + else + { + throw new InvalidOperationException($"The scheme '{scheme}' is not allowed."); + } + } } - return builder.Uri; + return result.Uri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs new file mode 100644 index 00000000000..fae7bd6f4fc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal sealed class ServiceDiscoveryOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ServiceDiscoveryOptions options) + { + if (options.AllowedSchemes is null) + { + return ValidateOptionsResult.Fail("At least one allowed scheme must be specified."); + } + + return ValidateOptionsResult.Success; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs new file mode 100644 index 00000000000..de047481872 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal sealed class ServiceNameParser(IOptions options) +{ + private readonly string[] _allowedSchemes = options.Value.AllowedSchemes; + + public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) + { + if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) + { + parts = Create(uri, hasScheme: false); + return true; + } + + if (Uri.TryCreate(serviceName, default, out uri)) + { + parts = Create(uri, hasScheme: true); + return true; + } + + parts = default; + return false; + + ServiceNameParts Create(Uri uri, bool hasScheme) + { + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + var port = uri.Port > 0 ? uri.Port : 0; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". + var schemes = hasScheme ? ParseSchemes(uri.Scheme) : []; + return new(schemes, host, endPointName, port); + } + } + + private string[] ParseSchemes(string scheme) + { + if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes)) + { + return scheme.Split('+'); + } + + List result = []; + foreach (var s in scheme.Split('+')) + { + foreach (var allowed in _allowedSchemes) + { + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } + } + } + + return result.ToArray(); + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index f9ab6ff6c2b..f93729a40ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -1,15 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; -using System.Net; - namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal readonly struct ServiceNameParts : IEquatable { - public ServiceNameParts(string host, string? endPointName, int port) : this() + public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() { + Schemes = schemePriority; Host = host; EndPointName = endPointName; Port = port; @@ -17,86 +15,14 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public string? EndPointName { get; init; } + public string[] Schemes { get; init; } + public string Host { get; init; } public int Port { get; init; } public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; - public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) - { - if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) - { - parts = Create(uri, hasScheme: false); - return true; - } - - if (Uri.TryCreate(serviceName, default, out uri)) - { - parts = Create(uri, hasScheme: true); - return true; - } - - parts = default; - return false; - - static ServiceNameParts Create(Uri uri, bool hasScheme) - { - var uriHost = uri.Host; - var segmentSeparatorIndex = uriHost.IndexOf('.'); - string host; - string? endPointName = null; - var port = uri.Port > 0 ? uri.Port : 0; - if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') - { - endPointName = uriHost[1..segmentSeparatorIndex]; - - // Skip the endpoint name, including its prefix ('_') and suffix ('.'). - host = uriHost[(segmentSeparatorIndex + 1)..]; - } - else - { - host = uriHost; - if (hasScheme) - { - endPointName = uri.Scheme; - } - } - - return new(host, endPointName, port); - } - } - - public static bool TryCreateEndPoint(ServiceNameParts parts, [NotNullWhen(true)] out EndPoint? endPoint) - { - if (IPAddress.TryParse(parts.Host, out var ip)) - { - endPoint = new IPEndPoint(ip, parts.Port); - } - else if (!string.IsNullOrEmpty(parts.Host)) - { - endPoint = new DnsEndPoint(parts.Host, parts.Port); - } - else - { - endPoint = null; - return false; - } - - return true; - } - - public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) - { - if (TryParse(serviceName, out var parts)) - { - return TryCreateEndPoint(parts, out serviceEndPoint); - } - - serviceEndPoint = null; - return false; - } - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 1fa156be1d4..60ec55fe9df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 32a64b8d350..b3a326010bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -5,7 +5,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -17,7 +16,7 @@ internal sealed class PassThroughServiceEndPointResolverProvider(ILogger public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) + if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); @@ -26,4 +25,43 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } + + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + { + if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) + { + serviceEndPoint = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(host, out var ip)) + { + serviceEndPoint = new IPEndPoint(ip, port); + } + else if (!string.IsNullOrEmpty(host)) + { + serviceEndPoint = new DnsEndPoint(host, port); + } + else + { + serviceEndPoint = null; + return false; + } + + return true; + } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs new file mode 100644 index 00000000000..d9510a3cf22 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for configuring service discovery. +/// +public sealed class ServiceDiscoveryOptions +{ + /// + /// The value for which indicates that all schemes are allowed. + /// +#pragma warning disable IDE0300 // Simplify collection initialization +#pragma warning disable CA1825 // Avoid zero-length array allocations + public static readonly string[] AllSchemes = new string[0]; +#pragma warning restore CA1825 // Avoid zero-length array allocations +#pragma warning restore IDE0300 // Simplify collection initialization + + /// + /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". + /// + /// + /// When set to , all schemes are allowed. + /// Schemes are not case-sensitive. + /// + public string[] AllowedSchemes { get; set; } = AllSchemes; +} + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index bd69969199d..7e2ef478ef7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -164,8 +164,8 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo { InitialData = new Dictionary { - ["services:basket:0"] = "localhost:8080", - ["services:basket:1"] = "remotehost:9090", + ["services:basket:http:0"] = "localhost:8080", + ["services:basket:http:1"] = "remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index 66bc1ab8d1b..f35ffa2026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -22,7 +22,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { - ["services:basket"] = "localhost:8080", + ["services:basket:http"] = "localhost:8080", }); var services = new ServiceCollection() .AddSingleton(config.Build()) @@ -52,6 +52,72 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } } + [Fact] + public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + { + // Try to resolve an http endpoint when only https is allowed. + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:foo:0"] = "http://localhost:8080", + ["services:basket:foo:1"] = "https://localhost", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .Configure(o => o.AllowedSchemes = ["https"]) + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointWatcher resolver; + + // Explicitly specifying http. + // We should get no endpoint back because http is not allowed by configuration. + await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Empty(initialResult.EndPoints); + } + + // Specifying either https or http. + // The result should be that we only get the http endpoint back. + await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + + // Specifying either https or http, but in reverse. + // The result should be that we only get the http endpoint back. + await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + var ep = Assert.Single(initialResult.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + } + [Fact] public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { @@ -59,8 +125,8 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:http:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -82,8 +148,31 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. + await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); Assert.All(initialResult.EndPoints, ep => { @@ -101,10 +190,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", - ["services:basket:2"] = "http://_grpc.localhost:2222", - ["services:basket:3"] = "grpc://remotehost:2222", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:https:1"] = "https://remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -125,9 +216,60 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointWatcher resolver; + await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Equal(3, initialResult.EndPoints.Count); + + // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + + // We expect the HTTPS endpoint back but not the HTTP one. + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint); Assert.All(initialResult.EndPoints, ep => { diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 696061949d9..d8adcbca529 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -49,7 +49,7 @@ public async Task ResolveServiceEndPoint_Superseded() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:http:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -72,7 +72,7 @@ public async Task ResolveServiceEndPoint_Superseded() // We expect the basket service to be resolved from Configuration, not the pass-through provider. Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); } } @@ -83,7 +83,7 @@ public async Task ResolveServiceEndPoint_Fallback() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -118,7 +118,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); From b982636b25c3a6dfb498f4b5fce1ba12a8bd535d Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 18 Mar 2024 17:29:22 +1100 Subject: [PATCH 031/472] Filling in empty doc comments. (#2968) * Docs! * Build failures. * Update src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.AWS/SDKResourceExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs Co-authored-by: James Newton-King * A cancellation token instead of the cancellation token * More cancellation token. * Update src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs Co-authored-by: James Newton-King --------- Co-authored-by: James Newton-King --- .../Http/HttpServiceEndPointResolver.cs | 2 +- .../ServiceEndPointResolver.cs | 2 +- .../ServiceEndPointWatcher.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index a135a2b02ed..b4f6249f28f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -26,7 +26,7 @@ public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolver /// Resolves and returns a service endpoint for the specified request. /// /// The request message. - /// The cancellation token. + /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 965363dea9e..9de4a61b41b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -38,7 +38,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider /// Resolves and returns service endpoints for the specified service. /// /// The service name. - /// The cancellation token. + /// A . /// The resolved service endpoints. public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 51e597ee89d..8936d3722b5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -57,7 +57,7 @@ public void Start() /// /// Returns a collection of resolved endpoints for the service. /// - /// The cancellation token. + /// A . /// A collection of resolved endpoints for the service. public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { From 4e104b0ca3259f1960197922e6278af6eba6057a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 18 Mar 2024 10:32:00 -0500 Subject: [PATCH 032/472] Remove ValueStopwatch (#2935) It is not necessary on net8.0. --- .../Http/ResolvingHttpClientHandler.cs | 6 +++--- .../Http/ResolvingHttpDelegatingHandler.cs | 6 +++--- .../Microsoft.Extensions.ServiceDiscovery.csproj | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index ba32139f6ab..39eb65cc182 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Internal; +using System.Diagnostics; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -21,7 +21,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = ValueStopwatch.StartNew(); + var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -41,7 +41,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 2ab3c8dc3ec..976bfb331ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,7 +45,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = ValueStopwatch.StartNew(); + var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -65,7 +65,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 60ec55fe9df..6836e58cf6b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,6 @@ - From a846b38fa3433f3e7781d37ecb2cf6f07236e2b8 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:54:38 -0700 Subject: [PATCH 033/472] Point to Kubernetes C# client logic from IsInKubernetesCluster() (#3049) --- .../DnsSrvServiceEndPointResolverProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index bf2f502c97a..d02dcfb7274 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -122,6 +122,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private static bool IsInKubernetesCluster() { + // This logic is based on the Kubernetes C# client logic found here: + // https://github.com/kubernetes-client/csharp/blob/52c3c00d4c55b28bdb491a219f4967823a83df2d/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs#L21 var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT"); if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) From 38756b14164276c1a88239a8a932a68ce3956ae7 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:51:17 -0700 Subject: [PATCH 034/472] Service Discovery API refactoring (#3114) * API review feedback & general cleanup including removal of currently unused features * Align namespaces * Hide more of the API, rename for consistency * Hide more, rename more * ResolutionStatus does not need to be equatable * Make ServiceEndPointQuery public to break InternalsVisibleTo with Dns provider * Break InternalsVisibleTo from ServiceDiscovery package to YARP by adding a middleware factory * Remove ResolutionStatus, simplifying Service Discovery interfaces * Clean up ServiceEndPointImpl * Mark ServiceEndPointResolverResult as internal * Remove unnecessary members from ServiceEndPointCollection/Source * Seal service discovery types * Remove IServiceEndPointSelectorFactory and use DI instead * Remove unused endpoint selectors * Remove unused PendingStatusRefreshPeriod option * Rename UseServiceDiscovery to AddServiceDiscovery * Remove possible ambiguity in AddConfigurationServiceEndPointResolver signature * Add configuration delegate overloads to AddServiceDiscovery methods * Clean up logging in configuration-based service endpoint provider * API review: rename ServiceEndPointCollectionSource to IServiceEndPointBuilder * Rename IServiceDiscoveryDelegatingHttpMessageHandlerFactory * Rename IServiceEndPointProvider.ResolveAsync to PopulateAsync * Hide IServiceEndPointSelector * Remove allowedSchemes from ServiceEndPointQuery.TryParse * Rename ServiceEndPointQuery.Host to .ServiceName * Fix build * Review feedback * nit param rename * Improve ServiceEndPointQuery.ToString output * fixup --- .../Features/IEndPointHealthFeature.cs | 18 -- .../Features/IEndPointLoadFeature.cs | 16 -- .../{Features => }/IHostNameFeature.cs | 4 +- .../IServiceEndPointBuilder.cs | 29 +++ .../IServiceEndPointProvider.cs | 4 +- ....cs => IServiceEndPointProviderFactory.cs} | 10 +- .../IServiceEndPointSelectorProvider.cs | 16 -- .../Internal/ServiceEndPointImpl.cs | 18 +- .../ResolutionStatus.cs | 101 --------- .../ResolutionStatusCode.cs | 40 ---- .../ServiceEndPoint.cs | 3 +- .../ServiceEndPointCollectionSource.cs | 57 ----- .../ServiceEndPointQuery.cs | 97 +++++++++ .../ServiceEndPointResolverResult.cs | 30 --- ...Collection.cs => ServiceEndPointSource.cs} | 36 +--- .../DnsServiceEndPointResolver.cs | 4 +- .../DnsServiceEndPointResolverBase.cs | 68 ++---- .../DnsServiceEndPointResolverOptions.cs | 2 - .../DnsServiceEndPointResolverProvider.cs | 16 +- .../DnsSrvServiceEndPointResolver.cs | 4 +- .../DnsSrvServiceEndPointResolverOptions.cs | 2 - .../DnsSrvServiceEndPointResolverProvider.cs | 21 +- ...iscoveryDnsServiceCollectionExtensions.cs} | 8 +- .../ServiceDiscoveryDestinationResolver.cs | 22 +- ...viceDiscoveryForwarderHttpClientFactory.cs | 12 +- ...everseProxyServiceCollectionExtensions.cs} | 3 +- ...onfigurationServiceEndPointResolver.Log.cs | 42 +--- .../ConfigurationServiceEndPointResolver.cs | 79 +++---- ...erviceEndPointResolverOptionsValidator.cs} | 20 +- ...gurationServiceEndPointResolverProvider.cs | 13 +- ...igurationServiceEndPointResolverOptions.cs | 22 ++ .../Http/HttpServiceEndPointResolver.cs | 14 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Http/ResolvingHttpClientHandler.cs | 27 +-- .../Http/ResolvingHttpDelegatingHandler.cs | 16 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Internal/ServiceEndPointResolverResult.cs | 30 +++ .../Internal/ServiceNameParser.cs | 78 ------- .../Internal/ServiceNameParts.cs | 39 ---- .../IServiceEndPointSelector.cs | 8 +- .../PickFirstServiceEndPointSelector.cs | 29 --- ...ickFirstServiceEndPointSelectorProvider.cs | 18 -- ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 ----- ...oChoicesServiceEndPointSelectorProvider.cs | 18 -- .../RandomServiceEndPointSelector.cs | 29 --- .../RandomServiceEndPointSelectorProvider.cs | 18 -- .../RoundRobinServiceEndPointSelector.cs | 10 +- ...undRobinServiceEndPointSelectorProvider.cs | 18 -- ...crosoft.Extensions.ServiceDiscovery.csproj | 3 +- .../PassThroughServiceEndPointResolver.cs | 16 +- ...sThroughServiceEndPointResolverProvider.cs | 6 +- ...ceDiscoveryHttpClientBuilderExtensions.cs} | 33 +-- .../ServiceDiscoveryOptions.cs | 46 +++- ...ceDiscoveryServiceCollectionExtensions.cs} | 53 +++-- .../ServiceEndPointBuilder.cs | 46 ++++ .../ServiceEndPointResolver.cs | 11 +- .../ServiceEndPointResolverOptions.cs | 22 -- .../ServiceEndPointWatcher.Log.cs | 5 +- .../ServiceEndPointWatcher.cs | 199 ++++-------------- ...s => ServiceEndPointWatcherFactory.Log.cs} | 3 +- ...ry.cs => ServiceEndPointWatcherFactory.cs} | 22 +- .../UriEndPoint.cs | 4 +- .../DnsSrvServiceEndPointResolverTests.cs | 40 ++-- ...nfigurationServiceEndPointResolverTests.cs | 88 ++++---- ...PassThroughServiceEndPointResolverTests.cs | 34 ++- .../ServiceEndPointResolverTests.cs | 76 ++++--- 66 files changed, 680 insertions(+), 1284 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{Features => }/IHostNameFeature.cs (70%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolverProvider.cs => IServiceEndPointProviderFactory.cs} (59%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointCollection.cs => ServiceEndPointSource.cs} (55%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{HostingExtensions.cs => ServiceDiscoveryDnsServiceCollectionExtensions.cs} (88%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/{ReverseProxyServiceCollectionExtensions.cs => ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs} (94%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndPointResolverOptionsValidator.cs} (50%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery/LoadBalancing}/IServiceEndPointSelector.cs (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Http/HttpClientBuilderExtensions.cs => ServiceDiscoveryHttpClientBuilderExtensions.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{HostingExtensions.cs => ServiceDiscoveryServiceCollectionExtensions.cs} (57%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.Log.cs => ServiceEndPointWatcherFactory.Log.cs} (90%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.cs => ServiceEndPointWatcherFactory.cs} (69%) rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery}/UriEndPoint.cs (86%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs deleted file mode 100644 index 63dc3e11a3a..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing. -/// -public interface IEndPointHealthFeature -{ - /// - /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op. - /// - /// The response time of the endpoint. - /// An optional exception that occurred while checking the endpoint's health. - void ReportHealth(TimeSpan responseTime, Exception? exception); -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs deleted file mode 100644 index 2610f135945..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that provides information about the current load of an endpoint. -/// -public interface IEndPointLoadFeature -{ - /// - /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). - /// - public double CurrentLoad { get; } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs index fff3c3fa3f8..c7489472374 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Exposes the host name of the end point. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs new file mode 100644 index 00000000000..468adea1c09 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Builder to create a instances. +/// +public interface IServiceEndPointBuilder +{ + /// + /// Gets the endpoints. + /// + IList EndPoints { get; } + + /// + /// Gets the feature collection. + /// + IFeatureCollection Features { get; } + + /// + /// Adds a change token to the resulting . + /// + /// The change token. + void AddChangeToken(IChangeToken changeToken); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index 3b369a97850..950823257af 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. @@ -14,5 +14,5 @@ public interface IServiceEndPointProvider : IAsyncDisposable /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs similarity index 59% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs index 51343a53697..4b1876f808e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs @@ -3,18 +3,18 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public interface IServiceEndPointResolverProvider +public interface IServiceEndPointProviderFactory { /// - /// Tries to create an instance for the specified . + /// Tries to create an instance for the specified . /// - /// The service to create the resolver for. + /// The service to create the resolver for. /// The resolver. /// if the resolver was created, otherwise. - bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); + bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs deleted file mode 100644 index 27f4ec4324a..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Functionality for creating instances. -/// -public interface IServiceEndPointSelectorProvider -{ - /// - /// Creates an instance. - /// - /// A new instance. - IServiceEndPointSelector CreateSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index b73635ecd57..7d135dfe97d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -4,21 +4,11 @@ using System.Net; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl : ServiceEndPoint +internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint { - private readonly IFeatureCollection _features; - private readonly EndPoint _endPoint; - - public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) - { - _endPoint = endPoint; - _features = features ?? new FeatureCollection(); - } - - public override EndPoint EndPoint => _endPoint; - public override IFeatureCollection Features => _features; - + public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs deleted file mode 100644 index 04eec95dc63..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the status of an endpoint resolution operation. -/// -public readonly struct ResolutionStatus(ResolutionStatusCode statusCode, Exception? exception, string message) : IEquatable -{ - /// - /// Indicates that resolution was not performed. - /// - public static readonly ResolutionStatus None = new(ResolutionStatusCode.None, exception: null, message: ""); - - /// - /// Indicates that resolution is ongoing and has not yet completed. - /// - public static readonly ResolutionStatus Pending = new(ResolutionStatusCode.Pending, exception: null, message: "Pending"); - - /// - /// Indicates that resolution has completed successfully. - /// - public static readonly ResolutionStatus Success = new(ResolutionStatusCode.Success, exception: null, message: "Success"); - - /// - /// Indicates that resolution was cancelled. - /// - public static readonly ResolutionStatus Cancelled = new(ResolutionStatusCode.Cancelled, exception: null, message: "Cancelled"); - - /// - /// Indicates that resolution did not find a result for the service. - /// - public static ResolutionStatus CreateNotFound(string message) => new(ResolutionStatusCode.NotFound, exception: null, message: message); - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception. - /// A new instance. - public static ResolutionStatus FromException(Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Error, exception, exception.Message); - } - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception, if there was one. - /// A new instance. - public static ResolutionStatus FromPending(Exception? exception = null) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Pending, exception, exception.Message); - } - - /// - /// Gets the resolution status code. - /// - public ResolutionStatusCode StatusCode { get; } = statusCode; - - /// - /// Gets the resolution exception. - /// - - public Exception? Exception { get; } = exception; - - /// - /// Gets the resolution status message. - /// - public string Message { get; } = message; - - /// - /// Compares the provided operands, returning if they are equal and if they are not equal. - /// - public static bool operator ==(ResolutionStatus left, ResolutionStatus right) => left.Equals(right); - - /// - /// Compares the provided operands, returning if they are not equal and if they are equal. - /// - public static bool operator !=(ResolutionStatus left, ResolutionStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is ResolutionStatus status && Equals(status); - - /// - public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && - EqualityComparer.Default.Equals(Exception, other.Exception) && - Message == other.Message; - - /// - public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message); - - /// - public override string ToString() => Exception switch - { - not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]", - _ => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}]" - }; -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs deleted file mode 100644 index 7157eac758f..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Status codes for . -/// -public enum ResolutionStatusCode -{ - /// - /// Resolution has not been performed. - /// - None = 0, - - /// - /// Resolution is pending completion. - /// - Pending = 1, - - /// - /// Resolution did not find any end points for the specified service. - /// - NotFound = 2, - - /// - /// Resolution was successful. - /// - Success = 3, - - /// - /// Resolution was canceled. - /// - Cancelled = 4, - - /// - /// Resolution failed. - /// - Error = 5, -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs index 9dc4675dade..a3cde62ce0d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs @@ -4,8 +4,9 @@ using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs deleted file mode 100644 index 94f274a38e8..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs +++ /dev/null @@ -1,57 +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 Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A mutable collection of service endpoints. -/// -public class ServiceEndPointCollectionSource(string serviceName, IFeatureCollection features) -{ - private readonly List _endPoints = new(); - private readonly List _changeTokens = new(); - - /// - /// Gets the service name. - /// - public string ServiceName { get; } = serviceName; - - /// - /// Adds a change token. - /// - /// The change token. - public void AddChangeToken(IChangeToken changeToken) - { - _changeTokens.Add(changeToken); - } - - /// - /// Gets the composite change token. - /// - /// The composite change token. - public IChangeToken GetChangeToken() => new CompositeChangeToken(_changeTokens); - - /// - /// Gets the feature collection. - /// - public IFeatureCollection Features { get; } = features; - - /// - /// Gets the endpoints. - /// - public IList EndPoints => _endPoints; - - /// - /// Creates a from the provided instance. - /// - /// The source collection. - /// The service endpoint collection. - public static ServiceEndPointCollection CreateServiceEndPointCollection(ServiceEndPointCollectionSource source) - { - return new ServiceEndPointCollection(source.ServiceName, source._endPoints, source.GetChangeToken(), source.Features); - } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs new file mode 100644 index 00000000000..99c92cce27c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Describes a query for endpoints of a service. +/// +public sealed class ServiceEndPointQuery +{ + /// + /// Initializes a new instance. + /// + /// The string which the query was constructed from. + /// The ordered list of included URI schemes. + /// The service name. + /// The optional endpoint name. + private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName) + { + OriginalString = originalString; + IncludeSchemes = includedSchemes; + ServiceName = serviceName; + EndPointName = endPointName; + } + + /// + /// Tries to parse the provided input as a service endpoint query. + /// + /// The value to parse. + /// The resulting query. + /// if the value was successfully parsed; otherwise . + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query) + { + bool hasScheme; + if (!input.Contains("://", StringComparison.InvariantCulture) + && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) + { + hasScheme = false; + } + else if (Uri.TryCreate(input, default, out uri)) + { + hasScheme = true; + } + else + { + query = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". + var schemes = hasScheme ? uri.Scheme.Split('+') : []; + query = new(input, schemes, host, endPointName); + return true; + } + + /// + /// Gets the string which the query was constructed from. + /// + public string OriginalString { get; } + + /// + /// Gets the ordered list of included URI schemes. + /// + public IReadOnlyList IncludeSchemes { get; } + + /// + /// Gets the endpoint name, or if no endpoint name is specified. + /// + public string? EndPointName { get; } + + /// + /// Gets the service name. + /// + public string ServiceName { get; } + + /// + public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}"; +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs deleted file mode 100644 index 9179ed2f113..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs +++ /dev/null @@ -1,30 +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.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the result of service endpoint resolution. -/// -/// The endpoint collection. -/// The status. -public sealed class ServiceEndPointResolverResult(ServiceEndPointCollection? endPoints, ResolutionStatus status) -{ - /// - /// Gets the status. - /// - public ResolutionStatus Status { get; } = status; - - /// - /// Gets a value indicating whether resolution completed successfully. - /// - [MemberNotNullWhen(true, nameof(EndPoints))] - public bool ResolvedSuccessfully => Status.StatusCode is ResolutionStatusCode.Success; - - /// - /// Gets the endpoints. - /// - public ServiceEndPointCollection? EndPoints { get; } = endPoints; -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs similarity index 55% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs index c9540f2e7a1..807981226e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs @@ -1,47 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; using System.Diagnostics; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Represents an immutable collection of service endpoints. +/// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] [DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public class ServiceEndPointCollection : IReadOnlyList +public sealed class ServiceEndPointSource { private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointCollection(string serviceName, List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { - ArgumentNullException.ThrowIfNull(serviceName); ArgumentNullException.ThrowIfNull(changeToken); _endpoints = endpoints; Features = features; - ServiceName = serviceName; ChangeToken = changeToken; } - /// - public ServiceEndPoint this[int index] => _endpoints?[index] ?? throw new ArgumentOutOfRangeException(nameof(index)); - /// - /// Gets the service name. + /// Gets the endpoints. /// - public string ServiceName { get; } + public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -53,15 +46,6 @@ public ServiceEndPointCollection(string serviceName, List? endp /// public IFeatureCollection Features { get; } - /// - public int Count => _endpoints?.Count ?? 0; - - /// - public IEnumerator GetEnumerator() => _endpoints?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - /// public override string ToString() { @@ -73,15 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value) + private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) { - public string ServiceName => value.ServiceName; - public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.ToArray(); + public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4a8350483eb..a2601c84b45 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -4,7 +4,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -45,8 +44,7 @@ protected override async Task ResolveAsyncCore() if (endPoints.Count == 0) { - SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).")); - return; + throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); } SetResult(endPoints, ttl); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 516c8ed1f69..9d6c54e4755 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -18,7 +17,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private readonly TimeProvider _timeProvider; private long _lastRefreshTimeStamp; private Task _resolveTask = Task.CompletedTask; - private ResolutionStatus _lastStatus; + private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; private List? _lastEndPointCollection; @@ -59,13 +58,13 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return; } if (ShouldRefresh()) @@ -75,7 +74,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS { if (_resolveTask.IsCompleted && ShouldRefresh()) { - _resolveTask = ResolveAsyncInternal(); + _resolveTask = ResolveAsyncCore(); } resolveTask = _resolveTask; @@ -95,7 +94,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS } endPoints.AddChangeToken(_lastChangeToken); - return _lastStatus; + return; } } @@ -103,53 +102,21 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS protected abstract Task ResolveAsyncCore(); - private async Task ResolveAsyncInternal() - { - try - { - await ResolveAsyncCore().ConfigureAwait(false); - } - catch (Exception exception) - { - SetException(exception); - throw; - } - - } - - protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero); - - protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); - - private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + protected void SetResult(List endPoints, TimeSpan validityPeriod) { lock (_lock) { - if (exception is not null) + if (endPoints is { Count: > 0 }) { - _nextRefreshPeriod = GetRefreshPeriod(); - if (_lastEndPointCollection is null) - { - // Since end points have never been resolved, use a pending status to indicate that they might appear - // soon and to retry for some period until they do. - _lastStatus = ResolutionStatus.FromPending(exception); - } - else - { - _lastStatus = ResolutionStatus.FromException(exception); - } + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + _nextRefreshPeriod = DefaultRefreshPeriod; + _hasEndpoints = true; } - else if (endPoints is not { Count: > 0 }) + else { _nextRefreshPeriod = GetRefreshPeriod(); validityPeriod = TimeSpan.Zero; - _lastStatus = ResolutionStatus.Pending; - } - else - { - _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); - _nextRefreshPeriod = DefaultRefreshPeriod; - _lastStatus = ResolutionStatus.Success; + _hasEndpoints = false; } if (validityPeriod <= TimeSpan.Zero) @@ -169,13 +136,18 @@ private void SetResult(List? endPoints, Exception? exception, T TimeSpan GetRefreshPeriod() { - if (_lastStatus.StatusCode is ResolutionStatusCode.Success) + if (_hasEndpoints) { return MinRetryPeriod; } - var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor)); - return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod; + var nextTicks = (long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor); + if (nextTicks <= 0 || nextTicks > MaxRetryPeriod.Ticks) + { + return MaxRetryPeriod; + } + + return TimeSpan.FromTicks(nextTicks); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 37879b5f3e3..665c98bbc09 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 8f676327d5f..51525663a03 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -4,28 +4,18 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider); + resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 97a0d47d028..d59dbfbb69c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -6,7 +6,6 @@ using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -39,8 +38,7 @@ protected override async Task ResolveAsyncCore() var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); if (result.HasError) { - SetException(CreateException(srvQuery, result.ErrorMessage)); - return; + throw CreateException(srvQuery, result.ErrorMessage); } var lookupMapping = new Dictionary(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 5bac96c6c0a..704e03cd9ca 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index d02dcfb7274..8a75c1d1bbf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -5,8 +5,6 @@ using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -14,8 +12,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -23,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md @@ -33,6 +30,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". + var serviceName = query.OriginalString; if (string.IsNullOrWhiteSpace(_querySuffix)) { DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); @@ -40,16 +38,9 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - var portName = parts.EndPointName ?? "default"; - var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider); + var portName = query.EndPointName ?? "default"; + var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs similarity index 88% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index e385bde69a7..0d795660fd2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; namespace Microsoft.Extensions.Hosting; @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Hosting; /// /// Extensions for to add service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryDnsServiceCollectionExtensions { /// /// Adds DNS SRV service discovery to the . @@ -28,7 +28,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; @@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index e35be5d629b..22d7e6d8327 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -54,32 +54,32 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); + var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; - foreach (var endPoint in endPoints) + foreach (var endPoint in result.EndPoints) { var addressString = endPoint.GetEndPointString(); - Uri result; + Uri uri; if (!addressString.Contains("://")) { - result = new Uri($"https://{addressString}"); + uri = new Uri($"https://{addressString}"); } else { - result = new Uri(addressString); + uri = new Uri(addressString); } - uriBuilder.Host = result.Host; - uriBuilder.Port = result.Port; + uriBuilder.Host = uri.Host; + uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); var healthAddress = originalConfig.Health; if (healthUriBuilder is not null) { - healthUriBuilder.Host = result.Host; - healthUriBuilder.Port = result.Port; + healthUriBuilder.Host = uri.Host; + healthUriBuilder.Port = uri.Port; healthAddress = healthUriBuilder.Uri.ToString(); } @@ -88,6 +88,6 @@ public async ValueTask ResolveDestinationsAsync(I results.Add((name, config)); } - return (results, endPoints.ChangeToken); + return (results, result.ChangeToken); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index d37e4f1407d..84aafe2a67e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -1,22 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; using Yarp.ReverseProxy.Forwarder; namespace Microsoft.Extensions.ServiceDiscovery.Yarp; -internal sealed class ServiceDiscoveryForwarderHttpClientFactory( - TimeProvider timeProvider, - IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory, - IOptions options) : ForwarderHttpClientFactory +internal sealed class ServiceDiscoveryForwarderHttpClientFactory(IServiceDiscoveryHttpMessageHandlerFactory handlerFactory) + : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, options, handler); + return handlerFactory.CreateHandler(handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index e52ff65ee2a..9f473fd3a9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.ServiceDiscovery.Yarp; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.ServiceDiscovery; @@ -11,7 +10,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for used to register the ReverseProxy's components. /// -public static class ReverseProxyServiceCollectionExtensions +public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions { /// /// Provides a implementation which uses service discovery to resolve destinations. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index 5916951c69d..fdb61ef59f4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed partial class ConfigurationServiceEndPointResolver { @@ -15,38 +13,16 @@ private sealed partial class Log [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] - public static partial void MatchingEndPointNames(ILogger logger, string serviceName); - - [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")] - public static partial void IgnoringEndPointNames(ILogger logger, string serviceName); - - public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames) - { - if (!logger.IsEnabled(LogLevel.Debug)) - { - return; - } - - if (matchEndPointNames) - { - MatchingEndPointNames(logger, serviceName); - } - else - { - IgnoringEndPointNames(logger, serviceName); - } - } - - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + [LoggerMessage(2, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName); - [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] + [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); - [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] + [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); - public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues) + + internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -54,21 +30,21 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin } StringBuilder endpointValues = new(); - for (var i = 0; i < parsedValues.Count; i++) + for (var i = endpoints.Count - added; i < endpoints.Count; i++) { if (endpointValues.Length > 0) { endpointValues.Append(", "); } - endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + endpointValues.Append(endpoints[i].ToString()); } var configuredEndPoints = endpointValues.ToString(); ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); } - [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] + [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index dae054c9883..9604ec0c201 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -6,9 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// /// A service endpoint resolver that uses configuration to resolve resolved. @@ -26,29 +25,21 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// Initializes a new instance. /// - /// The service name. + /// The query. /// The configuration. /// The logger. - /// The options. - /// The service name parser. + /// Configuration resolver options. + /// Service discovery options. public ConfigurationServiceEndPointResolver( - string serviceName, + ServiceEndPointQuery query, IConfiguration configuration, ILogger logger, IOptions options, - ServiceNameParser parser) + IOptions serviceDiscoveryOptions) { - if (parser.TryParse(serviceName, out var parts)) - { - _serviceName = parts.Host; - _endpointName = parts.EndPointName; - _schemes = parts.Schemes; - } - else - { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); - } - + _serviceName = query.ServiceName; + _endpointName = query.EndPointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -58,24 +49,22 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); - - string IHostNameFeature.HostName => _serviceName; - - private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add resolved to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return default; } // Get the corresponding config section. var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); + return default; } endPoints.AddChangeToken(section.GetReloadToken()); @@ -119,7 +108,8 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; if (!namedSection.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + Log.EndpointConfigurationNotFound(_logger, endpointName, _serviceName, configPath); + return default; } List resolved = []; @@ -129,10 +119,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, namedSection, endpointName); } else { @@ -141,13 +128,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin { if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - if (!TryAddEndPoint(resolved, child, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, child, endpointName); } } @@ -186,25 +170,27 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (added == 0) { - return CreateNotFoundResponse(endPoints, configPath); + Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); + } + else + { + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); } - return ResolutionStatus.Success; - + return default; } - private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) + string IHostNameFeature.HostName => _serviceName; + + private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); - return false; + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } endPoints.Add(CreateEndPoint(endPoint)); - error = default; - return true; } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -246,12 +232,5 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'."); - } - public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs index c83589eb268..91e97b5d0bc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs @@ -1,25 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for . -/// -public sealed class ConfigurationServiceEndPointResolverOptions -{ - /// - /// The name of the configuration section which contains service endpoints. Defaults to "Services". - /// - public string SectionName { get; set; } = "Services"; - - /// - /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. - /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; -} +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 472205f12f9..032c50b6f27 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,23 +5,22 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILogger logger, - ServiceNameParser parser) : IServiceEndPointResolverProvider + IOptions serviceDiscoveryOptions, + ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); + resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..d3b94f2f1e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ServiceDiscovery.Configuration; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for . +/// +public sealed class ConfigurationServiceEndPointResolverOptions +{ + /// + /// The name of the configuration section which contains service endpoints. Defaults to "Services". + /// + public string SectionName { get; set; } = "Services"; + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index b4f6249f28f..44e58b0dbbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -3,21 +3,21 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory; - private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; + private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -148,8 +148,8 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateResolver(serviceName); - var selector = _selectorProvider.CreateSelector(); + var resolver = _resolverFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); var result = new ResolverEntry(resolver, selector); resolver.Start(); return result; @@ -173,7 +173,7 @@ public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector s { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPoints); + _selector.SetEndPoints(result.EndPointSource); } }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0febfa94815 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// Factory which creates instances which resolve endpoints using service discovery +/// before delegating to a provided handler. +/// +public interface IServiceDiscoveryHttpMessageHandlerFactory +{ + /// + /// Creates an instance which resolve endpoints using service discovery before + /// delegating to a provided handler. + /// + /// The handler to delegate to. + /// The new . + HttpMessageHandler CreateHandler(HttpMessageHandler handler); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 39eb65cc182..bc06a031700 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -1,16 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; @@ -19,29 +17,20 @@ public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IO protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); - if (originalUri?.Host is not null) - { - var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); - request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); - } try { + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.Headers.Host ??= result.Features.Get()?.HostName; + } + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 976bfb331ed..daa7b8a17de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// HTTP message handler which resolves endpoints using service discovery. /// -public class ResolvingHttpDelegatingHandler : DelegatingHandler +internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; private readonly ServiceDiscoveryOptions _options; @@ -43,29 +41,19 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); } try { return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } @@ -124,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, if (uri.Scheme.IndexOf('+') > 0) { var scheme = uri.Scheme.Split('+')[0]; - if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0d3ba00122f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( + TimeProvider timeProvider, + IServiceProvider serviceProvider, + ServiceEndPointWatcherFactory factory, + IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory +{ + public HttpMessageHandler CreateHandler(HttpMessageHandler handler) + { + var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry, options, handler); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..07bffa5654b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +/// +/// Represents the result of service endpoint resolution. +/// +/// The endpoint collection. +/// The exception which occurred during resolution. +internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +{ + /// + /// Gets the exception which occurred during resolution. + /// + public Exception? Exception { get; } = exception; + + /// + /// Gets a value indicating whether resolution completed successfully. + /// + [MemberNotNullWhen(true, nameof(EndPointSource))] + public bool ResolvedSuccessfully => Exception is null; + + /// + /// Gets the endpoints. + /// + public ServiceEndPointSource? EndPointSource { get; } = endPointSource; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs deleted file mode 100644 index de047481872..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs +++ /dev/null @@ -1,78 +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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal sealed class ServiceNameParser(IOptions options) -{ - private readonly string[] _allowedSchemes = options.Value.AllowedSchemes; - - public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) - { - if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) - { - parts = Create(uri, hasScheme: false); - return true; - } - - if (Uri.TryCreate(serviceName, default, out uri)) - { - parts = Create(uri, hasScheme: true); - return true; - } - - parts = default; - return false; - - ServiceNameParts Create(Uri uri, bool hasScheme) - { - var uriHost = uri.Host; - var segmentSeparatorIndex = uriHost.IndexOf('.'); - string host; - string? endPointName = null; - var port = uri.Port > 0 ? uri.Port : 0; - if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') - { - endPointName = uriHost[1..segmentSeparatorIndex]; - - // Skip the endpoint name, including its prefix ('_') and suffix ('.'). - host = uriHost[(segmentSeparatorIndex + 1)..]; - } - else - { - host = uriHost; - } - - // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". - var schemes = hasScheme ? ParseSchemes(uri.Scheme) : []; - return new(schemes, host, endPointName, port); - } - } - - private string[] ParseSchemes(string scheme) - { - if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes)) - { - return scheme.Split('+'); - } - - List result = []; - foreach (var s in scheme.Split('+')) - { - foreach (var allowed in _allowedSchemes) - { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) - { - result.Add(s); - break; - } - } - } - - return result.ToArray(); - } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs deleted file mode 100644 index f93729a40ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal readonly struct ServiceNameParts : IEquatable -{ - public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() - { - Schemes = schemePriority; - Host = host; - EndPointName = endPointName; - Port = port; - } - - public string? EndPointName { get; init; } - - public string[] Schemes { get; init; } - - public string Host { get; init; } - - public int Port { get; init; } - - public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; - - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); - - public bool Equals(ServiceNameParts other) => - EndPointName == other.EndPointName && - Host == other.Host && - Port == other.Port; - - public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right); - - public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right); -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs index e2ffbd0421f..bd0172c45cf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs @@ -1,21 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -public interface IServiceEndPointSelector +internal interface IServiceEndPointSelector { /// /// Sets the collection of endpoints which this instance will select from. /// /// The collection of endpoints to select from. - void SetEndPoints(ServiceEndPointCollection endPoints); + void SetEndPoints(ServiceEndPointSource endPoints); /// - /// Selects an endpoints from the collection provided by the most recent call to . + /// Selects an endpoints from the collection provided by the most recent call to . /// /// The context. /// An endpoint. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs deleted file mode 100644 index 9395896e520..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which always returns the first endpoint in a collection. -/// -public class PickFirstServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } endPoints) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return endPoints[0]; - } - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs deleted file mode 100644 index d3f657c9550..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PickFirstServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PickFirstServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PickFirstServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs deleted file mode 100644 index e233dfb7b5c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.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. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Selects endpoints using the Power of Two Choices algorithm for distributed load balancing based on -/// the last-known load of the candidate endpoints. -/// -public class PowerOfTwoChoicesServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - if (collection.Count == 1) - { - return collection[0]; - } - - var first = collection[Random.Shared.Next(collection.Count)]; - ServiceEndPoint second; - do - { - second = collection[Random.Shared.Next(collection.Count)]; - } while (ReferenceEquals(first, second)); - - // Note that this relies on fresh data to be effective. - if (first.Features.Get() is { } firstLoad - && second.Features.Get() is { } secondLoad) - { - return firstLoad.CurrentLoad < secondLoad.CurrentLoad ? first : second; - } - - // Degrade to random. - return first; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs deleted file mode 100644 index 00832bc7811..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PowerOfTwoChoicesServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PowerOfTwoChoicesServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PowerOfTwoChoicesServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs deleted file mode 100644 index 8e4bb2378d8..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which returns random endpoints from the collection. -/// -public class RandomServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return collection[Random.Shared.Next(collection.Count)]; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs deleted file mode 100644 index ae74b4032bc..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RandomServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RandomServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs index 5848c7d8f72..92da7cf25bf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs @@ -1,20 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector { private uint _next; - private ServiceEndPointCollection? _endPoints; + private IReadOnlyList? _endPoints; /// - public void SetEndPoints(ServiceEndPointCollection endPoints) + public void SetEndPoints(ServiceEndPointSource endPoints) { - _endPoints = endPoints; + _endPoints = endPoints.EndPoints; } /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs deleted file mode 100644 index 40d9ce7845c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6836e58cf6b..9a5d67db04e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index ab0ea286b68..483c08702df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -12,18 +11,17 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count != 0) + if (endPoints.EndPoints.Count == 0) { - return new(ResolutionStatus.None); + Log.UsingPassThrough(logger, serviceName); + var ep = ServiceEndPoint.Create(endPoint); + ep.Features.Set(this); + endPoints.EndPoints.Add(ep); } - Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); - return new(ResolutionStatus.Success); + return default; } public ValueTask DisposeAsync() => default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index b3a326010bb..83455e0979c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -4,18 +4,18 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { + var serviceName = query.OriginalString; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index c1c833de89f..bcfa59056cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; namespace Microsoft.Extensions.DependencyInjection; @@ -14,49 +12,30 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for configuring with service discovery. /// -public static class HttpClientBuilderExtensions +public static class ServiceDiscoveryHttpClientBuilderExtensions { /// /// Adds service discovery to the . /// /// The builder. - /// The provider that creates selector instances. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder, IServiceEndPointSelectorProvider selectorProvider) - { - var services = httpClientBuilder.Services; - services.AddServiceDiscoveryCore(); - httpClientBuilder.AddHttpMessageHandler(services => - { - var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - var options = services.GetRequiredService>(); - return new ResolvingHttpDelegatingHandler(registry, options); - }); - - // Configure the HttpClient to disable gRPC load balancing. - // This is done on all HttpClient instances but only impacts gRPC clients. - AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); - - return httpClientBuilder; - } + [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")] + public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery(); /// /// Adds service discovery to the . /// /// The builder. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) + public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { var services = httpClientBuilder.Services; services.AddServiceDiscoveryCore(); httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); + var resolverProvider = services.GetRequiredService(); + var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index d9510a3cf22..89c5a2d2eb0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -1,29 +1,63 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Primitives; + namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for configuring service discovery. +/// Options for service endpoint resolvers. /// public sealed class ServiceDiscoveryOptions { /// - /// The value for which indicates that all schemes are allowed. + /// The value indicating that all endpoint schemes are allowed. /// #pragma warning disable IDE0300 // Simplify collection initialization #pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllSchemes = new string[0]; + public static readonly string[] AllowAllSchemes = new string[0]; #pragma warning restore CA1825 // Avoid zero-length array allocations #pragma warning restore IDE0300 // Simplify collection initialization + /// + /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// + public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); + /// /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". /// /// - /// When set to , all schemes are allowed. + /// When set to , all schemes are allowed. /// Schemes are not case-sensitive. /// - public string[] AllowedSchemes { get; set; } = AllSchemes; -} + public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + { + if (allowedSchemes.Equals(AllowAllSchemes)) + { + if (schemes is string[] array) + { + return array; + } + + return schemes.ToArray(); + } + + List result = []; + foreach (var s in schemes) + { + foreach (var allowed in allowedSchemes) + { + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } + } + } + + return result.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs similarity index 57% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a4ba9b63b31..6403b214631 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -2,20 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Http; using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; using Microsoft.Extensions.ServiceDiscovery.PassThrough; -namespace Microsoft.Extensions.Hosting; +namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for configuring service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryServiceCollectionExtensions { /// /// Adds the core service discovery services and configures defaults. @@ -29,21 +30,47 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser .AddPassThroughServiceEndPointResolver(); } + /// + /// Adds the core service discovery services and configures defaults. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + { + return services.AddServiceDiscoveryCore(configureOptions: configureOptions) + .AddConfigurationServiceEndPointResolver() + .AddPassThroughServiceEndPointResolver(); + } + /// /// Adds the core service discovery services. /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + + /// + /// Adds the core service discovery services. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) { services.AddOptions(); services.AddLogging(); - services.TryAddSingleton(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); - services.TryAddSingleton(static sp => TimeProvider.System); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(_ => TimeProvider.System); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + if (configureOptions is not null) + { + services.Configure(configureOptions); + } + return services; } @@ -63,10 +90,10 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { @@ -84,7 +111,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs new file mode 100644 index 00000000000..1a14cb961b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// A mutable collection of service endpoints. +/// +internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +{ + private readonly List _endPoints = new(); + private readonly List _changeTokens = new(); + private readonly FeatureCollection _features = new FeatureCollection(); + + /// + /// Adds a change token. + /// + /// The change token. + public void AddChangeToken(IChangeToken changeToken) + { + _changeTokens.Add(changeToken); + } + + /// + /// Gets the feature collection. + /// + public IFeatureCollection Features => _features; + + /// + /// Gets the endpoints. + /// + public IList EndPoints => _endPoints; + + /// + /// Creates a from the provided instance. + /// + /// The service endpoint source. + public ServiceEndPointSource Build() + { + return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 9de4a61b41b..029d2601243 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -16,7 +15,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider; + private readonly ServiceEndPointWatcherFactory _resolverProvider; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -28,7 +27,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable /// /// The resolver factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) { _resolverProvider = resolverProvider; _timeProvider = timeProvider; @@ -40,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider /// The service name. /// A . /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(serviceName); ObjectDisposedException.ThrowIf(_disposed, this); @@ -157,7 +156,7 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateResolver(serviceName); + var resolver = _resolverProvider.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } @@ -182,7 +181,7 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) { try { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs deleted file mode 100644 index 415a2192c30..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs +++ /dev/null @@ -1,22 +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 Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for service endpoint resolvers. -/// -public sealed class ServiceEndPointResolverOptions -{ - /// - /// Gets or sets the period between polling resolvers which are in a pending state and do not support refresh notifications via . - /// - public TimeSpan PendingStatusRefreshPeriod { get; set; } = TimeSpan.FromSeconds(15); - - /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . - /// - public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index 17864811062..78a8f84b556 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -19,11 +18,11 @@ private sealed partial class Log [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); - public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); } static string GetEndPointString(ServiceEndPoint ep) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 8936d3722b5..9b1069d31e7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -4,34 +4,32 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -public sealed partial class ServiceEndPointWatcher( +internal sealed partial class ServiceEndPointWatcher( IServiceEndPointProvider[] resolvers, ILogger logger, string serviceName, TimeProvider timeProvider, - IOptions options) : IAsyncDisposable + IOptions options) : IAsyncDisposable { private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); private readonly object _lock = new(); private readonly ILogger _logger = logger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly ServiceEndPointResolverOptions _options = options.Value; + private readonly ServiceDiscoveryOptions _options = options.Value; private readonly IServiceEndPointProvider[] _resolvers = resolvers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointCollection? _cachedEndPoints; + private ServiceEndPointSource? _cachedEndPoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -59,23 +57,23 @@ public void Start() /// /// A . /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoResolvers(); // If the cache is valid, return the cached value. if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) { - return new ValueTask(cached); + return new ValueTask(cached); } // Otherwise, ensure the cache is being refreshed // Wait for the cache refresh to complete and return the cached value. return GetEndPointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) { - ServiceEndPointCollection? result; + ServiceEndPointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); @@ -126,79 +124,48 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointCollection? newEndPoints = null; + ServiceEndPointSource? newEndPoints = null; CacheStatus newCacheState; - ResolutionStatus status = ResolutionStatus.Success; - while (true) + try { - try + Log.ResolvingEndPoints(_logger, ServiceName); + var builder = new ServiceEndPointBuilder(); + foreach (var resolver in _resolvers) { - var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); - status = ResolutionStatus.Success; - Log.ResolvingEndPoints(_logger, ServiceName); - foreach (var resolver in _resolvers) - { - var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); - status = CombineStatus(status, resolverStatus); - } + await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + } - var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; - if (statusCode != ResolutionStatusCode.Success) + var endPoints = builder.Build(); + newCacheState = CacheStatus.Valid; + + lock (_lock) + { + // Check if we need to poll for updates or if we can register for change notification callbacks. + if (endPoints.ChangeToken.ActiveChangeCallbacks) { - if (statusCode is ResolutionStatusCode.Pending) - { - // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. - Log.ResolutionPending(_logger, ServiceName); - await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); - continue; - } - else if (statusCode is ResolutionStatusCode.Cancelled) + // Initiate a background refresh, if necessary. + endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) { - newCacheState = CacheStatus.Invalid; - error = status.Exception ?? new OperationCanceledException(); - break; - } - else if (statusCode is ResolutionStatusCode.Error) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception; - break; + _pollingTimer = null; + timer.Dispose(); } } - - lock (_lock) + else { - // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) - { - // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } - } - else - { - SchedulePollingTimer(); - } - - // The cache is valid - newEndPoints = endPoints; - newCacheState = CacheStatus.Valid; - break; + SchedulePollingTimer(); } + + // The cache is valid + newEndPoints = endPoints; + newCacheState = CacheStatus.Valid; } - catch (Exception exception) - { - error = exception; - newCacheState = CacheStatus.Invalid; - SchedulePollingTimer(); - status = CombineStatus(status, ResolutionStatus.FromException(exception)); - break; - } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); } // If there was an error, the cache must be invalid. @@ -215,7 +182,7 @@ private async Task RefreshAsyncInternal() if (OnEndPointsUpdated is { } callback) { - callback(new(newEndPoints, status)); + callback(new(newEndPoints, error)); } lock (_lock) @@ -255,48 +222,6 @@ private void SchedulePollingTimer() } } - private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) - { - if (existing.StatusCode > newStatus.StatusCode) - { - return existing; - } - - var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); - Exception? exception; - if (existing.Exception is not null && newStatus.Exception is not null) - { - List exceptions = new(); - AddExceptions(existing.Exception, exceptions); - AddExceptions(newStatus.Exception, exceptions); - exception = new AggregateException(exceptions); - } - else - { - exception = existing.Exception ?? newStatus.Exception; - } - - var message = code switch - { - ResolutionStatusCode.Error => exception!.Message ?? "Error", - _ => code.ToString(), - }; - - return new ResolutionStatus(code, exception, message); - - static void AddExceptions(Exception? exception, List exceptions) - { - if (exception is AggregateException ae) - { - exceptions.AddRange(ae.InnerExceptions); - } - else if (exception is not null) - { - exceptions.Add(exception); - } - } - } - /// public async ValueTask DisposeAsync() { @@ -328,48 +253,6 @@ private enum CacheStatus Valid } - private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) - { - if (changeToken.HasChanged) - { - return; - } - - TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - IDisposable? changeTokenRegistration = null; - IDisposable? cancellationRegistration = null; - IDisposable? pollPeriodRegistration = null; - CancellationTokenSource? timerCancellation = null; - - try - { - // Either wait for a callback or poll externally. - if (changeToken.ActiveChangeCallbacks) - { - changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - else - { - timerCancellation = new(pollPeriod); - pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - if (cancellationToken.CanBeCanceled) - { - cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - await completion.Task.ConfigureAwait(false); - } - finally - { - changeTokenRegistration?.Dispose(); - cancellationRegistration?.Dispose(); - pollPeriodRegistration?.Dispose(); - timerCancellation?.Dispose(); - } - } - private void ThrowIfNoResolvers() { if (_resolvers.Length == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs similarity index 90% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs index d7835f26d08..69f565eb8e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointResolverFactory +partial class ServiceEndPointWatcherFactory { private sealed partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs index c545c82e9e6..90f62ab0597 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs @@ -3,38 +3,42 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Creates service endpoint resolvers. +/// Creates service endpoint watchers. /// -public partial class ServiceEndPointResolverFactory( - IEnumerable resolvers, +internal sealed partial class ServiceEndPointWatcherFactory( + IEnumerable resolvers, ILogger resolverLogger, - IOptions options, + IOptions options, TimeProvider timeProvider) { - private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers + private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly IOptions _options = options; + private readonly IOptions _options = options; /// /// Creates a service endpoint resolver for the provided service name. /// - public ServiceEndPointWatcher CreateResolver(string serviceName) + public ServiceEndPointWatcher CreateWatcher(string serviceName) { ArgumentNullException.ThrowIfNull(serviceName); + if (!ServiceEndPointQuery.TryParse(serviceName, out var query)) + { + throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); + } + List? resolvers = null; foreach (var factory in _resolverProviders) { - if (factory.TryCreateResolver(serviceName, out var resolver)) + if (factory.TryCreateProvider(query, out var resolver)) { resolvers ??= []; resolvers.Add(resolver); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs index 6d3132da880..6b5b07d199e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// An endpoint represented by a . /// /// The . -public sealed class UriEndPoint(Uri uri) : EndPoint +internal sealed class UriEndPoint(Uri uri) : EndPoint { /// /// Gets the associated with this endpoint. diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7e2ef478ef7..25cd88a1436 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class DnsSrvServiceEndPointResolverTests { @@ -103,9 +103,9 @@ public async Task ResolveServiceEndPoint_Dns() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -114,14 +114,13 @@ public async Task ResolveServiceEndPoint_Dns() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -190,9 +189,9 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -200,20 +199,19 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo resolver.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); - Assert.Null(initialResult.Status.Exception); + Assert.Null(initialResult.Exception); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -223,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo else { // We expect only the results from the Configuration provider. - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -271,9 +269,9 @@ public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() .AddServiceDiscovery() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var channel = Channel.CreateUnbounded(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index f35ffa2026c..6d8091f026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -5,15 +5,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// public class ConfigurationServiceEndPointResolverTests { @@ -29,9 +29,9 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -40,11 +40,10 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -67,12 +66,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() .AddConfigurationServiceEndPointResolver() .Configure(o => o.AllowedSchemes = ["https"]) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -81,13 +80,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Empty(initialResult.EndPoints); + Assert.Empty(initialResult.EndPointSource.EndPoints); } // Specifying either https or http. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -96,14 +94,13 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } // Specifying either https or http, but in reverse. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -112,8 +109,7 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } @@ -135,9 +131,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -146,12 +142,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -160,7 +155,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. - await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -169,12 +164,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -204,9 +198,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -215,13 +209,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -250,9 +243,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -261,17 +254,16 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); // We expect the HTTPS endpoint back but not the HTTP one. - Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index d8adcbca529..643bbfad441 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -5,8 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; using Xunit; @@ -14,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for . -/// These also cover and by extension. +/// These also cover and by extension. /// public class PassThroughServiceEndPointResolverTests { @@ -25,9 +24,9 @@ public async Task ResolveServiceEndPoint_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -36,8 +35,7 @@ public async Task ResolveServiceEndPoint_PassThrough() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } @@ -57,9 +55,9 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -68,11 +66,10 @@ public async Task ResolveServiceEndPoint_Superseded() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -91,9 +88,9 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -102,11 +99,10 @@ public async Task ResolveServiceEndPoint_Fallback() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -128,7 +124,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .BuildServiceProvider(); var resolver = services.GetRequiredService(); - var endPoints = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); + var result = await resolver.GetEndPointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 0628bedbe73..f5e506a9b72 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// public class ServiceEndPointResolverTests { @@ -25,8 +25,8 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + var resolverFactory = services.GetRequiredService(); + var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket")); Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } @@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); var exception = Assert.Throws(resolverFactory.Start); Assert.Equal("No service endpoint resolvers are configured.", exception.Message); exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); @@ -49,35 +49,34 @@ public void ResolveServiceEndPoint_NullServiceName_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - Assert.Throws(() => resolverFactory.CreateResolver(null!)); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task UseServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoResolvers_Throws() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")) - .UseServiceDiscovery(); + serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery(); var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory { - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { bool result; - (result, resolver) = createResolverDelegate(serviceName); + (result, resolver) = createResolverDelegate(query); return result; } } - private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } @@ -101,18 +100,18 @@ public async Task ResolveServiceEndPoint() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -124,10 +123,9 @@ public async Task ResolveServiceEndPoint() cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); - Assert.Equal(ResolutionStatus.Success, resolverResult.Status); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPoints.Count); - var endpoints = resolverResult.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); + var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); endpoints.Sort((l, r) => l.Port - r.Port); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); @@ -154,15 +152,15 @@ public async Task ResolveServiceEndPointOneShot() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -190,12 +188,11 @@ public async Task ResolveHttpServiceEndPointOneShot() disposeAsync: () => default); var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System); Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); @@ -232,25 +229,24 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); - return ResolutionStatus.Success; }, disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); var initialEndPoints = await initialEndPointsTask; Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints); + Assert.Single(initialEndPoints.EndPoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -283,9 +279,9 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var task = resolver.GetEndPointsAsync(CancellationToken.None); sem.Release(1); - var endPoints = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, endPoints); - var sep = Assert.Single(endPoints); + var result = await task.ConfigureAwait(false); + Assert.NotSame(initialEndPoints, result); + var sep = Assert.Single(result.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); From 7e294b992a3a4526319c8b7b228f960ae09122b5 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:24:30 -0700 Subject: [PATCH 035/472] Service Discovery API refactoring (#3114) (#3196) * API review feedback & general cleanup including removal of currently unused features * Align namespaces * Hide more of the API, rename for consistency * Hide more, rename more * ResolutionStatus does not need to be equatable * Make ServiceEndPointQuery public to break InternalsVisibleTo with Dns provider * Break InternalsVisibleTo from ServiceDiscovery package to YARP by adding a middleware factory * Remove ResolutionStatus, simplifying Service Discovery interfaces * Clean up ServiceEndPointImpl * Mark ServiceEndPointResolverResult as internal * Remove unnecessary members from ServiceEndPointCollection/Source * Seal service discovery types * Remove IServiceEndPointSelectorFactory and use DI instead * Remove unused endpoint selectors * Remove unused PendingStatusRefreshPeriod option * Rename UseServiceDiscovery to AddServiceDiscovery * Remove possible ambiguity in AddConfigurationServiceEndPointResolver signature * Add configuration delegate overloads to AddServiceDiscovery methods * Clean up logging in configuration-based service endpoint provider * API review: rename ServiceEndPointCollectionSource to IServiceEndPointBuilder * Rename IServiceDiscoveryDelegatingHttpMessageHandlerFactory * Rename IServiceEndPointProvider.ResolveAsync to PopulateAsync * Hide IServiceEndPointSelector * Remove allowedSchemes from ServiceEndPointQuery.TryParse * Rename ServiceEndPointQuery.Host to .ServiceName * Fix build * Review feedback * nit param rename * Improve ServiceEndPointQuery.ToString output * fixup (cherry picked from commit 38756b14164276c1a88239a8a932a68ce3956ae7) --- .../Features/IEndPointHealthFeature.cs | 18 -- .../Features/IEndPointLoadFeature.cs | 16 -- .../{Features => }/IHostNameFeature.cs | 4 +- .../IServiceEndPointBuilder.cs | 29 +++ .../IServiceEndPointProvider.cs | 4 +- ....cs => IServiceEndPointProviderFactory.cs} | 10 +- .../IServiceEndPointSelectorProvider.cs | 16 -- .../Internal/ServiceEndPointImpl.cs | 18 +- .../ResolutionStatus.cs | 101 --------- .../ResolutionStatusCode.cs | 40 ---- .../ServiceEndPoint.cs | 3 +- .../ServiceEndPointCollectionSource.cs | 57 ----- .../ServiceEndPointQuery.cs | 97 +++++++++ .../ServiceEndPointResolverResult.cs | 30 --- ...Collection.cs => ServiceEndPointSource.cs} | 36 +--- .../DnsServiceEndPointResolver.cs | 4 +- .../DnsServiceEndPointResolverBase.cs | 68 ++---- .../DnsServiceEndPointResolverOptions.cs | 2 - .../DnsServiceEndPointResolverProvider.cs | 16 +- .../DnsSrvServiceEndPointResolver.cs | 4 +- .../DnsSrvServiceEndPointResolverOptions.cs | 2 - .../DnsSrvServiceEndPointResolverProvider.cs | 21 +- ...iscoveryDnsServiceCollectionExtensions.cs} | 8 +- .../ServiceDiscoveryDestinationResolver.cs | 22 +- ...viceDiscoveryForwarderHttpClientFactory.cs | 12 +- ...everseProxyServiceCollectionExtensions.cs} | 3 +- ...onfigurationServiceEndPointResolver.Log.cs | 42 +--- .../ConfigurationServiceEndPointResolver.cs | 79 +++---- ...erviceEndPointResolverOptionsValidator.cs} | 20 +- ...gurationServiceEndPointResolverProvider.cs | 13 +- ...igurationServiceEndPointResolverOptions.cs | 22 ++ .../Http/HttpServiceEndPointResolver.cs | 14 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Http/ResolvingHttpClientHandler.cs | 27 +-- .../Http/ResolvingHttpDelegatingHandler.cs | 16 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Internal/ServiceEndPointResolverResult.cs | 30 +++ .../Internal/ServiceNameParser.cs | 78 ------- .../Internal/ServiceNameParts.cs | 39 ---- .../IServiceEndPointSelector.cs | 8 +- .../PickFirstServiceEndPointSelector.cs | 29 --- ...ickFirstServiceEndPointSelectorProvider.cs | 18 -- ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 ----- ...oChoicesServiceEndPointSelectorProvider.cs | 18 -- .../RandomServiceEndPointSelector.cs | 29 --- .../RandomServiceEndPointSelectorProvider.cs | 18 -- .../RoundRobinServiceEndPointSelector.cs | 10 +- ...undRobinServiceEndPointSelectorProvider.cs | 18 -- ...crosoft.Extensions.ServiceDiscovery.csproj | 3 +- .../PassThroughServiceEndPointResolver.cs | 16 +- ...sThroughServiceEndPointResolverProvider.cs | 6 +- ...ceDiscoveryHttpClientBuilderExtensions.cs} | 33 +-- .../ServiceDiscoveryOptions.cs | 46 +++- ...ceDiscoveryServiceCollectionExtensions.cs} | 53 +++-- .../ServiceEndPointBuilder.cs | 46 ++++ .../ServiceEndPointResolver.cs | 11 +- .../ServiceEndPointResolverOptions.cs | 22 -- .../ServiceEndPointWatcher.Log.cs | 5 +- .../ServiceEndPointWatcher.cs | 199 ++++-------------- ...s => ServiceEndPointWatcherFactory.Log.cs} | 3 +- ...ry.cs => ServiceEndPointWatcherFactory.cs} | 22 +- .../UriEndPoint.cs | 4 +- .../DnsSrvServiceEndPointResolverTests.cs | 40 ++-- ...nfigurationServiceEndPointResolverTests.cs | 88 ++++---- ...PassThroughServiceEndPointResolverTests.cs | 34 ++- .../ServiceEndPointResolverTests.cs | 76 ++++--- 66 files changed, 680 insertions(+), 1284 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{Features => }/IHostNameFeature.cs (70%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolverProvider.cs => IServiceEndPointProviderFactory.cs} (59%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointCollection.cs => ServiceEndPointSource.cs} (55%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{HostingExtensions.cs => ServiceDiscoveryDnsServiceCollectionExtensions.cs} (88%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/{ReverseProxyServiceCollectionExtensions.cs => ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs} (94%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndPointResolverOptionsValidator.cs} (50%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery/LoadBalancing}/IServiceEndPointSelector.cs (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Http/HttpClientBuilderExtensions.cs => ServiceDiscoveryHttpClientBuilderExtensions.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{HostingExtensions.cs => ServiceDiscoveryServiceCollectionExtensions.cs} (57%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.Log.cs => ServiceEndPointWatcherFactory.Log.cs} (90%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.cs => ServiceEndPointWatcherFactory.cs} (69%) rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery}/UriEndPoint.cs (86%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs deleted file mode 100644 index 63dc3e11a3a..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing. -/// -public interface IEndPointHealthFeature -{ - /// - /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op. - /// - /// The response time of the endpoint. - /// An optional exception that occurred while checking the endpoint's health. - void ReportHealth(TimeSpan responseTime, Exception? exception); -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs deleted file mode 100644 index 2610f135945..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that provides information about the current load of an endpoint. -/// -public interface IEndPointLoadFeature -{ - /// - /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). - /// - public double CurrentLoad { get; } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs index fff3c3fa3f8..c7489472374 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Exposes the host name of the end point. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs new file mode 100644 index 00000000000..468adea1c09 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Builder to create a instances. +/// +public interface IServiceEndPointBuilder +{ + /// + /// Gets the endpoints. + /// + IList EndPoints { get; } + + /// + /// Gets the feature collection. + /// + IFeatureCollection Features { get; } + + /// + /// Adds a change token to the resulting . + /// + /// The change token. + void AddChangeToken(IChangeToken changeToken); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index 3b369a97850..950823257af 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. @@ -14,5 +14,5 @@ public interface IServiceEndPointProvider : IAsyncDisposable /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs similarity index 59% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs index 51343a53697..4b1876f808e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs @@ -3,18 +3,18 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public interface IServiceEndPointResolverProvider +public interface IServiceEndPointProviderFactory { /// - /// Tries to create an instance for the specified . + /// Tries to create an instance for the specified . /// - /// The service to create the resolver for. + /// The service to create the resolver for. /// The resolver. /// if the resolver was created, otherwise. - bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); + bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs deleted file mode 100644 index 27f4ec4324a..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Functionality for creating instances. -/// -public interface IServiceEndPointSelectorProvider -{ - /// - /// Creates an instance. - /// - /// A new instance. - IServiceEndPointSelector CreateSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index b73635ecd57..7d135dfe97d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -4,21 +4,11 @@ using System.Net; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl : ServiceEndPoint +internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint { - private readonly IFeatureCollection _features; - private readonly EndPoint _endPoint; - - public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) - { - _endPoint = endPoint; - _features = features ?? new FeatureCollection(); - } - - public override EndPoint EndPoint => _endPoint; - public override IFeatureCollection Features => _features; - + public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs deleted file mode 100644 index 04eec95dc63..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the status of an endpoint resolution operation. -/// -public readonly struct ResolutionStatus(ResolutionStatusCode statusCode, Exception? exception, string message) : IEquatable -{ - /// - /// Indicates that resolution was not performed. - /// - public static readonly ResolutionStatus None = new(ResolutionStatusCode.None, exception: null, message: ""); - - /// - /// Indicates that resolution is ongoing and has not yet completed. - /// - public static readonly ResolutionStatus Pending = new(ResolutionStatusCode.Pending, exception: null, message: "Pending"); - - /// - /// Indicates that resolution has completed successfully. - /// - public static readonly ResolutionStatus Success = new(ResolutionStatusCode.Success, exception: null, message: "Success"); - - /// - /// Indicates that resolution was cancelled. - /// - public static readonly ResolutionStatus Cancelled = new(ResolutionStatusCode.Cancelled, exception: null, message: "Cancelled"); - - /// - /// Indicates that resolution did not find a result for the service. - /// - public static ResolutionStatus CreateNotFound(string message) => new(ResolutionStatusCode.NotFound, exception: null, message: message); - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception. - /// A new instance. - public static ResolutionStatus FromException(Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Error, exception, exception.Message); - } - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception, if there was one. - /// A new instance. - public static ResolutionStatus FromPending(Exception? exception = null) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Pending, exception, exception.Message); - } - - /// - /// Gets the resolution status code. - /// - public ResolutionStatusCode StatusCode { get; } = statusCode; - - /// - /// Gets the resolution exception. - /// - - public Exception? Exception { get; } = exception; - - /// - /// Gets the resolution status message. - /// - public string Message { get; } = message; - - /// - /// Compares the provided operands, returning if they are equal and if they are not equal. - /// - public static bool operator ==(ResolutionStatus left, ResolutionStatus right) => left.Equals(right); - - /// - /// Compares the provided operands, returning if they are not equal and if they are equal. - /// - public static bool operator !=(ResolutionStatus left, ResolutionStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is ResolutionStatus status && Equals(status); - - /// - public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && - EqualityComparer.Default.Equals(Exception, other.Exception) && - Message == other.Message; - - /// - public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message); - - /// - public override string ToString() => Exception switch - { - not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]", - _ => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}]" - }; -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs deleted file mode 100644 index 7157eac758f..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Status codes for . -/// -public enum ResolutionStatusCode -{ - /// - /// Resolution has not been performed. - /// - None = 0, - - /// - /// Resolution is pending completion. - /// - Pending = 1, - - /// - /// Resolution did not find any end points for the specified service. - /// - NotFound = 2, - - /// - /// Resolution was successful. - /// - Success = 3, - - /// - /// Resolution was canceled. - /// - Cancelled = 4, - - /// - /// Resolution failed. - /// - Error = 5, -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs index 9dc4675dade..a3cde62ce0d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs @@ -4,8 +4,9 @@ using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs deleted file mode 100644 index 94f274a38e8..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs +++ /dev/null @@ -1,57 +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 Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A mutable collection of service endpoints. -/// -public class ServiceEndPointCollectionSource(string serviceName, IFeatureCollection features) -{ - private readonly List _endPoints = new(); - private readonly List _changeTokens = new(); - - /// - /// Gets the service name. - /// - public string ServiceName { get; } = serviceName; - - /// - /// Adds a change token. - /// - /// The change token. - public void AddChangeToken(IChangeToken changeToken) - { - _changeTokens.Add(changeToken); - } - - /// - /// Gets the composite change token. - /// - /// The composite change token. - public IChangeToken GetChangeToken() => new CompositeChangeToken(_changeTokens); - - /// - /// Gets the feature collection. - /// - public IFeatureCollection Features { get; } = features; - - /// - /// Gets the endpoints. - /// - public IList EndPoints => _endPoints; - - /// - /// Creates a from the provided instance. - /// - /// The source collection. - /// The service endpoint collection. - public static ServiceEndPointCollection CreateServiceEndPointCollection(ServiceEndPointCollectionSource source) - { - return new ServiceEndPointCollection(source.ServiceName, source._endPoints, source.GetChangeToken(), source.Features); - } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs new file mode 100644 index 00000000000..99c92cce27c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Describes a query for endpoints of a service. +/// +public sealed class ServiceEndPointQuery +{ + /// + /// Initializes a new instance. + /// + /// The string which the query was constructed from. + /// The ordered list of included URI schemes. + /// The service name. + /// The optional endpoint name. + private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName) + { + OriginalString = originalString; + IncludeSchemes = includedSchemes; + ServiceName = serviceName; + EndPointName = endPointName; + } + + /// + /// Tries to parse the provided input as a service endpoint query. + /// + /// The value to parse. + /// The resulting query. + /// if the value was successfully parsed; otherwise . + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query) + { + bool hasScheme; + if (!input.Contains("://", StringComparison.InvariantCulture) + && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) + { + hasScheme = false; + } + else if (Uri.TryCreate(input, default, out uri)) + { + hasScheme = true; + } + else + { + query = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". + var schemes = hasScheme ? uri.Scheme.Split('+') : []; + query = new(input, schemes, host, endPointName); + return true; + } + + /// + /// Gets the string which the query was constructed from. + /// + public string OriginalString { get; } + + /// + /// Gets the ordered list of included URI schemes. + /// + public IReadOnlyList IncludeSchemes { get; } + + /// + /// Gets the endpoint name, or if no endpoint name is specified. + /// + public string? EndPointName { get; } + + /// + /// Gets the service name. + /// + public string ServiceName { get; } + + /// + public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}"; +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs deleted file mode 100644 index 9179ed2f113..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs +++ /dev/null @@ -1,30 +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.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the result of service endpoint resolution. -/// -/// The endpoint collection. -/// The status. -public sealed class ServiceEndPointResolverResult(ServiceEndPointCollection? endPoints, ResolutionStatus status) -{ - /// - /// Gets the status. - /// - public ResolutionStatus Status { get; } = status; - - /// - /// Gets a value indicating whether resolution completed successfully. - /// - [MemberNotNullWhen(true, nameof(EndPoints))] - public bool ResolvedSuccessfully => Status.StatusCode is ResolutionStatusCode.Success; - - /// - /// Gets the endpoints. - /// - public ServiceEndPointCollection? EndPoints { get; } = endPoints; -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs similarity index 55% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs index c9540f2e7a1..807981226e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs @@ -1,47 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; using System.Diagnostics; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Represents an immutable collection of service endpoints. +/// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] [DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public class ServiceEndPointCollection : IReadOnlyList +public sealed class ServiceEndPointSource { private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointCollection(string serviceName, List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { - ArgumentNullException.ThrowIfNull(serviceName); ArgumentNullException.ThrowIfNull(changeToken); _endpoints = endpoints; Features = features; - ServiceName = serviceName; ChangeToken = changeToken; } - /// - public ServiceEndPoint this[int index] => _endpoints?[index] ?? throw new ArgumentOutOfRangeException(nameof(index)); - /// - /// Gets the service name. + /// Gets the endpoints. /// - public string ServiceName { get; } + public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -53,15 +46,6 @@ public ServiceEndPointCollection(string serviceName, List? endp /// public IFeatureCollection Features { get; } - /// - public int Count => _endpoints?.Count ?? 0; - - /// - public IEnumerator GetEnumerator() => _endpoints?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - /// public override string ToString() { @@ -73,15 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value) + private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) { - public string ServiceName => value.ServiceName; - public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.ToArray(); + public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4a8350483eb..a2601c84b45 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -4,7 +4,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -45,8 +44,7 @@ protected override async Task ResolveAsyncCore() if (endPoints.Count == 0) { - SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).")); - return; + throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); } SetResult(endPoints, ttl); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 516c8ed1f69..9d6c54e4755 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -18,7 +17,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private readonly TimeProvider _timeProvider; private long _lastRefreshTimeStamp; private Task _resolveTask = Task.CompletedTask; - private ResolutionStatus _lastStatus; + private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; private List? _lastEndPointCollection; @@ -59,13 +58,13 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return; } if (ShouldRefresh()) @@ -75,7 +74,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS { if (_resolveTask.IsCompleted && ShouldRefresh()) { - _resolveTask = ResolveAsyncInternal(); + _resolveTask = ResolveAsyncCore(); } resolveTask = _resolveTask; @@ -95,7 +94,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS } endPoints.AddChangeToken(_lastChangeToken); - return _lastStatus; + return; } } @@ -103,53 +102,21 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS protected abstract Task ResolveAsyncCore(); - private async Task ResolveAsyncInternal() - { - try - { - await ResolveAsyncCore().ConfigureAwait(false); - } - catch (Exception exception) - { - SetException(exception); - throw; - } - - } - - protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero); - - protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); - - private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + protected void SetResult(List endPoints, TimeSpan validityPeriod) { lock (_lock) { - if (exception is not null) + if (endPoints is { Count: > 0 }) { - _nextRefreshPeriod = GetRefreshPeriod(); - if (_lastEndPointCollection is null) - { - // Since end points have never been resolved, use a pending status to indicate that they might appear - // soon and to retry for some period until they do. - _lastStatus = ResolutionStatus.FromPending(exception); - } - else - { - _lastStatus = ResolutionStatus.FromException(exception); - } + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + _nextRefreshPeriod = DefaultRefreshPeriod; + _hasEndpoints = true; } - else if (endPoints is not { Count: > 0 }) + else { _nextRefreshPeriod = GetRefreshPeriod(); validityPeriod = TimeSpan.Zero; - _lastStatus = ResolutionStatus.Pending; - } - else - { - _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); - _nextRefreshPeriod = DefaultRefreshPeriod; - _lastStatus = ResolutionStatus.Success; + _hasEndpoints = false; } if (validityPeriod <= TimeSpan.Zero) @@ -169,13 +136,18 @@ private void SetResult(List? endPoints, Exception? exception, T TimeSpan GetRefreshPeriod() { - if (_lastStatus.StatusCode is ResolutionStatusCode.Success) + if (_hasEndpoints) { return MinRetryPeriod; } - var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor)); - return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod; + var nextTicks = (long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor); + if (nextTicks <= 0 || nextTicks > MaxRetryPeriod.Ticks) + { + return MaxRetryPeriod; + } + + return TimeSpan.FromTicks(nextTicks); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 37879b5f3e3..665c98bbc09 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 8f676327d5f..51525663a03 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -4,28 +4,18 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider); + resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 97a0d47d028..d59dbfbb69c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -6,7 +6,6 @@ using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -39,8 +38,7 @@ protected override async Task ResolveAsyncCore() var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); if (result.HasError) { - SetException(CreateException(srvQuery, result.ErrorMessage)); - return; + throw CreateException(srvQuery, result.ErrorMessage); } var lookupMapping = new Dictionary(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 5bac96c6c0a..704e03cd9ca 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index d02dcfb7274..8a75c1d1bbf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -5,8 +5,6 @@ using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -14,8 +12,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -23,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md @@ -33,6 +30,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". + var serviceName = query.OriginalString; if (string.IsNullOrWhiteSpace(_querySuffix)) { DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); @@ -40,16 +38,9 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - var portName = parts.EndPointName ?? "default"; - var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider); + var portName = query.EndPointName ?? "default"; + var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs similarity index 88% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index e385bde69a7..0d795660fd2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; namespace Microsoft.Extensions.Hosting; @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Hosting; /// /// Extensions for to add service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryDnsServiceCollectionExtensions { /// /// Adds DNS SRV service discovery to the . @@ -28,7 +28,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; @@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index e35be5d629b..22d7e6d8327 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -54,32 +54,32 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); + var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; - foreach (var endPoint in endPoints) + foreach (var endPoint in result.EndPoints) { var addressString = endPoint.GetEndPointString(); - Uri result; + Uri uri; if (!addressString.Contains("://")) { - result = new Uri($"https://{addressString}"); + uri = new Uri($"https://{addressString}"); } else { - result = new Uri(addressString); + uri = new Uri(addressString); } - uriBuilder.Host = result.Host; - uriBuilder.Port = result.Port; + uriBuilder.Host = uri.Host; + uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); var healthAddress = originalConfig.Health; if (healthUriBuilder is not null) { - healthUriBuilder.Host = result.Host; - healthUriBuilder.Port = result.Port; + healthUriBuilder.Host = uri.Host; + healthUriBuilder.Port = uri.Port; healthAddress = healthUriBuilder.Uri.ToString(); } @@ -88,6 +88,6 @@ public async ValueTask ResolveDestinationsAsync(I results.Add((name, config)); } - return (results, endPoints.ChangeToken); + return (results, result.ChangeToken); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index d37e4f1407d..84aafe2a67e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -1,22 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; using Yarp.ReverseProxy.Forwarder; namespace Microsoft.Extensions.ServiceDiscovery.Yarp; -internal sealed class ServiceDiscoveryForwarderHttpClientFactory( - TimeProvider timeProvider, - IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory, - IOptions options) : ForwarderHttpClientFactory +internal sealed class ServiceDiscoveryForwarderHttpClientFactory(IServiceDiscoveryHttpMessageHandlerFactory handlerFactory) + : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, options, handler); + return handlerFactory.CreateHandler(handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index e52ff65ee2a..9f473fd3a9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.ServiceDiscovery.Yarp; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.ServiceDiscovery; @@ -11,7 +10,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for used to register the ReverseProxy's components. /// -public static class ReverseProxyServiceCollectionExtensions +public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions { /// /// Provides a implementation which uses service discovery to resolve destinations. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index 5916951c69d..fdb61ef59f4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed partial class ConfigurationServiceEndPointResolver { @@ -15,38 +13,16 @@ private sealed partial class Log [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] - public static partial void MatchingEndPointNames(ILogger logger, string serviceName); - - [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")] - public static partial void IgnoringEndPointNames(ILogger logger, string serviceName); - - public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames) - { - if (!logger.IsEnabled(LogLevel.Debug)) - { - return; - } - - if (matchEndPointNames) - { - MatchingEndPointNames(logger, serviceName); - } - else - { - IgnoringEndPointNames(logger, serviceName); - } - } - - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + [LoggerMessage(2, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName); - [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] + [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); - [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] + [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); - public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues) + + internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -54,21 +30,21 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin } StringBuilder endpointValues = new(); - for (var i = 0; i < parsedValues.Count; i++) + for (var i = endpoints.Count - added; i < endpoints.Count; i++) { if (endpointValues.Length > 0) { endpointValues.Append(", "); } - endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + endpointValues.Append(endpoints[i].ToString()); } var configuredEndPoints = endpointValues.ToString(); ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); } - [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] + [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index dae054c9883..9604ec0c201 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -6,9 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// /// A service endpoint resolver that uses configuration to resolve resolved. @@ -26,29 +25,21 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// Initializes a new instance. /// - /// The service name. + /// The query. /// The configuration. /// The logger. - /// The options. - /// The service name parser. + /// Configuration resolver options. + /// Service discovery options. public ConfigurationServiceEndPointResolver( - string serviceName, + ServiceEndPointQuery query, IConfiguration configuration, ILogger logger, IOptions options, - ServiceNameParser parser) + IOptions serviceDiscoveryOptions) { - if (parser.TryParse(serviceName, out var parts)) - { - _serviceName = parts.Host; - _endpointName = parts.EndPointName; - _schemes = parts.Schemes; - } - else - { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); - } - + _serviceName = query.ServiceName; + _endpointName = query.EndPointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -58,24 +49,22 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); - - string IHostNameFeature.HostName => _serviceName; - - private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add resolved to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return default; } // Get the corresponding config section. var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); + return default; } endPoints.AddChangeToken(section.GetReloadToken()); @@ -119,7 +108,8 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; if (!namedSection.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + Log.EndpointConfigurationNotFound(_logger, endpointName, _serviceName, configPath); + return default; } List resolved = []; @@ -129,10 +119,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, namedSection, endpointName); } else { @@ -141,13 +128,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin { if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - if (!TryAddEndPoint(resolved, child, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, child, endpointName); } } @@ -186,25 +170,27 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (added == 0) { - return CreateNotFoundResponse(endPoints, configPath); + Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); + } + else + { + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); } - return ResolutionStatus.Success; - + return default; } - private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) + string IHostNameFeature.HostName => _serviceName; + + private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); - return false; + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } endPoints.Add(CreateEndPoint(endPoint)); - error = default; - return true; } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -246,12 +232,5 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'."); - } - public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs index c83589eb268..91e97b5d0bc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs @@ -1,25 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for . -/// -public sealed class ConfigurationServiceEndPointResolverOptions -{ - /// - /// The name of the configuration section which contains service endpoints. Defaults to "Services". - /// - public string SectionName { get; set; } = "Services"; - - /// - /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. - /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; -} +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 472205f12f9..032c50b6f27 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,23 +5,22 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILogger logger, - ServiceNameParser parser) : IServiceEndPointResolverProvider + IOptions serviceDiscoveryOptions, + ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); + resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..d3b94f2f1e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ServiceDiscovery.Configuration; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for . +/// +public sealed class ConfigurationServiceEndPointResolverOptions +{ + /// + /// The name of the configuration section which contains service endpoints. Defaults to "Services". + /// + public string SectionName { get; set; } = "Services"; + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index b4f6249f28f..44e58b0dbbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -3,21 +3,21 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory; - private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; + private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -148,8 +148,8 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateResolver(serviceName); - var selector = _selectorProvider.CreateSelector(); + var resolver = _resolverFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); var result = new ResolverEntry(resolver, selector); resolver.Start(); return result; @@ -173,7 +173,7 @@ public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector s { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPoints); + _selector.SetEndPoints(result.EndPointSource); } }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0febfa94815 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// Factory which creates instances which resolve endpoints using service discovery +/// before delegating to a provided handler. +/// +public interface IServiceDiscoveryHttpMessageHandlerFactory +{ + /// + /// Creates an instance which resolve endpoints using service discovery before + /// delegating to a provided handler. + /// + /// The handler to delegate to. + /// The new . + HttpMessageHandler CreateHandler(HttpMessageHandler handler); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 39eb65cc182..bc06a031700 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -1,16 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; @@ -19,29 +17,20 @@ public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IO protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); - if (originalUri?.Host is not null) - { - var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); - request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); - } try { + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.Headers.Host ??= result.Features.Get()?.HostName; + } + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 976bfb331ed..daa7b8a17de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// HTTP message handler which resolves endpoints using service discovery. /// -public class ResolvingHttpDelegatingHandler : DelegatingHandler +internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; private readonly ServiceDiscoveryOptions _options; @@ -43,29 +41,19 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); } try { return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } @@ -124,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, if (uri.Scheme.IndexOf('+') > 0) { var scheme = uri.Scheme.Split('+')[0]; - if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0d3ba00122f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( + TimeProvider timeProvider, + IServiceProvider serviceProvider, + ServiceEndPointWatcherFactory factory, + IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory +{ + public HttpMessageHandler CreateHandler(HttpMessageHandler handler) + { + var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry, options, handler); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..07bffa5654b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +/// +/// Represents the result of service endpoint resolution. +/// +/// The endpoint collection. +/// The exception which occurred during resolution. +internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +{ + /// + /// Gets the exception which occurred during resolution. + /// + public Exception? Exception { get; } = exception; + + /// + /// Gets a value indicating whether resolution completed successfully. + /// + [MemberNotNullWhen(true, nameof(EndPointSource))] + public bool ResolvedSuccessfully => Exception is null; + + /// + /// Gets the endpoints. + /// + public ServiceEndPointSource? EndPointSource { get; } = endPointSource; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs deleted file mode 100644 index de047481872..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs +++ /dev/null @@ -1,78 +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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal sealed class ServiceNameParser(IOptions options) -{ - private readonly string[] _allowedSchemes = options.Value.AllowedSchemes; - - public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) - { - if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) - { - parts = Create(uri, hasScheme: false); - return true; - } - - if (Uri.TryCreate(serviceName, default, out uri)) - { - parts = Create(uri, hasScheme: true); - return true; - } - - parts = default; - return false; - - ServiceNameParts Create(Uri uri, bool hasScheme) - { - var uriHost = uri.Host; - var segmentSeparatorIndex = uriHost.IndexOf('.'); - string host; - string? endPointName = null; - var port = uri.Port > 0 ? uri.Port : 0; - if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') - { - endPointName = uriHost[1..segmentSeparatorIndex]; - - // Skip the endpoint name, including its prefix ('_') and suffix ('.'). - host = uriHost[(segmentSeparatorIndex + 1)..]; - } - else - { - host = uriHost; - } - - // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". - var schemes = hasScheme ? ParseSchemes(uri.Scheme) : []; - return new(schemes, host, endPointName, port); - } - } - - private string[] ParseSchemes(string scheme) - { - if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes)) - { - return scheme.Split('+'); - } - - List result = []; - foreach (var s in scheme.Split('+')) - { - foreach (var allowed in _allowedSchemes) - { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) - { - result.Add(s); - break; - } - } - } - - return result.ToArray(); - } -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs deleted file mode 100644 index f93729a40ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal readonly struct ServiceNameParts : IEquatable -{ - public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() - { - Schemes = schemePriority; - Host = host; - EndPointName = endPointName; - Port = port; - } - - public string? EndPointName { get; init; } - - public string[] Schemes { get; init; } - - public string Host { get; init; } - - public int Port { get; init; } - - public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; - - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); - - public bool Equals(ServiceNameParts other) => - EndPointName == other.EndPointName && - Host == other.Host && - Port == other.Port; - - public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right); - - public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right); -} - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs index e2ffbd0421f..bd0172c45cf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs @@ -1,21 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -public interface IServiceEndPointSelector +internal interface IServiceEndPointSelector { /// /// Sets the collection of endpoints which this instance will select from. /// /// The collection of endpoints to select from. - void SetEndPoints(ServiceEndPointCollection endPoints); + void SetEndPoints(ServiceEndPointSource endPoints); /// - /// Selects an endpoints from the collection provided by the most recent call to . + /// Selects an endpoints from the collection provided by the most recent call to . /// /// The context. /// An endpoint. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs deleted file mode 100644 index 9395896e520..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which always returns the first endpoint in a collection. -/// -public class PickFirstServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } endPoints) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return endPoints[0]; - } - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs deleted file mode 100644 index d3f657c9550..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PickFirstServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PickFirstServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PickFirstServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs deleted file mode 100644 index e233dfb7b5c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.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. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Selects endpoints using the Power of Two Choices algorithm for distributed load balancing based on -/// the last-known load of the candidate endpoints. -/// -public class PowerOfTwoChoicesServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - if (collection.Count == 1) - { - return collection[0]; - } - - var first = collection[Random.Shared.Next(collection.Count)]; - ServiceEndPoint second; - do - { - second = collection[Random.Shared.Next(collection.Count)]; - } while (ReferenceEquals(first, second)); - - // Note that this relies on fresh data to be effective. - if (first.Features.Get() is { } firstLoad - && second.Features.Get() is { } secondLoad) - { - return firstLoad.CurrentLoad < secondLoad.CurrentLoad ? first : second; - } - - // Degrade to random. - return first; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs deleted file mode 100644 index 00832bc7811..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PowerOfTwoChoicesServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PowerOfTwoChoicesServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PowerOfTwoChoicesServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs deleted file mode 100644 index 8e4bb2378d8..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which returns random endpoints from the collection. -/// -public class RandomServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return collection[Random.Shared.Next(collection.Count)]; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs deleted file mode 100644 index ae74b4032bc..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RandomServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RandomServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs index 5848c7d8f72..92da7cf25bf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs @@ -1,20 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector { private uint _next; - private ServiceEndPointCollection? _endPoints; + private IReadOnlyList? _endPoints; /// - public void SetEndPoints(ServiceEndPointCollection endPoints) + public void SetEndPoints(ServiceEndPointSource endPoints) { - _endPoints = endPoints; + _endPoints = endPoints.EndPoints; } /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs deleted file mode 100644 index 40d9ce7845c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6836e58cf6b..9a5d67db04e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index ab0ea286b68..483c08702df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -12,18 +11,17 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count != 0) + if (endPoints.EndPoints.Count == 0) { - return new(ResolutionStatus.None); + Log.UsingPassThrough(logger, serviceName); + var ep = ServiceEndPoint.Create(endPoint); + ep.Features.Set(this); + endPoints.EndPoints.Add(ep); } - Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); - return new(ResolutionStatus.Success); + return default; } public ValueTask DisposeAsync() => default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index b3a326010bb..83455e0979c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -4,18 +4,18 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { + var serviceName = query.OriginalString; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index c1c833de89f..bcfa59056cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; namespace Microsoft.Extensions.DependencyInjection; @@ -14,49 +12,30 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for configuring with service discovery. /// -public static class HttpClientBuilderExtensions +public static class ServiceDiscoveryHttpClientBuilderExtensions { /// /// Adds service discovery to the . /// /// The builder. - /// The provider that creates selector instances. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder, IServiceEndPointSelectorProvider selectorProvider) - { - var services = httpClientBuilder.Services; - services.AddServiceDiscoveryCore(); - httpClientBuilder.AddHttpMessageHandler(services => - { - var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - var options = services.GetRequiredService>(); - return new ResolvingHttpDelegatingHandler(registry, options); - }); - - // Configure the HttpClient to disable gRPC load balancing. - // This is done on all HttpClient instances but only impacts gRPC clients. - AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); - - return httpClientBuilder; - } + [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")] + public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery(); /// /// Adds service discovery to the . /// /// The builder. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) + public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { var services = httpClientBuilder.Services; services.AddServiceDiscoveryCore(); httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); + var resolverProvider = services.GetRequiredService(); + var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index d9510a3cf22..89c5a2d2eb0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -1,29 +1,63 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Primitives; + namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for configuring service discovery. +/// Options for service endpoint resolvers. /// public sealed class ServiceDiscoveryOptions { /// - /// The value for which indicates that all schemes are allowed. + /// The value indicating that all endpoint schemes are allowed. /// #pragma warning disable IDE0300 // Simplify collection initialization #pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllSchemes = new string[0]; + public static readonly string[] AllowAllSchemes = new string[0]; #pragma warning restore CA1825 // Avoid zero-length array allocations #pragma warning restore IDE0300 // Simplify collection initialization + /// + /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// + public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); + /// /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". /// /// - /// When set to , all schemes are allowed. + /// When set to , all schemes are allowed. /// Schemes are not case-sensitive. /// - public string[] AllowedSchemes { get; set; } = AllSchemes; -} + public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + { + if (allowedSchemes.Equals(AllowAllSchemes)) + { + if (schemes is string[] array) + { + return array; + } + + return schemes.ToArray(); + } + + List result = []; + foreach (var s in schemes) + { + foreach (var allowed in allowedSchemes) + { + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } + } + } + + return result.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs similarity index 57% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a4ba9b63b31..6403b214631 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -2,20 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Http; using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; using Microsoft.Extensions.ServiceDiscovery.PassThrough; -namespace Microsoft.Extensions.Hosting; +namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for configuring service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryServiceCollectionExtensions { /// /// Adds the core service discovery services and configures defaults. @@ -29,21 +30,47 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser .AddPassThroughServiceEndPointResolver(); } + /// + /// Adds the core service discovery services and configures defaults. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + { + return services.AddServiceDiscoveryCore(configureOptions: configureOptions) + .AddConfigurationServiceEndPointResolver() + .AddPassThroughServiceEndPointResolver(); + } + /// /// Adds the core service discovery services. /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + + /// + /// Adds the core service discovery services. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) { services.AddOptions(); services.AddLogging(); - services.TryAddSingleton(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); - services.TryAddSingleton(static sp => TimeProvider.System); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(_ => TimeProvider.System); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + if (configureOptions is not null) + { + services.Configure(configureOptions); + } + return services; } @@ -63,10 +90,10 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { @@ -84,7 +111,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs new file mode 100644 index 00000000000..1a14cb961b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// A mutable collection of service endpoints. +/// +internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +{ + private readonly List _endPoints = new(); + private readonly List _changeTokens = new(); + private readonly FeatureCollection _features = new FeatureCollection(); + + /// + /// Adds a change token. + /// + /// The change token. + public void AddChangeToken(IChangeToken changeToken) + { + _changeTokens.Add(changeToken); + } + + /// + /// Gets the feature collection. + /// + public IFeatureCollection Features => _features; + + /// + /// Gets the endpoints. + /// + public IList EndPoints => _endPoints; + + /// + /// Creates a from the provided instance. + /// + /// The service endpoint source. + public ServiceEndPointSource Build() + { + return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 9de4a61b41b..029d2601243 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -16,7 +15,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider; + private readonly ServiceEndPointWatcherFactory _resolverProvider; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -28,7 +27,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable /// /// The resolver factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) { _resolverProvider = resolverProvider; _timeProvider = timeProvider; @@ -40,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider /// The service name. /// A . /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(serviceName); ObjectDisposedException.ThrowIf(_disposed, this); @@ -157,7 +156,7 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateResolver(serviceName); + var resolver = _resolverProvider.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } @@ -182,7 +181,7 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) { try { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs deleted file mode 100644 index 415a2192c30..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs +++ /dev/null @@ -1,22 +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 Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for service endpoint resolvers. -/// -public sealed class ServiceEndPointResolverOptions -{ - /// - /// Gets or sets the period between polling resolvers which are in a pending state and do not support refresh notifications via . - /// - public TimeSpan PendingStatusRefreshPeriod { get; set; } = TimeSpan.FromSeconds(15); - - /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . - /// - public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index 17864811062..78a8f84b556 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -19,11 +18,11 @@ private sealed partial class Log [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); - public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); } static string GetEndPointString(ServiceEndPoint ep) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 8936d3722b5..9b1069d31e7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -4,34 +4,32 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -public sealed partial class ServiceEndPointWatcher( +internal sealed partial class ServiceEndPointWatcher( IServiceEndPointProvider[] resolvers, ILogger logger, string serviceName, TimeProvider timeProvider, - IOptions options) : IAsyncDisposable + IOptions options) : IAsyncDisposable { private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); private readonly object _lock = new(); private readonly ILogger _logger = logger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly ServiceEndPointResolverOptions _options = options.Value; + private readonly ServiceDiscoveryOptions _options = options.Value; private readonly IServiceEndPointProvider[] _resolvers = resolvers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointCollection? _cachedEndPoints; + private ServiceEndPointSource? _cachedEndPoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -59,23 +57,23 @@ public void Start() /// /// A . /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoResolvers(); // If the cache is valid, return the cached value. if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) { - return new ValueTask(cached); + return new ValueTask(cached); } // Otherwise, ensure the cache is being refreshed // Wait for the cache refresh to complete and return the cached value. return GetEndPointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) { - ServiceEndPointCollection? result; + ServiceEndPointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); @@ -126,79 +124,48 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointCollection? newEndPoints = null; + ServiceEndPointSource? newEndPoints = null; CacheStatus newCacheState; - ResolutionStatus status = ResolutionStatus.Success; - while (true) + try { - try + Log.ResolvingEndPoints(_logger, ServiceName); + var builder = new ServiceEndPointBuilder(); + foreach (var resolver in _resolvers) { - var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); - status = ResolutionStatus.Success; - Log.ResolvingEndPoints(_logger, ServiceName); - foreach (var resolver in _resolvers) - { - var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); - status = CombineStatus(status, resolverStatus); - } + await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + } - var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; - if (statusCode != ResolutionStatusCode.Success) + var endPoints = builder.Build(); + newCacheState = CacheStatus.Valid; + + lock (_lock) + { + // Check if we need to poll for updates or if we can register for change notification callbacks. + if (endPoints.ChangeToken.ActiveChangeCallbacks) { - if (statusCode is ResolutionStatusCode.Pending) - { - // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. - Log.ResolutionPending(_logger, ServiceName); - await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); - continue; - } - else if (statusCode is ResolutionStatusCode.Cancelled) + // Initiate a background refresh, if necessary. + endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) { - newCacheState = CacheStatus.Invalid; - error = status.Exception ?? new OperationCanceledException(); - break; - } - else if (statusCode is ResolutionStatusCode.Error) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception; - break; + _pollingTimer = null; + timer.Dispose(); } } - - lock (_lock) + else { - // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) - { - // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } - } - else - { - SchedulePollingTimer(); - } - - // The cache is valid - newEndPoints = endPoints; - newCacheState = CacheStatus.Valid; - break; + SchedulePollingTimer(); } + + // The cache is valid + newEndPoints = endPoints; + newCacheState = CacheStatus.Valid; } - catch (Exception exception) - { - error = exception; - newCacheState = CacheStatus.Invalid; - SchedulePollingTimer(); - status = CombineStatus(status, ResolutionStatus.FromException(exception)); - break; - } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); } // If there was an error, the cache must be invalid. @@ -215,7 +182,7 @@ private async Task RefreshAsyncInternal() if (OnEndPointsUpdated is { } callback) { - callback(new(newEndPoints, status)); + callback(new(newEndPoints, error)); } lock (_lock) @@ -255,48 +222,6 @@ private void SchedulePollingTimer() } } - private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) - { - if (existing.StatusCode > newStatus.StatusCode) - { - return existing; - } - - var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); - Exception? exception; - if (existing.Exception is not null && newStatus.Exception is not null) - { - List exceptions = new(); - AddExceptions(existing.Exception, exceptions); - AddExceptions(newStatus.Exception, exceptions); - exception = new AggregateException(exceptions); - } - else - { - exception = existing.Exception ?? newStatus.Exception; - } - - var message = code switch - { - ResolutionStatusCode.Error => exception!.Message ?? "Error", - _ => code.ToString(), - }; - - return new ResolutionStatus(code, exception, message); - - static void AddExceptions(Exception? exception, List exceptions) - { - if (exception is AggregateException ae) - { - exceptions.AddRange(ae.InnerExceptions); - } - else if (exception is not null) - { - exceptions.Add(exception); - } - } - } - /// public async ValueTask DisposeAsync() { @@ -328,48 +253,6 @@ private enum CacheStatus Valid } - private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) - { - if (changeToken.HasChanged) - { - return; - } - - TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - IDisposable? changeTokenRegistration = null; - IDisposable? cancellationRegistration = null; - IDisposable? pollPeriodRegistration = null; - CancellationTokenSource? timerCancellation = null; - - try - { - // Either wait for a callback or poll externally. - if (changeToken.ActiveChangeCallbacks) - { - changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - else - { - timerCancellation = new(pollPeriod); - pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - if (cancellationToken.CanBeCanceled) - { - cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - await completion.Task.ConfigureAwait(false); - } - finally - { - changeTokenRegistration?.Dispose(); - cancellationRegistration?.Dispose(); - pollPeriodRegistration?.Dispose(); - timerCancellation?.Dispose(); - } - } - private void ThrowIfNoResolvers() { if (_resolvers.Length == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs similarity index 90% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs index d7835f26d08..69f565eb8e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointResolverFactory +partial class ServiceEndPointWatcherFactory { private sealed partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs index c545c82e9e6..90f62ab0597 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs @@ -3,38 +3,42 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Creates service endpoint resolvers. +/// Creates service endpoint watchers. /// -public partial class ServiceEndPointResolverFactory( - IEnumerable resolvers, +internal sealed partial class ServiceEndPointWatcherFactory( + IEnumerable resolvers, ILogger resolverLogger, - IOptions options, + IOptions options, TimeProvider timeProvider) { - private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers + private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly IOptions _options = options; + private readonly IOptions _options = options; /// /// Creates a service endpoint resolver for the provided service name. /// - public ServiceEndPointWatcher CreateResolver(string serviceName) + public ServiceEndPointWatcher CreateWatcher(string serviceName) { ArgumentNullException.ThrowIfNull(serviceName); + if (!ServiceEndPointQuery.TryParse(serviceName, out var query)) + { + throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); + } + List? resolvers = null; foreach (var factory in _resolverProviders) { - if (factory.TryCreateResolver(serviceName, out var resolver)) + if (factory.TryCreateProvider(query, out var resolver)) { resolvers ??= []; resolvers.Add(resolver); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs index 6d3132da880..6b5b07d199e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// An endpoint represented by a . /// /// The . -public sealed class UriEndPoint(Uri uri) : EndPoint +internal sealed class UriEndPoint(Uri uri) : EndPoint { /// /// Gets the associated with this endpoint. diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7e2ef478ef7..25cd88a1436 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class DnsSrvServiceEndPointResolverTests { @@ -103,9 +103,9 @@ public async Task ResolveServiceEndPoint_Dns() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -114,14 +114,13 @@ public async Task ResolveServiceEndPoint_Dns() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -190,9 +189,9 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -200,20 +199,19 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo resolver.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); - Assert.Null(initialResult.Status.Exception); + Assert.Null(initialResult.Exception); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -223,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo else { // We expect only the results from the Configuration provider. - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -271,9 +269,9 @@ public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() .AddServiceDiscovery() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var channel = Channel.CreateUnbounded(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index f35ffa2026c..6d8091f026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -5,15 +5,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// public class ConfigurationServiceEndPointResolverTests { @@ -29,9 +29,9 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -40,11 +40,10 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -67,12 +66,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() .AddConfigurationServiceEndPointResolver() .Configure(o => o.AllowedSchemes = ["https"]) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -81,13 +80,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Empty(initialResult.EndPoints); + Assert.Empty(initialResult.EndPointSource.EndPoints); } // Specifying either https or http. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -96,14 +94,13 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } // Specifying either https or http, but in reverse. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -112,8 +109,7 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } @@ -135,9 +131,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -146,12 +142,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -160,7 +155,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. - await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -169,12 +164,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -204,9 +198,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -215,13 +209,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -250,9 +243,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -261,17 +254,16 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); // We expect the HTTPS endpoint back but not the HTTP one. - Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index d8adcbca529..643bbfad441 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -5,8 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; using Xunit; @@ -14,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for . -/// These also cover and by extension. +/// These also cover and by extension. /// public class PassThroughServiceEndPointResolverTests { @@ -25,9 +24,9 @@ public async Task ResolveServiceEndPoint_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -36,8 +35,7 @@ public async Task ResolveServiceEndPoint_PassThrough() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } @@ -57,9 +55,9 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -68,11 +66,10 @@ public async Task ResolveServiceEndPoint_Superseded() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -91,9 +88,9 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -102,11 +99,10 @@ public async Task ResolveServiceEndPoint_Fallback() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -128,7 +124,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .BuildServiceProvider(); var resolver = services.GetRequiredService(); - var endPoints = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); + var result = await resolver.GetEndPointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 0628bedbe73..f5e506a9b72 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// public class ServiceEndPointResolverTests { @@ -25,8 +25,8 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + var resolverFactory = services.GetRequiredService(); + var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket")); Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } @@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); var exception = Assert.Throws(resolverFactory.Start); Assert.Equal("No service endpoint resolvers are configured.", exception.Message); exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); @@ -49,35 +49,34 @@ public void ResolveServiceEndPoint_NullServiceName_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - Assert.Throws(() => resolverFactory.CreateResolver(null!)); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task UseServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoResolvers_Throws() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")) - .UseServiceDiscovery(); + serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery(); var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory { - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { bool result; - (result, resolver) = createResolverDelegate(serviceName); + (result, resolver) = createResolverDelegate(query); return result; } } - private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } @@ -101,18 +100,18 @@ public async Task ResolveServiceEndPoint() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -124,10 +123,9 @@ public async Task ResolveServiceEndPoint() cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); - Assert.Equal(ResolutionStatus.Success, resolverResult.Status); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPoints.Count); - var endpoints = resolverResult.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); + var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); endpoints.Sort((l, r) => l.Port - r.Port); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); @@ -154,15 +152,15 @@ public async Task ResolveServiceEndPointOneShot() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -190,12 +188,11 @@ public async Task ResolveHttpServiceEndPointOneShot() disposeAsync: () => default); var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System); Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); @@ -232,25 +229,24 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); - return ResolutionStatus.Success; }, disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); var initialEndPoints = await initialEndPointsTask; Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints); + Assert.Single(initialEndPoints.EndPoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -283,9 +279,9 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var task = resolver.GetEndPointsAsync(CancellationToken.None); sem.Release(1); - var endPoints = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, endPoints); - var sep = Assert.Single(endPoints); + var result = await task.ConfigureAwait(false); + Assert.NotSame(initialEndPoints, result); + var sep = Assert.Single(result.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); From 76c92cbc99bffcf71016a406630febe678753bcd Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 2 Apr 2024 18:38:23 +1100 Subject: [PATCH 036/472] Remove obsolete APIs. (#3329) --- .../ServiceDiscoveryHttpClientBuilderExtensions.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index bcfa59056cc..b4c34ccb7c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -14,14 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceDiscoveryHttpClientBuilderExtensions { - /// - /// Adds service discovery to the . - /// - /// The builder. - /// The builder. - [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")] - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery(); - /// /// Adds service discovery to the . /// From 2837a066d733d9e9773f6ccd5391a632640c73ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:15:52 -0500 Subject: [PATCH 037/472] Remove obsolete APIs. (#3336) Co-authored-by: Mitch Denny --- .../ServiceDiscoveryHttpClientBuilderExtensions.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index bcfa59056cc..b4c34ccb7c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -14,14 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceDiscoveryHttpClientBuilderExtensions { - /// - /// Adds service discovery to the . - /// - /// The builder. - /// The builder. - [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")] - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery(); - /// /// Adds service discovery to the . /// From 30697f6b7e9134bf3db6cf60a78cd98d6e8caae1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:34:43 -0700 Subject: [PATCH 038/472] Service Discovery: Implement approved API (#3413) * Rename EndPoint to Endpoint, resolver to provider * Apply changes decided during API review * Find & fix straggler file names * Variables and members assignable to System.Net.EndPoint use upper-case P * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs Co-authored-by: Stephen Halter * Delete GetEndpointString() --------- Co-authored-by: Stephen Halter --- .../IServiceEndPointProviderFactory.cs | 20 -- ...tBuilder.cs => IServiceEndpointBuilder.cs} | 8 +- ...rovider.cs => IServiceEndpointProvider.cs} | 6 +- .../IServiceEndpointProviderFactory.cs | 20 ++ ...EndPointImpl.cs => ServiceEndpointImpl.cs} | 8 +- .../README.md | 2 +- ...{ServiceEndPoint.cs => ServiceEndpoint.cs} | 21 +-- ...dPointQuery.cs => ServiceEndpointQuery.cs} | 35 ++-- ...ointSource.cs => ServiceEndpointSource.cs} | 16 +- .../DnsServiceEndPointResolverProvider.cs | 21 --- ...olver.cs => DnsServiceEndpointProvider.cs} | 28 +-- ... => DnsServiceEndpointProviderBase.Log.cs} | 2 +- ...e.cs => DnsServiceEndpointProviderBase.cs} | 36 ++-- .../DnsServiceEndpointProviderFactory.cs | 21 +++ ...s => DnsServiceEndpointProviderOptions.cs} | 6 +- ...er.cs => DnsSrvServiceEndpointProvider.cs} | 34 ++-- ...> DnsSrvServiceEndpointProviderFactory.cs} | 19 +- ...> DnsSrvServiceEndpointProviderOptions.cs} | 6 +- .../README.md | 16 +- ...DiscoveryDnsServiceCollectionExtensions.cs | 33 +++- .../ServiceDiscoveryDestinationResolver.cs | 10 +- ...nfigurationServiceEndpointProvider.Log.cs} | 12 +- ...> ConfigurationServiceEndpointProvider.cs} | 64 +++---- ...gurationServiceEndpointProviderFactory.cs} | 12 +- ...erviceEndpointProviderOptionsValidator.cs} | 10 +- ...gurationServiceEndpointProviderOptions.cs} | 6 +- ...lver.cs => HttpServiceEndpointResolver.cs} | 46 ++--- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 2 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 20 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 4 +- ...lt.cs => ServiceEndpointResolverResult.cs} | 8 +- ...elector.cs => IServiceEndpointSelector.cs} | 10 +- ...s => RoundRobinServiceEndpointSelector.cs} | 12 +- ...PassThroughServiceEndpointProvider.Log.cs} | 4 +- ... => PassThroughServiceEndpointProvider.cs} | 14 +- ...sThroughServiceEndpointProviderFactory.cs} | 20 +- .../README.md | 76 ++++---- ...iceDiscoveryHttpClientBuilderExtensions.cs | 4 +- .../ServiceDiscoveryOptions.cs | 23 +-- ...iceDiscoveryServiceCollectionExtensions.cs | 40 ++-- .../ServiceEndPointWatcherFactory.cs | 61 ------ ...ntBuilder.cs => ServiceEndpointBuilder.cs} | 12 +- ...Resolver.cs => ServiceEndpointResolver.cs} | 36 ++-- ...r.Log.cs => ServiceEndpointWatcher.Log.cs} | 24 +-- ...ntWatcher.cs => ServiceEndpointWatcher.cs} | 80 ++++---- ...s => ServiceEndpointWatcherFactory.Log.cs} | 11 +- .../ServiceEndpointWatcherFactory.cs | 61 ++++++ ... => DnsSrvServiceEndpointResolverTests.cs} | 125 ++++-------- ...figurationServiceEndpointResolverTests.cs} | 178 +++++++++--------- ...assThroughServiceEndpointResolverTests.cs} | 74 ++++---- ...sts.cs => ServiceEndpointResolverTests.cs} | 150 +++++++-------- 52 files changed, 763 insertions(+), 810 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointBuilder.cs => IServiceEndpointBuilder.cs} (80%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointProvider.cs => IServiceEndpointProvider.cs} (75%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/{ServiceEndPointImpl.cs => ServiceEndpointImpl.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPoint.cs => ServiceEndpoint.cs} (52%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointQuery.cs => ServiceEndpointQuery.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointSource.cs => ServiceEndpointSource.cs} (74%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.cs => DnsServiceEndpointProvider.cs} (61%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.Log.cs => DnsServiceEndpointProviderBase.Log.cs} (97%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.cs => DnsServiceEndpointProviderBase.cs} (81%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverOptions.cs => DnsServiceEndpointProviderOptions.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolver.cs => DnsSrvServiceEndpointProvider.cs} (71%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverProvider.cs => DnsSrvServiceEndpointProviderFactory.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverOptions.cs => DnsSrvServiceEndpointProviderOptions.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.Log.cs => ConfigurationServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.cs => ConfigurationServiceEndpointProvider.cs} (76%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverProvider.cs => ConfigurationServiceEndpointProviderFactory.cs} (58%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptionsValidator.cs => ConfigurationServiceEndpointProviderOptionsValidator.cs} (62%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndpointProviderOptions.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/{HttpServiceEndPointResolver.cs => HttpServiceEndpointResolver.cs} (82%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/{ServiceEndPointResolverResult.cs => ServiceEndpointResolverResult.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{IServiceEndPointSelector.cs => IServiceEndpointSelector.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{RoundRobinServiceEndPointSelector.cs => RoundRobinServiceEndpointSelector.cs} (63%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.Log.cs => PassThroughServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.cs => PassThroughServiceEndpointProvider.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolverProvider.cs => PassThroughServiceEndpointProviderFactory.cs} (70%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointBuilder.cs => ServiceEndpointBuilder.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.cs => ServiceEndpointResolver.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.Log.cs => ServiceEndpointWatcher.Log.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.cs => ServiceEndpointWatcher.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcherFactory.Log.cs => ServiceEndpointWatcherFactory.Log.cs} (53%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsSrvServiceEndPointResolverTests.cs => DnsSrvServiceEndpointResolverTests.cs} (72%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ConfigurationServiceEndPointResolverTests.cs => ConfigurationServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{PassThroughServiceEndPointResolverTests.cs => PassThroughServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ServiceEndPointResolverTests.cs => ServiceEndpointResolverTests.cs} (62%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs deleted file mode 100644 index 4b1876f808e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs +++ /dev/null @@ -1,20 +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.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates instances. -/// -public interface IServiceEndPointProviderFactory -{ - /// - /// Tries to create an instance for the specified . - /// - /// The service to create the resolver for. - /// The resolver. - /// if the resolver was created, otherwise. - bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs similarity index 80% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs index 468adea1c09..e051b2bf746 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs @@ -7,14 +7,14 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Builder to create a instances. +/// Builder to create a instances. /// -public interface IServiceEndPointBuilder +public interface IServiceEndpointBuilder { /// /// Gets the endpoints. /// - IList EndPoints { get; } + IList Endpoints { get; } /// /// Gets the feature collection. @@ -22,7 +22,7 @@ public interface IServiceEndPointBuilder IFeatureCollection Features { get; } /// - /// Adds a change token to the resulting . + /// Adds a change token to the resulting . /// /// The change token. void AddChangeToken(IChangeToken changeToken); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs index 950823257af..4a192180b66 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. /// -public interface IServiceEndPointProvider : IAsyncDisposable +public interface IServiceEndpointProvider : IAsyncDisposable { /// /// Resolves the endpoints for the service. /// - /// The endpoint collection, which resolved endpoints will be added to. + /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..009cbf05d76 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates instances. +/// +public interface IServiceEndpointProviderFactory +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the provider for. + /// The provider. + /// if the provider was created, otherwise. + bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 7d135dfe97d..8bfb50fe930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -6,9 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint +internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); - public override string? ToString() => GetEndPointString(); + public override string? ToString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + _ => EndPoint.ToString()! + }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md index c5cf6b9bc78..0d97211313e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -1,6 +1,6 @@ # Microsoft.Extensions.ServiceDiscovery.Abstractions -The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint providers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). ## Feedback & contributing diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs similarity index 52% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index a3cde62ce0d..238e383a957 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -11,8 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. /// -[DebuggerDisplay("{GetEndPointString(),nq}")] -public abstract class ServiceEndPoint +public abstract class ServiceEndpoint { /// /// Gets the endpoint. @@ -25,21 +23,10 @@ public abstract class ServiceEndPoint public abstract IFeatureCollection Features { get; } /// - /// Creates a new . + /// Creates a new . /// /// The endpoint being represented. /// Features of the endpoint. - /// A newly initialized . - public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); - - /// - /// Gets a string representation of the . - /// - /// A string representation of the . - public virtual string GetEndPointString() => EndPoint switch - { - DnsEndPoint dns => $"{dns.Host}:{dns.Port}", - IPEndPoint ip => ip.ToString(), - _ => EndPoint.ToString()! - }; + /// A newly initialized . + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 99c92cce27c..600dc5cc28c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -8,21 +8,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Describes a query for endpoints of a service. /// -public sealed class ServiceEndPointQuery +public sealed class ServiceEndpointQuery { + private readonly string _originalString; + /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The string which the query was constructed from. /// The ordered list of included URI schemes. /// The service name. - /// The optional endpoint name. - private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName) + /// The optional endpoint name. + private ServiceEndpointQuery(string originalString, string[] includedSchemes, string serviceName, string? endpointName) { - OriginalString = originalString; - IncludeSchemes = includedSchemes; + _originalString = originalString; + IncludedSchemes = includedSchemes; ServiceName = serviceName; - EndPointName = endPointName; + EndpointName = endpointName; } /// @@ -31,7 +33,7 @@ private ServiceEndPointQuery(string originalString, string[] includedSchemes, st /// The value to parse. /// The resulting query. /// if the value was successfully parsed; otherwise . - public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query) + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) @@ -52,10 +54,10 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin var uriHost = uri.Host; var segmentSeparatorIndex = uriHost.IndexOf('.'); string host; - string? endPointName = null; + string? endpointName = null; if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') { - endPointName = uriHost[1..segmentSeparatorIndex]; + endpointName = uriHost[1..segmentSeparatorIndex]; // Skip the endpoint name, including its prefix ('_') and suffix ('.'). host = uriHost[(segmentSeparatorIndex + 1)..]; @@ -67,24 +69,19 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". var schemes = hasScheme ? uri.Scheme.Split('+') : []; - query = new(input, schemes, host, endPointName); + query = new(input, schemes, host, endpointName); return true; } - /// - /// Gets the string which the query was constructed from. - /// - public string OriginalString { get; } - /// /// Gets the ordered list of included URI schemes. /// - public IReadOnlyList IncludeSchemes { get; } + public IReadOnlyList IncludedSchemes { get; } /// /// Gets the endpoint name, or if no endpoint name is specified. /// - public string? EndPointName { get; } + public string? EndpointName { get; } /// /// Gets the service name. @@ -92,6 +89,6 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin public string ServiceName { get; } /// - public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}"; + public override string? ToString() => _originalString; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs similarity index 74% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index 807981226e3..fb5bff1b288 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -11,18 +11,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public sealed class ServiceEndPointSource +[DebuggerTypeProxy(typeof(ServiceEndpointCollectionDebuggerView))] +public sealed class ServiceEndpointSource { - private readonly List? _endpoints; + private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); @@ -34,7 +34,7 @@ public ServiceEndPointSource(List? endpoints, IChangeToken chan /// /// Gets the endpoints. /// - public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; + public IReadOnlyList Endpoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -57,13 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) + private sealed class ServiceEndpointCollectionDebuggerView(ServiceEndpointSource value) { public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); + public ServiceEndpoint[] Endpoints => value.Endpoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs deleted file mode 100644 index 51525663a03..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ /dev/null @@ -1,21 +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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Dns; - -internal sealed partial class DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : IServiceEndPointProviderFactory -{ - /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) - { - resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); - return true; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index a2601c84b45..6cc9f92bc46 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -7,12 +7,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsServiceEndpointProvider( + ServiceEndpointQuery query, string hostName, - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; @@ -26,27 +26,27 @@ internal sealed partial class DnsServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - endPoints.Add(serviceEndPoint); + endpoints.Add(serviceEndpoint); } - if (endPoints.Count == 0) + if (endpoints.Count == 0) { - throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); + throw new InvalidOperationException($"No DNS records were found for service '{ServiceName}' (DNS name: '{hostName}')."); } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs index cd664215aa9..29aaaf8e930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndpointProviderBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 9d6c54e4755..6c69cc7a760 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -7,9 +7,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// A service end point resolver that uses DNS to resolve the service end points. +/// A service end point provider that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider +internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpointProvider { private readonly object _lock = new(); private readonly ILogger _logger; @@ -20,23 +20,23 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; + private List? _lastEndpointCollection; private TimeSpan _nextRefreshPeriod; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. + /// The service name. /// The logger. /// The time provider. - protected DnsServiceEndPointResolverBase( - string serviceName, + protected DnsServiceEndpointProviderBase( + ServiceEndpointQuery query, ILogger logger, TimeProvider timeProvider) { - ServiceName = serviceName; + ServiceName = query.ToString()!; _logger = logger; - _lastEndPointCollection = null; + _lastEndpointCollection = null; _timeProvider = timeProvider; _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); @@ -58,10 +58,10 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public async ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. - if (endPoints.EndPoints.Count != 0) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return; @@ -85,28 +85,28 @@ public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, Cancella lock (_lock) { - if (_lastEndPointCollection is { Count: > 0 } eps) + if (_lastEndpointCollection is { Count: > 0 } eps) { foreach (var ep in eps) { - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } - endPoints.AddChangeToken(_lastChangeToken); + endpoints.AddChangeToken(_lastChangeToken); return; } } - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + private bool ShouldRefresh() => _lastEndpointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; protected abstract Task ResolveAsyncCore(); - protected void SetResult(List endPoints, TimeSpan validityPeriod) + protected void SetResult(List endpoints, TimeSpan validityPeriod) { lock (_lock) { - if (endPoints is { Count: > 0 }) + if (endpoints is { Count: > 0 }) { _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); _nextRefreshPeriod = DefaultRefreshPeriod; @@ -131,7 +131,7 @@ protected void SetResult(List endPoints, TimeSpan validityPerio _lastCollectionCancellation.Cancel(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; + _lastEndpointCollection = endpoints; } TimeSpan GetRefreshPeriod() diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..c241ad89dd3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndpointProviderFactory +{ + /// + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) + { + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs index 665c98bbc09..b163afc76ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsServiceEndPointResolverOptions +public class DnsServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -31,5 +31,5 @@ public class DnsServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs similarity index 71% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index d59dbfbb69c..dd17a7e2732 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -9,14 +9,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsSrvServiceEndpointProvider( + ServiceEndpointQuery query, string srvQuery, string hostName, - IOptionsMonitor options, - ILogger logger, + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); @@ -59,15 +59,15 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); + endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) { @@ -79,22 +79,22 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) { var msg = errorMessage switch { - { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", - _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", + _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." }; return new InvalidOperationException(msg); } - ServiceEndPoint CreateEndPoint(EndPoint endPoint) + ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 8a75c1d1bbf..fd0cb28353d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -8,11 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, +internal sealed partial class DnsSrvServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointProviderFactory + TimeProvider timeProvider) : IServiceEndpointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -20,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md @@ -30,17 +30,16 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". - var serviceName = query.OriginalString; if (string.IsNullOrWhiteSpace(_querySuffix)) { - DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); - resolver = default; + DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); + provider = default; return false; } - var portName = query.EndPointName ?? "default"; + var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index 704e03cd9ca..c908c56d770 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsSrvServiceEndPointResolverOptions +public class DnsSrvServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -39,5 +39,5 @@ public class DnsSrvServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md index d3fbe2a75e5..8be4560870b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -1,25 +1,25 @@ # Microsoft.Extensions.ServiceDiscovery.Dns -This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint providers: -- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. - _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). ## Resolving service endpoints with DNS -The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +The _DNS_ service endpoint provider resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS service endpoint provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. -To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: +To configure the DNS service endpoint provider in your application, add the DNS service endpoint provider to your host builder's service collection using the `AddDnsServiceEndpointProvider` method. service discovery as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsServiceEndPointResolver(); +builder.Services.AddDnsServiceEndpointProvider(); ``` ## Resolving service endpoints in Kubernetes with DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -37,11 +37,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 0d795660fd2..17544d09486 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -14,6 +14,17 @@ namespace Microsoft.Extensions.Hosting; /// public static class ServiceDiscoveryDnsServiceCollectionExtensions { + /// + /// Adds DNS SRV service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + /// /// Adds DNS SRV service discovery to the . /// @@ -24,16 +35,26 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } + /// + /// Adds DNS service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + /// /// Adds DNS service discovery to the . /// @@ -43,11 +64,11 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 22d7e6d8327..8ec810f2c05 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,14 +54,14 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count); + var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(result.Endpoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; - foreach (var endPoint in result.EndPoints) + foreach (var endpoint in result.Endpoints) { - var addressString = endPoint.GetEndPointString(); + var addressString = endpoint.ToString()!; Uri uri; if (!addressString.Contains("://")) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index fdb61ef59f4..48c922a85b3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed partial class ConfigurationServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndpointProvider { private sealed partial class Log { @@ -19,10 +19,10 @@ private sealed partial class Log [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); - [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] - internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); + [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndpoints}.", EventName = "ConfiguredEndpoints")] + internal static partial void ConfiguredEndpoints(ILogger logger, string serviceName, string path, string configuredEndpoints); - internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) + internal static void ConfiguredEndpoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -40,8 +40,8 @@ internal static void ConfiguredEndPoints(ILogger logger, string serviceName, str endpointValues.Append(endpoints[i].ToString()); } - var configuredEndPoints = endpointValues.ToString(); - ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + var configuredEndpoints = endpointValues.ToString(); + ConfiguredEndpoints(logger, serviceName, path, configuredEndpoints); } [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs similarity index 76% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 9604ec0c201..37078e01969 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -10,36 +10,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// A service endpoint resolver that uses configuration to resolve resolved. +/// A service endpoint provider that uses configuration to resolve resolved. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature +internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEndpointProvider, IHostNameFeature { - private const string DefaultEndPointName = "default"; + private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; private readonly string[] _schemes; private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IOptions _options; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The query. /// The configuration. /// The logger. - /// Configuration resolver options. + /// Configuration provider options. /// Service discovery options. - public ConfigurationServiceEndPointResolver( - ServiceEndPointQuery query, + public ConfigurationServiceEndpointProvider( + ServiceEndpointQuery query, IConfiguration configuration, - ILogger logger, - IOptions options, + ILogger logger, + IOptions options, IOptions serviceDiscoveryOptions) { _serviceName = query.ServiceName; - _endpointName = query.EndPointName; - _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); + _endpointName = query.EndpointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -49,10 +49,10 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { // Only add resolved to the collection if a previous provider (eg, an override) did not add them. - if (endPoints.EndPoints.Count != 0) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return default; @@ -62,12 +62,12 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - endPoints.AddChangeToken(_configuration.GetReloadToken()); + endpoints.AddChangeToken(_configuration.GetReloadToken()); Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); return default; } - endPoints.AddChangeToken(section.GetReloadToken()); + endpoints.AddChangeToken(section.GetReloadToken()); // Find an appropriate configuration section based on the input. IConfigurationSection? namedSection = null; @@ -77,7 +77,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (_schemes.Length == 0) { // Use the section named "default". - endpointName = DefaultEndPointName; + endpointName = DefaultEndpointName; namedSection = section.GetSection(endpointName); } else @@ -112,14 +112,14 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo return default; } - List resolved = []; + List resolved = []; Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); // Account for both the single and multi-value cases. if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - AddEndPoint(resolved, namedSection, endpointName); + AddEndpoint(resolved, namedSection, endpointName); } else { @@ -131,7 +131,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - AddEndPoint(resolved, child, endpointName); + AddEndpoint(resolved, child, endpointName); } } @@ -158,13 +158,13 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (index >= 0 && index <= minIndex) { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } else { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } @@ -174,7 +174,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo } else { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); } return default; @@ -182,7 +182,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo string IHostNameFeature.HostName => _serviceName; - private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) + private void AddEndpoint(List endpoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) @@ -190,7 +190,7 @@ private void AddEndPoint(List endPoints, IConfigurationSection throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } - endPoints.Add(CreateEndPoint(endPoint)); + endpoints.Add(CreateEndpoint(endPoint)); } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -220,16 +220,16 @@ private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPo return true; } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + private ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (_options.Value.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } public override string ToString() => "Configuration"; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs similarity index 58% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs index 032c50b6f27..a966cd44794 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs @@ -9,18 +9,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// -internal sealed class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndpointProviderFactory( IConfiguration configuration, - IOptions options, + IOptions options, IOptions serviceDiscoveryOptions, - ILogger logger) : IServiceEndPointProviderFactory + ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); + provider = new ConfigurationServiceEndpointProvider(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs index 91e97b5d0bc..f8092c4dd51 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs @@ -1,22 +1,22 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +internal sealed class ConfigurationServiceEndpointProviderOptionsValidator : IValidateOptions { - public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndpointProviderOptions options) { if (string.IsNullOrWhiteSpace(options.SectionName)) { return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); } - if (options.ApplyHostNameMetadata is null) + if (options.ShouldApplyHostNameMetadata is null) { - return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + return ValidateOptionsResult.Fail($"{nameof(options.ShouldApplyHostNameMetadata)} must not be null."); } return ValidateOptionsResult.Success; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs index d3b94f2f1e7..29f28e359f7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for . +/// Options for . /// -public sealed class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndpointProviderOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". @@ -18,5 +18,5 @@ public sealed class ConfigurationServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index 44e58b0dbbb..e11f593776f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -11,13 +11,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; + private readonly ServiceEndpointWatcherFactory _watcherFactory = watcherFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -29,7 +29,7 @@ internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. - public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); if (request.RequestUri is null) @@ -47,15 +47,15 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ static (name, self) => self.CreateResolver(name), this); - var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + var (valid, endpoint) = await resolver.TryGetEndpointAsync(request, cancellationToken).ConfigureAwait(false); if (valid) { - if (endPoint is null) + if (endpoint is null) { throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); } - return endPoint; + return endpoint; } } } @@ -148,37 +148,37 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateWatcher(serviceName); - var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); - var result = new ResolverEntry(resolver, selector); - resolver.Start(); + var watcher = _watcherFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndpointSelector(); + var result = new ResolverEntry(watcher, selector); + watcher.Start(); return result; } private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver; - private readonly IServiceEndPointSelector _selector; + private readonly ServiceEndpointWatcher _watcher; + private readonly IServiceEndpointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndpointWatcher watcher, IServiceEndpointSelector selector) { - _resolver = resolver; + _watcher = watcher; _selector = selector; - _resolver.OnEndPointsUpdated += result => + _watcher.OnEndpointsUpdated += result => { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPointSource); + _selector.SetEndpoints(result.EndpointSource); } }; } - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -189,17 +189,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpoint? Endpoint)> TryGetEndpointAsync(object? context, CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - var result = _selector.GetEndPoint(context); + await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndpoint(context); return (true, result); } else @@ -246,7 +246,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs index 0febfa94815..0c5bd02d10d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Extensions.ServiceDiscovery.Http; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index bc06a031700..a0063ae476b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndpointResolver resolver, IOptions options) : HttpClientHandler { - private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly HttpServiceEndpointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; /// @@ -23,7 +23,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index daa7b8a17de..8f13bb60ab5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { - private readonly HttpServiceEndPointResolver _resolver; + private readonly HttpServiceEndpointResolver _resolver; private readonly ServiceDiscoveryOptions _options; /// @@ -19,7 +19,7 @@ internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler /// /// The endpoint resolver. /// The service discovery options. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options) { _resolver = resolver; _options = options.Value; @@ -31,7 +31,7 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt /// The endpoint resolver. /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; _options = options.Value; @@ -44,7 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } @@ -58,11 +58,11 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) + internal static Uri GetUriWithEndpoint(Uri uri, ServiceEndpoint serviceEndpoint, ServiceDiscoveryOptions options) { - var endpoint = serviceEndPoint.EndPoint; + var endPoint = serviceEndpoint.EndPoint; UriBuilder result; - if (endpoint is UriEndPoint { Uri: { } ep }) + if (endPoint is UriEndPoint { Uri: { } ep }) { result = new UriBuilder(uri) { @@ -84,7 +84,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, { string host; int port; - switch (endpoint) + switch (endPoint) { case IPEndPoint ip: host = ip.Address.ToString(); @@ -95,7 +95,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, port = dns.Port; break; default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + throw new InvalidOperationException($"Endpoints of type {endPoint.GetType()} are not supported"); } result = new UriBuilder(uri) @@ -112,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, if (uri.Scheme.IndexOf('+') > 0) { var scheme = uri.Scheme.Split('+')[0]; - if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowAllSchemes || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs index 0d3ba00122f..e5e7f7587bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -8,12 +8,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( TimeProvider timeProvider, IServiceProvider serviceProvider, - ServiceEndPointWatcherFactory factory, + ServiceEndpointWatcherFactory factory, IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory { public HttpMessageHandler CreateHandler(HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + var registry = new HttpServiceEndpointResolver(factory, serviceProvider, timeProvider); return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs index 07bffa5654b..675941bb955 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; /// /// Represents the result of service endpoint resolution. /// -/// The endpoint collection. +/// The endpoint collection. /// The exception which occurred during resolution. -internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +internal sealed class ServiceEndpointResolverResult(ServiceEndpointSource? endpointSource, Exception? exception) { /// /// Gets the exception which occurred during resolution. @@ -20,11 +20,11 @@ internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPo /// /// Gets a value indicating whether resolution completed successfully. /// - [MemberNotNullWhen(true, nameof(EndPointSource))] + [MemberNotNullWhen(true, nameof(EndpointSource))] public bool ResolvedSuccessfully => Exception is null; /// /// Gets the endpoints. /// - public ServiceEndPointSource? EndPointSource { get; } = endPointSource; + public ServiceEndpointSource? EndpointSource { get; } = endpointSource; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs index bd0172c45cf..2d81ff38601 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs @@ -6,18 +6,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -internal interface IServiceEndPointSelector +internal interface IServiceEndpointSelector { /// /// Sets the collection of endpoints which this instance will select from. /// - /// The collection of endpoints to select from. - void SetEndPoints(ServiceEndPointSource endPoints); + /// The collection of endpoints to select from. + void SetEndpoints(ServiceEndpointSource endpoints); /// - /// Selects an endpoints from the collection provided by the most recent call to . + /// Selects an endpoints from the collection provided by the most recent call to . /// /// The context. /// An endpoint. - ServiceEndPoint GetEndPoint(object? context); + ServiceEndpoint GetEndpoint(object? context); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs similarity index 63% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs index 92da7cf25bf..e7e51bc6021 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs @@ -6,21 +6,21 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndpointSelector : IServiceEndpointSelector { private uint _next; - private IReadOnlyList? _endPoints; + private IReadOnlyList? _endpoints; /// - public void SetEndPoints(ServiceEndPointSource endPoints) + public void SetEndpoints(ServiceEndpointSource endpoints) { - _endPoints = endPoints.EndPoints; + _endpoints = endpoints.Endpoints; } /// - public ServiceEndPoint GetEndPoint(object? context) + public ServiceEndpoint GetEndpoint(object? context) { - if (_endPoints is not { Count: > 0 } collection) + if (_endpoints is not { Count: > 0 } collection) { throw new InvalidOperationException("The endpoint collection contains no endpoints"); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index 570eb5e4e47..f9a984cfe4f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -5,11 +5,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; -internal sealed partial class PassThroughServiceEndPointResolver +internal sealed partial class PassThroughServiceEndpointProvider { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs index 483c08702df..478d81d42dc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs @@ -7,18 +7,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver which passes through the provided value. +/// Service endpoint provider which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider +internal sealed partial class PassThroughServiceEndpointProvider(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count == 0) + if (endpoints.Endpoints.Count == 0) { Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); + var ep = ServiceEndpoint.Create(endPoint); + ep.Features.Set(this); + endpoints.Endpoints.Add(ep); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs index 83455e0979c..2bf8c0cb481 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs @@ -8,29 +8,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver provider which passes through the provided value. +/// Service endpoint provider factory which creates pass-through providers. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory +internal sealed class PassThroughServiceEndpointProviderFactory(ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - var serviceName = query.OriginalString; + var serviceName = query.ToString()!; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); + provider = new PassThroughServiceEndpointProvider(logger, serviceName, endPoint); return true; } - private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? endPoint) { if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) { - serviceEndPoint = null; + endPoint = null; return false; } @@ -50,15 +50,15 @@ private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] ou var port = uri.Port > 0 ? uri.Port : 0; if (IPAddress.TryParse(host, out var ip)) { - serviceEndPoint = new IPEndPoint(ip, port); + endPoint = new IPEndPoint(ip, port); } else if (!string.IsNullOrEmpty(host)) { - serviceEndPoint = new DnsEndPoint(host, port); + endPoint = new DnsEndPoint(host, port); } else { - serviceEndPoint = null; + endPoint = null; return false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 8bece9644ff..04119540e10 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -6,29 +6,27 @@ In typical systems, service configuration changes over time. Service discovery a ## How it works -Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). +Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. -Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. +Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. ### Change notifications -Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndpointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). ### Extensibility using features -Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: +Service endpoints (`ServiceEndpoint` instances) and collections of service endpoints (`ServiceEndpointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by providers. Features which may be available on a `ServiceEndpoint` include: * `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). -* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. -* `IEndPointLoadFeature`: used to query estimated endpoint load. ### Resolution order -The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. +The providers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. ## Getting Started @@ -42,19 +40,19 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); ``` -Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: +Add service discovery to an individual `IHttpClientBuilder` by calling the `AddServiceDiscovery` extension method: ```csharp builder.Services.AddHttpClient(c => { c.BaseAddress = new("http://catalog")); -}).UseServiceDiscovery(); +}).AddServiceDiscovery(); ``` Alternatively, you can add service discovery to all `HttpClient` instances by default: @@ -63,14 +61,14 @@ Alternatively, you can add service discovery to all `HttpClient` instances by de builder.Services.ConfigureHttpClientDefaults(http => { // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); ``` ### Resolving service endpoints from configuration -The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. -This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The `AddServiceDiscovery` extension method adds a configuration-based endpoint provider by default. +This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: @@ -89,30 +87,30 @@ Here is an example demonstrating how to configure a endpoints for the service na The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. ### Configuration -The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: +The configuration provider is configured using the `ConfigurationServiceEndpointProviderOptions` class, which offers these configuration options: * **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. -* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. +* **`ShouldApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(options => +builder.Services.Configure(options => { options.SectionName = "MyServiceEndpoints"; // Configure the logic for applying host name metadata - options.ApplyHostNameMetadata = endpoint => + options.ShouldApplyHostNameMetadata = endpoint => { // Your custom logic here. For example: - return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + return endpoint.Endpoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); }; }); ``` @@ -121,38 +119,38 @@ This example demonstrates setting a custom section name for your service endpoin ## Resolving service endpoints using platform-provided service discovery -Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. -The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. +The pass-through provider performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndpointProvider` extension method on `IServiceCollection`. In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". ## Load-balancing with endpoint selectors -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: ```csharp builder.Services.AddHttpClient( static client => client.BaseAddress = new("http://catalog")); - .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); + .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); ``` The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` -Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. +Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. ## Service discovery in .NET Aspire -.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: @@ -184,7 +182,7 @@ In the above example, two `HttpClient`s are added: one for the core basket servi ### Named endpoints using configuration -With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": +With the configuration-based endpoint provider, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": ```json { @@ -199,7 +197,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified ### Named endpoints in .NET Aspire -.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: +.NET Aspire uses the configuration-based provider at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: ```csharp var builder = DistributedApplication.CreateBuilder(args); @@ -208,13 +206,13 @@ var basket = builder.AddProject("basket") .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") - .WithReference(basket.GetEndPoint("admin")); + .WithReference(basket.GetEndpoint("admin")); var frontend = builder.AddProject("frontend") .WithReference(basket); ``` -In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndpoint(string name)` method, as in the following example: ```csharp @@ -226,7 +224,7 @@ var frontend = builder.AddProject("frontend") ### Named endpoints in Kubernetes using DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -244,11 +242,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index b4c34ccb7c5..8a137aad4f8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -26,8 +26,8 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); + var watcherFactory = services.GetRequiredService(); + var registry = new HttpServiceEndpointResolver(watcherFactory, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 89c5a2d2eb0..02ce1af162b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -6,21 +6,19 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for service endpoint resolvers. +/// Options for service endpoint resolution. /// public sealed class ServiceDiscoveryOptions { /// - /// The value indicating that all endpoint schemes are allowed. + /// Gets or sets a value indicating whether all URI schemes for URIs resolved by the service discovery system are allowed. + /// If this value is , all URI schemes are allowed. + /// If this value is , only the schemes specified in are allowed. /// -#pragma warning disable IDE0300 // Simplify collection initialization -#pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllowAllSchemes = new string[0]; -#pragma warning restore CA1825 // Avoid zero-length array allocations -#pragma warning restore IDE0300 // Simplify collection initialization + public bool AllowAllSchemes { get; set; } = true; /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// Gets or sets the period between polling attempts for providers which do not support refresh notifications via . /// public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); @@ -28,14 +26,13 @@ public sealed class ServiceDiscoveryOptions /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". /// /// - /// When set to , all schemes are allowed. - /// Schemes are not case-sensitive. + /// When is , this property is ignored. /// - public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + public IList AllowedSchemes { get; set; } = new List(); - internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowedSchemes.Equals(AllowAllSchemes)) + if (allowAllSchemes) { if (schemes is string[] array) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index 6403b214631..a5d789b7e4e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -26,8 +26,8 @@ public static class ServiceDiscoveryServiceCollectionExtensions public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { return services.AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -36,11 +36,11 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action configureOptions) { return services.AddServiceDiscoveryCore(configureOptions: configureOptions) - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -56,16 +56,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(_ => TimeProvider.System); - services.TryAddTransient(); - services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(sp => new ServiceEndpointResolver(sp.GetRequiredService(), sp.GetRequiredService())); if (configureOptions is not null) { services.Configure(configureOptions); @@ -75,26 +75,26 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { - return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); + services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); @@ -104,14 +104,14 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS } /// - /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// Configures a service discovery endpoint provider which passes through the input without performing resolution. /// /// The service collection. /// The service collection. - public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs deleted file mode 100644 index 90f62ab0597..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs +++ /dev/null @@ -1,61 +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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.PassThrough; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates service endpoint watchers. -/// -internal sealed partial class ServiceEndPointWatcherFactory( - IEnumerable resolvers, - ILogger resolverLogger, - IOptions options, - TimeProvider timeProvider) -{ - private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers - .Where(r => r is not PassThroughServiceEndPointResolverProvider) - .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _logger = resolverLogger; - private readonly TimeProvider _timeProvider = timeProvider; - private readonly IOptions _options = options; - - /// - /// Creates a service endpoint resolver for the provided service name. - /// - public ServiceEndPointWatcher CreateWatcher(string serviceName) - { - ArgumentNullException.ThrowIfNull(serviceName); - - if (!ServiceEndPointQuery.TryParse(serviceName, out var query)) - { - throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); - } - - List? resolvers = null; - foreach (var factory in _resolverProviders) - { - if (factory.TryCreateProvider(query, out var resolver)) - { - resolvers ??= []; - resolvers.Add(resolver); - } - } - - if (resolvers is not { Count: > 0 }) - { - throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); - } - - Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointWatcher( - resolvers: [.. resolvers], - logger: _logger, - serviceName: serviceName, - timeProvider: _timeProvider, - options: _options); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs index 1a14cb961b7..947f24b2f81 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs @@ -9,9 +9,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// A mutable collection of service endpoints. /// -internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +internal sealed class ServiceEndpointBuilder : IServiceEndpointBuilder { - private readonly List _endPoints = new(); + private readonly List _endpoints = new(); private readonly List _changeTokens = new(); private readonly FeatureCollection _features = new FeatureCollection(); @@ -32,15 +32,15 @@ public void AddChangeToken(IChangeToken changeToken) /// /// Gets the endpoints. /// - public IList EndPoints => _endPoints; + public IList Endpoints => _endpoints; /// - /// Creates a from the provided instance. + /// Creates a from the provided instance. /// /// The service endpoint source. - public ServiceEndPointSource Build() + public ServiceEndpointSource Build() { - return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + return new ServiceEndpointSource(_endpoints, new CompositeChangeToken(_changeTokens), _features); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 029d2601243..92df120940d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -9,13 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves service names to collections of endpoints. /// -public sealed class ServiceEndPointResolver : IAsyncDisposable +public sealed class ServiceEndpointResolver : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverProvider; + private readonly ServiceEndpointWatcherFactory _watcherFactory; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -23,13 +23,13 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The resolver factory. + /// The watcher factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, TimeProvider timeProvider) { - _resolverProvider = resolverProvider; + _watcherFactory = watcherFactory; _timeProvider = timeProvider; } @@ -39,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, /// The service name. /// A . /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + public async ValueTask GetEndpointsAsync(string serviceName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(serviceName); ObjectDisposedException.ThrowIf(_disposed, this); @@ -54,7 +54,7 @@ public async ValueTask GetEndPointsAsync(string serviceNa static (name, self) => self.CreateResolver(name), this); - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var (valid, result) = await resolver.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); if (valid) { if (result is null) @@ -156,21 +156,21 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateWatcher(serviceName); + var resolver = _watcherFactory.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } - private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable + private sealed class ResolverEntry(ServiceEndpointWatcher watcher) : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver = resolver; + private readonly ServiceEndpointWatcher _watcher = watcher; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -181,17 +181,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpointSource? Endpoints)> GetEndpointsAsync(CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); + var endpoints = await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endpoints); } else { @@ -237,7 +237,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index 78a8f84b556..fce9f667b40 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -5,35 +5,35 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcher +partial class ServiceEndpointWatcher { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] - public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] + public static partial void ResolvingEndpoints(ILogger logger, string serviceName); [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] public static partial void ResolutionPending(ILogger logger, string serviceName); - [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] - public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); + [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {Endpoints}.", EventName = "ResolutionSucceeded")] + public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endpoints); - public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndpointSource endpointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endpointSource.Endpoints.Count, serviceName, string.Join(", ", endpointSource.Endpoints.Select(GetEndpointString))); } - static string GetEndPointString(ServiceEndPoint ep) + static string GetEndpointString(ServiceEndpoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } provider) { - return $"{ep.GetEndPointString()} ({resolver})"; + return $"{ep} ({provider})"; } - return ep.GetEndPointString(); - } + return ep.ToString()!; + } } [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index 9b1069d31e7..ba6df9b43c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -13,23 +13,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -internal sealed partial class ServiceEndPointWatcher( - IServiceEndPointProvider[] resolvers, +internal sealed partial class ServiceEndpointWatcher( + IServiceEndpointProvider[] providers, ILogger logger, string serviceName, TimeProvider timeProvider, IOptions options) : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); + private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: true); private readonly object _lock = new(); private readonly ILogger _logger = logger; private readonly TimeProvider _timeProvider = timeProvider; private readonly ServiceDiscoveryOptions _options = options.Value; - private readonly IServiceEndPointProvider[] _resolvers = resolvers; + private readonly IServiceEndpointProvider[] _providers = providers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointSource? _cachedEndPoints; + private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -41,14 +41,14 @@ internal sealed partial class ServiceEndPointWatcher( /// /// Gets or sets the action called when endpoints are updated. /// - public Action? OnEndPointsUpdated { get; set; } + public Action? OnEndpointsUpdated { get; set; } /// /// Starts the endpoint resolver. /// public void Start() { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); _ = RefreshAsync(force: false); } @@ -57,27 +57,27 @@ public void Start() /// /// A . /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) { - return new ValueTask(cached); + return new ValueTask(cached); } // Otherwise, ensure the cache is being refreshed // Wait for the cache refresh to complete and return the cached value. - return GetEndPointsInternal(cancellationToken); + return GetEndpointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { - ServiceEndPointSource? result; + ServiceEndpointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; + result = _cachedEndpoints; } while (result is null); return result; } @@ -89,7 +89,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -124,27 +124,27 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointSource? newEndPoints = null; + ServiceEndpointSource? newEndpoints = null; CacheStatus newCacheState; try { - Log.ResolvingEndPoints(_logger, ServiceName); - var builder = new ServiceEndPointBuilder(); - foreach (var resolver in _resolvers) + Log.ResolvingEndpoints(_logger, ServiceName); + var builder = new ServiceEndpointBuilder(); + foreach (var provider in _providers) { - await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } - var endPoints = builder.Build(); + var endpoints = builder.Build(); newCacheState = CacheStatus.Valid; lock (_lock) { // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) + if (endpoints.ChangeToken.ActiveChangeCallbacks) { // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); if (_pollingTimer is { } timer) { _pollingTimer = null; @@ -157,7 +157,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = endPoints; + newEndpoints = endpoints; newCacheState = CacheStatus.Valid; } } @@ -171,26 +171,26 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // To ensure coherence between the value returned by calls made to GetEndpointsAsync and value passed to the callback, // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // before receiving the updated value. An alternative approach is to lock access to _cachedEndpoints, but // that will have more overhead in the common case. if (newCacheState is CacheStatus.Valid) { - Interlocked.Exchange(ref _cachedEndPoints, null); + Interlocked.Exchange(ref _cachedEndpoints, null); } - if (OnEndPointsUpdated is { } callback) + if (OnEndpointsUpdated is { } callback) { - callback(new(newEndPoints, error)); + callback(new(newEndpoints, error)); } lock (_lock) { if (newCacheState is CacheStatus.Valid) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + Debug.Assert(newEndpoints is not null); + _cachedEndpoints = newEndpoints; } _cacheState = newCacheState; @@ -201,9 +201,9 @@ private async Task RefreshAsyncInternal() Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (newEndPoints is not null) + else if (newEndpoints is not null) { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndpoints); } } @@ -240,9 +240,9 @@ public async ValueTask DisposeAsync() await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } - foreach (var resolver in _resolvers) + foreach (var provider in _providers) { - await resolver.DisposeAsync().ConfigureAwait(false); + await provider.DisposeAsync().ConfigureAwait(false); } } @@ -253,14 +253,14 @@ private enum CacheStatus Valid } - private void ThrowIfNoResolvers() + private void ThrowIfNoProviders() { - if (_resolvers.Length == 0) + if (_providers.Length == 0) { - ThrowNoResolversConfigured(); + ThrowNoProvidersConfigured(); } } [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); + private static void ThrowNoProvidersConfigured() => throw new InvalidOperationException("No service endpoint providers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 69f565eb8e3..5f4acc89874 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -5,17 +5,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcherFactory +partial class ServiceEndpointWatcherFactory { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] - public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); + + public static void CreatingResolver(ILogger logger, string serviceName, List providers) { if (logger.IsEnabled(LogLevel.Debug)) { - ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); + ServiceEndpointProviderListCore(logger, serviceName, providers.Count, string.Join(", ", providers.Select(static r => r.ToString()))); } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs new file mode 100644 index 00000000000..6cc7cb2cbc5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates service endpoint watchers. +/// +internal sealed partial class ServiceEndpointWatcherFactory( + IEnumerable providerFactories, + ILogger logger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndpointProviderFactory[] _providerFactories = providerFactories + .Where(r => r is not PassThroughServiceEndpointProviderFactory) + .Concat(providerFactories.Where(static r => r is PassThroughServiceEndpointProviderFactory)).ToArray(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a service endpoint watcher for the provided service name. + /// + public ServiceEndpointWatcher CreateWatcher(string serviceName) + { + ArgumentNullException.ThrowIfNull(serviceName); + + if (!ServiceEndpointQuery.TryParse(serviceName, out var query)) + { + throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); + } + + List? providers = null; + foreach (var factory in _providerFactories) + { + if (factory.TryCreateProvider(query, out var provider)) + { + providers ??= []; + providers.Add(provider); + } + } + + if (providers is not { Count: > 0 }) + { + throw new InvalidOperationException($"No provider which supports the provided service name, '{serviceName}', has been configured."); + } + + Log.CreatingResolver(_logger, serviceName, providers); + return new ServiceEndpointWatcher( + providers: [.. providers], + logger: _logger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs similarity index 72% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 25cd88a1436..7cadb4e3c7f 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for and . +/// These also cover and by extension. /// -public class DnsSrvServiceEndPointResolverTests +public class DnsSrvServiceEndpointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndPoint_Dns() + public async Task ResolveServiceEndpoint_Dns() { var dnsClientMock = new FakeDnsClient { @@ -101,26 +101,26 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.EndPoints; + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -134,7 +134,7 @@ public async Task ResolveServiceEndPoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -175,28 +175,28 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => + .AddDnsSrvServiceEndpointProvider(options => { options.QuerySuffix = ".ns"; - options.ApplyHostNameMetadata = _ => true; + options.ShouldApplyHostNameMetadata = _ => true; }) - .AddConfigurationServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider(); } else { serviceCollection - .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + .AddConfigurationServiceEndpointProvider() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); @@ -205,13 +205,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.EndPoints; + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -221,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo else { // We expect only the results from the Configuration provider. - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -248,59 +248,4 @@ public void SetValues(IEnumerable> values) OnReload(); } } - - /* - [Fact] - public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() - { - var oneEndPoint = new Dictionary - { - ["services:basket:http:0:host"] = "localhost", - ["services:basket:http:0:port"] = "8080", - }; - var bothEndPoints = new Dictionary(oneEndPoint) - { - ["services:basket:http:1:host"] = "remotehost", - ["services:basket:http:1:port"] = "9090", - }; - var configSource = new MyConfigurationProvider(); - var services = new ServiceCollection() - .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) - .AddServiceDiscovery() - .AddConfigurationServiceEndPointResolver() - .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) - { - Assert.NotNull(resolver); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); - resolver.Start(); - var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialResult); - Assert.False(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); - Assert.Null(initialResult.EndPoints); - - // Update the config and check that it flows through the system. - configSource.SetValues(oneEndPoint); - - // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to - // cause an indefinite test hang. We expect the result to be published practically immediately, though. - _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); - var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - var firstEp = Assert.Single(oneEpResult); - Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); - - // Do it again to check that an updated (not cached) version is published. - configSource.SetValues(bothEndPoints); - var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.True(twoEpResult.ResolvedSuccessfully); - Assert.Equal(2, twoEpResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); - } - } - */ } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6d8091f026c..db720782107 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -12,13 +12,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class ConfigurationServiceEndPointResolverTests +public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -27,23 +27,23 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -52,7 +52,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() { // Try to resolve an http endpoint when only https is allowed. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary @@ -63,59 +63,63 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .Configure(o => o.AllowedSchemes = ["https"]) + .AddConfigurationServiceEndpointProvider() + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Empty(initialResult.EndPointSource.EndPoints); + Assert.Empty(initialResult.EndpointSource.Endpoints); } // Specifying either https or http. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } // Specifying either https or http, but in reverse. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { var configSource = new MemoryConfigurationSource { @@ -129,24 +133,24 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) + .AddConfigurationServiceEndpointProvider(options => options.ShouldApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -155,20 +159,20 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. - await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -178,7 +182,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() { var configSource = new MemoryConfigurationSource { @@ -196,25 +200,25 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndpointSource.Endpoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -223,7 +227,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() { var configSource = new MemoryConfigurationSource { @@ -241,29 +245,29 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndpointSource.Endpoints[1].EndPoint); // We expect the HTTPS endpoint back but not the HTTP one. - Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index 643bbfad441..e0af5c03ed4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -12,36 +12,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class PassThroughServiceEndPointResolverTests +public class PassThroughServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_PassThrough() + public async Task ResolveServiceEndpoint_PassThrough() { var services = new ServiceCollection() .AddServiceDiscoveryCore() - .AddPassThroughServiceEndPointResolver() + .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Superseded() + public async Task ResolveServiceEndpoint_Superseded() { var configSource = new MemoryConfigurationSource { @@ -55,26 +55,26 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Fallback() + public async Task ResolveServiceEndpoint_Fallback() { var configSource = new MemoryConfigurationSource { @@ -88,27 +88,27 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndpointSource.Endpoints[0].EndPoint); } } // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. [Fact] - public async Task ResolveServiceEndPoint_Fallback_NoScheme() + public async Task ResolveServiceEndpoint_Fallback_NoScheme() { var configSource = new MemoryConfigurationSource { @@ -123,8 +123,8 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); - var result = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); + var resolver = services.GetRequiredService(); + var result = await resolver.GetEndpointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.Endpoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs similarity index 62% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index f5e506a9b72..16950e67374 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -15,58 +15,58 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// -public class ServiceEndPointResolverTests +public class ServiceEndpointResolverTests { [Fact] - public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + public void ResolveServiceEndpoint_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket")); - Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); + Assert.Equal("No provider which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] - public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + public async Task ServiceEndpointResolver_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); - var exception = Assert.Throws(resolverFactory.Start); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); - exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + var watcher = new ServiceEndpointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); + var exception = Assert.Throws(watcher.Start); + Assert.Equal("No service endpoint providers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await watcher.GetEndpointsAsync()); + Assert.Equal("No service endpoint providers are configured.", exception.Message); } [Fact] - public void ResolveServiceEndPoint_NullServiceName_Throws() + public void ResolveServiceEndpoint_NullServiceName_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task AddServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoProviders_Throws() { var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery(); var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); - Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); + Assert.Equal("No provider which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory + private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(query); @@ -74,58 +74,58 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou } } - private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndpointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) => resolveAsync(endpoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } [Fact] - public async Task ResolveServiceEndPoint() + public async Task ResolveServiceEndpoint() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); - var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + Assert.Equal(2, resolverResult.EndpointSource.Endpoints.Count); + var endpoints = resolverResult.EndpointSource.Endpoints.Select(ep => ep.EndPoint).OfType().ToList(); endpoints.Sort((l, r) => l.Port - r.Port); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); @@ -133,34 +133,34 @@ public async Task ResolveServiceEndPoint() } [Fact] - public async Task ResolveServiceEndPointOneShot() + public async Task ResolveServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -169,36 +169,36 @@ public async Task ResolveServiceEndPointOneShot() } [Fact] - public async Task ResolveHttpServiceEndPointOneShot() + public async Task ResolveHttpServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var fakeResolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndpointResolver(resolverProvider, services, TimeProvider.System); Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); - var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(endPoint); - var ip = Assert.IsType(endPoint.EndPoint); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endpoint); + var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -206,12 +206,12 @@ public async Task ResolveHttpServiceEndPointOneShot() } [Fact] - public async Task ResolveServiceEndPoint_ThrowOnReload() + public async Task ResolveServiceEndpoint_ThrowOnReload() { var sem = new SemaphoreSlim(0); var cts = new[] { new CancellationTokenSource() }; var throwOnNextResolve = new[] { false }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: async (collection, ct) => { await sem.WaitAsync(ct).ConfigureAwait(false); @@ -228,25 +228,25 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); - var initialEndPoints = await initialEndPointsTask; - Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints.EndPoints); + var initialEndpoints = await initialEndpointsTask; + Assert.NotNull(initialEndpoints); + Assert.Single(initialEndpoints.Endpoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -254,21 +254,21 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var exception = await Assert.ThrowsAsync(async () => { - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); }).ConfigureAwait(false); Assert.Equal("throwing", exception.Message); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + var channel = Channel.CreateUnbounded(); + watcher.OnEndpointsUpdated = result => channel.Writer.TryWrite(result); do { cts[0].Cancel(); sem.Release(1); - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); await resolveTask.ConfigureAwait(false); var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); if (next.ResolvedSuccessfully) @@ -277,11 +277,11 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } } while (true); - var task = resolver.GetEndPointsAsync(CancellationToken.None); + var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var result = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, result); - var sep = Assert.Single(result.EndPoints); + Assert.NotSame(initialEndpoints, result); + var sep = Assert.Single(result.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); From 95ea2e8b4c758aaa1887858b9050f7257ab41bb2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:06:14 -0700 Subject: [PATCH 039/472] Update README.md to reflect Service Discovery API changes (#3228) --- .../README.md | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 04119540e10..6c47ee67507 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -51,7 +51,7 @@ Add service discovery to an individual `IHttpClientBuilder` by calling the `AddS ```csharp builder.Services.AddHttpClient(c => { - c.BaseAddress = new("http://catalog")); + c.BaseAddress = new("https://catalog")); }).AddServiceDiscovery(); ``` @@ -71,20 +71,22 @@ The `AddServiceDiscovery` extension method adds a configuration-based endpoint p This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. -Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: +Here is an example demonstrating how to configure endpoints for the service named _catalog_ via `appsettings.json`: ```json { "Services": { - "catalog": [ - "localhost:8080", - "10.46.24.90:80", + "catalog": { + "https": [ + "https://localhost:8443", + "https://10.46.24.90:443" ] } + } } ``` -The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. +The above example adds two endpoints for the service named _catalog_: `https://localhost:8443`, and `"https://10.46.24.90:443"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. @@ -117,6 +119,25 @@ builder.Services.Configure(options This example demonstrates setting a custom section name for your service endpoints and providing a custom logic for applying host name metadata based on a condition. +## Scheme selection when resolving HTTP(S) endpoints + +It is common to use HTTP while developing and testing a service locally and HTTPS when the service is deployed. Service Discovery supports this by allowing for a priority list of URI schemes to be specified in the input string given to Service Discovery. Service Discovery will attempt to resolve the services for the schemes in order and will stop after an endpoint is found. URI schemes are separated by a `+` character, for example: `"https+http://basket"`. Service Discovery will first try to find HTTPS endpoints for the `"basket"` service and will then fall back to HTTP endpoints. If any HTTPS endpoint is found, Service Discovery will not include HTTP endpoints. +Schemes can be filtered by configuring the `AllowedSchemes` and `AllowAllSchemes` properties on `ServiceDiscoveryOptions`. The `AllowAllSchemes` property is used to indicate that all schemes are allowed. By default, `AllowAllSchemes` is `true` and all schemes are allowed. Schemes can be restricted by setting `AllowAllSchemes` to `false` and adding allowed schemes to the `AllowedSchemes` property. For example, to allow only HTTPS: + +```csharp +services.Configure(options => +{ + options.AllowAllSchemes = false; + options.AllowedSchemes = ["https"]; +}); +``` + +To explicitly allow all schemes, set the `ServiceDiscoveryOptions.AllowAllSchemes` property to `true`: + +```csharp +services.Configure(options => options.AllowAllSchemes = true); +``` + ## Resolving service endpoints using platform-provided service discovery Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. @@ -129,25 +150,6 @@ If service discovery was added to the host using the `AddServiceDiscoveryCore` e In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". -## Load-balancing with endpoint selectors - -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: - -```csharp -builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://catalog")); - .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); -``` - -The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: - -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` - -Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. - ## Service discovery in .NET Aspire .NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. @@ -169,13 +171,13 @@ In the above example, the _frontend_ project references the _catalog_ project an ## Named endpoints -Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `http://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `http://_dashboard.basket` can be used to specify this endpoint, for example: +Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `scheme://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `https+http://_dashboard.basket` can be used to specify this endpoint, for example: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://basket")); + static client => client.BaseAddress = new("https+http://basket")); builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://_dashboard.basket")); + static client => client.BaseAddress = new("https+http://_dashboard.basket")); ``` In the above example, two `HttpClient`s are added: one for the core basket service and one for the basket service's dashboard. @@ -187,10 +189,10 @@ With the configuration-based endpoint provider, named endpoints can be specified ```json { "Services": { - "basket": [ - "10.2.3.4:8080", /* the default endpoint, when resolving http://basket */ - "_dashboard.10.2.3.4:9999" /* the "dashboard" endpoint, resolved via http://_dashboard.basket */ - ] + "basket": + "https": "https://10.2.3.4:8080", /* the https endpoint, requested via https://basket */ + "dashboard": "https://10.2.3.4:9999" /* the "dashboard" endpoint, requested via https://_dashboard.basket */ + } } } ``` @@ -203,7 +205,7 @@ With the configuration-based endpoint provider, named endpoints can be specified var builder = DistributedApplication.CreateBuilder(args); var basket = builder.AddProject("basket") - .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); + .WithEndpoint(hostPort: 9999, scheme: "https", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") .WithReference(basket.GetEndpoint("admin")); @@ -219,7 +221,7 @@ In the above example, the "basket" service exposes an "admin" endpoint in additi // The preceding code is the same as in the above sample var frontend = builder.AddProject("frontend") - .WithReference(basket.GetEndpoint("http")); + .WithReference(basket.GetEndpoint("https")); ``` ### Named endpoints in Kubernetes using DNS SRV @@ -249,20 +251,20 @@ builder.Services.AddServiceDiscoveryCore(); builder.Services.AddDnsSrvServiceEndpointProvider(); ``` -The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. +The special port name "default" is used to specify the default endpoint, resolved using the URI `https://basket`. As in the previous example, add service discovery to an `HttpClient` for the basket service: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://basket")); + static client => client.BaseAddress = new("https://basket")); ``` Similarly, the "dashboard" endpoint can be targeted as follows: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://_dashboard.basket")); + static client => client.BaseAddress = new("https://_dashboard.basket")); ``` ### Named endpoints in Azure Container Apps From 616f098cf88b26920710a6a596042ba454711ed7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:37:57 +1000 Subject: [PATCH 040/472] [release/8.0] Service Discovery: Implement approved API (#3460) * Rename EndPoint to Endpoint, resolver to provider * Apply changes decided during API review * Find & fix straggler file names * Variables and members assignable to System.Net.EndPoint use upper-case P * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs Co-authored-by: Stephen Halter * Delete GetEndpointString() --------- Co-authored-by: Reuben Bond Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Co-authored-by: Stephen Halter --- .../IServiceEndPointProviderFactory.cs | 20 -- ...tBuilder.cs => IServiceEndpointBuilder.cs} | 8 +- ...rovider.cs => IServiceEndpointProvider.cs} | 6 +- .../IServiceEndpointProviderFactory.cs | 20 ++ ...EndPointImpl.cs => ServiceEndpointImpl.cs} | 8 +- .../README.md | 2 +- ...{ServiceEndPoint.cs => ServiceEndpoint.cs} | 21 +-- ...dPointQuery.cs => ServiceEndpointQuery.cs} | 35 ++-- ...ointSource.cs => ServiceEndpointSource.cs} | 16 +- .../DnsServiceEndPointResolverProvider.cs | 21 --- ...olver.cs => DnsServiceEndpointProvider.cs} | 28 +-- ... => DnsServiceEndpointProviderBase.Log.cs} | 2 +- ...e.cs => DnsServiceEndpointProviderBase.cs} | 36 ++-- .../DnsServiceEndpointProviderFactory.cs | 21 +++ ...s => DnsServiceEndpointProviderOptions.cs} | 6 +- ...er.cs => DnsSrvServiceEndpointProvider.cs} | 34 ++-- ...> DnsSrvServiceEndpointProviderFactory.cs} | 19 +- ...> DnsSrvServiceEndpointProviderOptions.cs} | 6 +- .../README.md | 16 +- ...DiscoveryDnsServiceCollectionExtensions.cs | 33 +++- .../ServiceDiscoveryDestinationResolver.cs | 10 +- ...nfigurationServiceEndpointProvider.Log.cs} | 12 +- ...> ConfigurationServiceEndpointProvider.cs} | 64 +++---- ...gurationServiceEndpointProviderFactory.cs} | 12 +- ...erviceEndpointProviderOptionsValidator.cs} | 10 +- ...gurationServiceEndpointProviderOptions.cs} | 6 +- ...lver.cs => HttpServiceEndpointResolver.cs} | 46 ++--- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 2 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 20 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 4 +- ...lt.cs => ServiceEndpointResolverResult.cs} | 8 +- ...elector.cs => IServiceEndpointSelector.cs} | 10 +- ...s => RoundRobinServiceEndpointSelector.cs} | 12 +- ...PassThroughServiceEndpointProvider.Log.cs} | 4 +- ... => PassThroughServiceEndpointProvider.cs} | 14 +- ...sThroughServiceEndpointProviderFactory.cs} | 20 +- .../README.md | 76 ++++---- ...iceDiscoveryHttpClientBuilderExtensions.cs | 4 +- .../ServiceDiscoveryOptions.cs | 23 +-- ...iceDiscoveryServiceCollectionExtensions.cs | 40 ++-- .../ServiceEndPointWatcherFactory.cs | 61 ------ ...ntBuilder.cs => ServiceEndpointBuilder.cs} | 12 +- ...Resolver.cs => ServiceEndpointResolver.cs} | 36 ++-- ...r.Log.cs => ServiceEndpointWatcher.Log.cs} | 24 +-- ...ntWatcher.cs => ServiceEndpointWatcher.cs} | 80 ++++---- ...s => ServiceEndpointWatcherFactory.Log.cs} | 11 +- .../ServiceEndpointWatcherFactory.cs | 61 ++++++ ... => DnsSrvServiceEndpointResolverTests.cs} | 125 ++++-------- ...figurationServiceEndpointResolverTests.cs} | 178 +++++++++--------- ...assThroughServiceEndpointResolverTests.cs} | 74 ++++---- ...sts.cs => ServiceEndpointResolverTests.cs} | 150 +++++++-------- 52 files changed, 763 insertions(+), 810 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointBuilder.cs => IServiceEndpointBuilder.cs} (80%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointProvider.cs => IServiceEndpointProvider.cs} (75%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/{ServiceEndPointImpl.cs => ServiceEndpointImpl.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPoint.cs => ServiceEndpoint.cs} (52%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointQuery.cs => ServiceEndpointQuery.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointSource.cs => ServiceEndpointSource.cs} (74%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.cs => DnsServiceEndpointProvider.cs} (61%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.Log.cs => DnsServiceEndpointProviderBase.Log.cs} (97%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.cs => DnsServiceEndpointProviderBase.cs} (81%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverOptions.cs => DnsServiceEndpointProviderOptions.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolver.cs => DnsSrvServiceEndpointProvider.cs} (71%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverProvider.cs => DnsSrvServiceEndpointProviderFactory.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverOptions.cs => DnsSrvServiceEndpointProviderOptions.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.Log.cs => ConfigurationServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.cs => ConfigurationServiceEndpointProvider.cs} (76%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverProvider.cs => ConfigurationServiceEndpointProviderFactory.cs} (58%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptionsValidator.cs => ConfigurationServiceEndpointProviderOptionsValidator.cs} (62%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndpointProviderOptions.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/{HttpServiceEndPointResolver.cs => HttpServiceEndpointResolver.cs} (82%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/{ServiceEndPointResolverResult.cs => ServiceEndpointResolverResult.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{IServiceEndPointSelector.cs => IServiceEndpointSelector.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{RoundRobinServiceEndPointSelector.cs => RoundRobinServiceEndpointSelector.cs} (63%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.Log.cs => PassThroughServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.cs => PassThroughServiceEndpointProvider.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolverProvider.cs => PassThroughServiceEndpointProviderFactory.cs} (70%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointBuilder.cs => ServiceEndpointBuilder.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.cs => ServiceEndpointResolver.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.Log.cs => ServiceEndpointWatcher.Log.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.cs => ServiceEndpointWatcher.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcherFactory.Log.cs => ServiceEndpointWatcherFactory.Log.cs} (53%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsSrvServiceEndPointResolverTests.cs => DnsSrvServiceEndpointResolverTests.cs} (72%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ConfigurationServiceEndPointResolverTests.cs => ConfigurationServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{PassThroughServiceEndPointResolverTests.cs => PassThroughServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ServiceEndPointResolverTests.cs => ServiceEndpointResolverTests.cs} (62%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs deleted file mode 100644 index 4b1876f808e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs +++ /dev/null @@ -1,20 +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.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates instances. -/// -public interface IServiceEndPointProviderFactory -{ - /// - /// Tries to create an instance for the specified . - /// - /// The service to create the resolver for. - /// The resolver. - /// if the resolver was created, otherwise. - bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs similarity index 80% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs index 468adea1c09..e051b2bf746 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs @@ -7,14 +7,14 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Builder to create a instances. +/// Builder to create a instances. /// -public interface IServiceEndPointBuilder +public interface IServiceEndpointBuilder { /// /// Gets the endpoints. /// - IList EndPoints { get; } + IList Endpoints { get; } /// /// Gets the feature collection. @@ -22,7 +22,7 @@ public interface IServiceEndPointBuilder IFeatureCollection Features { get; } /// - /// Adds a change token to the resulting . + /// Adds a change token to the resulting . /// /// The change token. void AddChangeToken(IChangeToken changeToken); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs index 950823257af..4a192180b66 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. /// -public interface IServiceEndPointProvider : IAsyncDisposable +public interface IServiceEndpointProvider : IAsyncDisposable { /// /// Resolves the endpoints for the service. /// - /// The endpoint collection, which resolved endpoints will be added to. + /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..009cbf05d76 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates instances. +/// +public interface IServiceEndpointProviderFactory +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the provider for. + /// The provider. + /// if the provider was created, otherwise. + bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 7d135dfe97d..8bfb50fe930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -6,9 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint +internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); - public override string? ToString() => GetEndPointString(); + public override string? ToString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + _ => EndPoint.ToString()! + }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md index c5cf6b9bc78..0d97211313e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -1,6 +1,6 @@ # Microsoft.Extensions.ServiceDiscovery.Abstractions -The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint providers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). ## Feedback & contributing diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs similarity index 52% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index a3cde62ce0d..238e383a957 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -11,8 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. /// -[DebuggerDisplay("{GetEndPointString(),nq}")] -public abstract class ServiceEndPoint +public abstract class ServiceEndpoint { /// /// Gets the endpoint. @@ -25,21 +23,10 @@ public abstract class ServiceEndPoint public abstract IFeatureCollection Features { get; } /// - /// Creates a new . + /// Creates a new . /// /// The endpoint being represented. /// Features of the endpoint. - /// A newly initialized . - public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); - - /// - /// Gets a string representation of the . - /// - /// A string representation of the . - public virtual string GetEndPointString() => EndPoint switch - { - DnsEndPoint dns => $"{dns.Host}:{dns.Port}", - IPEndPoint ip => ip.ToString(), - _ => EndPoint.ToString()! - }; + /// A newly initialized . + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 99c92cce27c..600dc5cc28c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -8,21 +8,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Describes a query for endpoints of a service. /// -public sealed class ServiceEndPointQuery +public sealed class ServiceEndpointQuery { + private readonly string _originalString; + /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The string which the query was constructed from. /// The ordered list of included URI schemes. /// The service name. - /// The optional endpoint name. - private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName) + /// The optional endpoint name. + private ServiceEndpointQuery(string originalString, string[] includedSchemes, string serviceName, string? endpointName) { - OriginalString = originalString; - IncludeSchemes = includedSchemes; + _originalString = originalString; + IncludedSchemes = includedSchemes; ServiceName = serviceName; - EndPointName = endPointName; + EndpointName = endpointName; } /// @@ -31,7 +33,7 @@ private ServiceEndPointQuery(string originalString, string[] includedSchemes, st /// The value to parse. /// The resulting query. /// if the value was successfully parsed; otherwise . - public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query) + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) @@ -52,10 +54,10 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin var uriHost = uri.Host; var segmentSeparatorIndex = uriHost.IndexOf('.'); string host; - string? endPointName = null; + string? endpointName = null; if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') { - endPointName = uriHost[1..segmentSeparatorIndex]; + endpointName = uriHost[1..segmentSeparatorIndex]; // Skip the endpoint name, including its prefix ('_') and suffix ('.'). host = uriHost[(segmentSeparatorIndex + 1)..]; @@ -67,24 +69,19 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". var schemes = hasScheme ? uri.Scheme.Split('+') : []; - query = new(input, schemes, host, endPointName); + query = new(input, schemes, host, endpointName); return true; } - /// - /// Gets the string which the query was constructed from. - /// - public string OriginalString { get; } - /// /// Gets the ordered list of included URI schemes. /// - public IReadOnlyList IncludeSchemes { get; } + public IReadOnlyList IncludedSchemes { get; } /// /// Gets the endpoint name, or if no endpoint name is specified. /// - public string? EndPointName { get; } + public string? EndpointName { get; } /// /// Gets the service name. @@ -92,6 +89,6 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin public string ServiceName { get; } /// - public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}"; + public override string? ToString() => _originalString; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs similarity index 74% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index 807981226e3..fb5bff1b288 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -11,18 +11,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public sealed class ServiceEndPointSource +[DebuggerTypeProxy(typeof(ServiceEndpointCollectionDebuggerView))] +public sealed class ServiceEndpointSource { - private readonly List? _endpoints; + private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); @@ -34,7 +34,7 @@ public ServiceEndPointSource(List? endpoints, IChangeToken chan /// /// Gets the endpoints. /// - public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; + public IReadOnlyList Endpoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -57,13 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) + private sealed class ServiceEndpointCollectionDebuggerView(ServiceEndpointSource value) { public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); + public ServiceEndpoint[] Endpoints => value.Endpoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs deleted file mode 100644 index 51525663a03..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ /dev/null @@ -1,21 +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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Dns; - -internal sealed partial class DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : IServiceEndPointProviderFactory -{ - /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) - { - resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); - return true; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index a2601c84b45..6cc9f92bc46 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -7,12 +7,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsServiceEndpointProvider( + ServiceEndpointQuery query, string hostName, - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; @@ -26,27 +26,27 @@ internal sealed partial class DnsServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - endPoints.Add(serviceEndPoint); + endpoints.Add(serviceEndpoint); } - if (endPoints.Count == 0) + if (endpoints.Count == 0) { - throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); + throw new InvalidOperationException($"No DNS records were found for service '{ServiceName}' (DNS name: '{hostName}')."); } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs index cd664215aa9..29aaaf8e930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndpointProviderBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 9d6c54e4755..6c69cc7a760 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -7,9 +7,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// A service end point resolver that uses DNS to resolve the service end points. +/// A service end point provider that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider +internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpointProvider { private readonly object _lock = new(); private readonly ILogger _logger; @@ -20,23 +20,23 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; + private List? _lastEndpointCollection; private TimeSpan _nextRefreshPeriod; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. + /// The service name. /// The logger. /// The time provider. - protected DnsServiceEndPointResolverBase( - string serviceName, + protected DnsServiceEndpointProviderBase( + ServiceEndpointQuery query, ILogger logger, TimeProvider timeProvider) { - ServiceName = serviceName; + ServiceName = query.ToString()!; _logger = logger; - _lastEndPointCollection = null; + _lastEndpointCollection = null; _timeProvider = timeProvider; _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); @@ -58,10 +58,10 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public async ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. - if (endPoints.EndPoints.Count != 0) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return; @@ -85,28 +85,28 @@ public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, Cancella lock (_lock) { - if (_lastEndPointCollection is { Count: > 0 } eps) + if (_lastEndpointCollection is { Count: > 0 } eps) { foreach (var ep in eps) { - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } - endPoints.AddChangeToken(_lastChangeToken); + endpoints.AddChangeToken(_lastChangeToken); return; } } - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + private bool ShouldRefresh() => _lastEndpointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; protected abstract Task ResolveAsyncCore(); - protected void SetResult(List endPoints, TimeSpan validityPeriod) + protected void SetResult(List endpoints, TimeSpan validityPeriod) { lock (_lock) { - if (endPoints is { Count: > 0 }) + if (endpoints is { Count: > 0 }) { _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); _nextRefreshPeriod = DefaultRefreshPeriod; @@ -131,7 +131,7 @@ protected void SetResult(List endPoints, TimeSpan validityPerio _lastCollectionCancellation.Cancel(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; + _lastEndpointCollection = endpoints; } TimeSpan GetRefreshPeriod() diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..c241ad89dd3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndpointProviderFactory +{ + /// + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) + { + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs index 665c98bbc09..b163afc76ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsServiceEndPointResolverOptions +public class DnsServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -31,5 +31,5 @@ public class DnsServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs similarity index 71% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index d59dbfbb69c..dd17a7e2732 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -9,14 +9,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsSrvServiceEndpointProvider( + ServiceEndpointQuery query, string srvQuery, string hostName, - IOptionsMonitor options, - ILogger logger, + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); @@ -59,15 +59,15 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); + endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) { @@ -79,22 +79,22 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) { var msg = errorMessage switch { - { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", - _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", + _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." }; return new InvalidOperationException(msg); } - ServiceEndPoint CreateEndPoint(EndPoint endPoint) + ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 8a75c1d1bbf..fd0cb28353d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -8,11 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, +internal sealed partial class DnsSrvServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointProviderFactory + TimeProvider timeProvider) : IServiceEndpointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -20,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md @@ -30,17 +30,16 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". - var serviceName = query.OriginalString; if (string.IsNullOrWhiteSpace(_querySuffix)) { - DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); - resolver = default; + DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); + provider = default; return false; } - var portName = query.EndPointName ?? "default"; + var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index 704e03cd9ca..c908c56d770 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsSrvServiceEndPointResolverOptions +public class DnsSrvServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -39,5 +39,5 @@ public class DnsSrvServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md index d3fbe2a75e5..8be4560870b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -1,25 +1,25 @@ # Microsoft.Extensions.ServiceDiscovery.Dns -This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint providers: -- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. - _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). ## Resolving service endpoints with DNS -The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +The _DNS_ service endpoint provider resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS service endpoint provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. -To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: +To configure the DNS service endpoint provider in your application, add the DNS service endpoint provider to your host builder's service collection using the `AddDnsServiceEndpointProvider` method. service discovery as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsServiceEndPointResolver(); +builder.Services.AddDnsServiceEndpointProvider(); ``` ## Resolving service endpoints in Kubernetes with DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -37,11 +37,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 0d795660fd2..17544d09486 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -14,6 +14,17 @@ namespace Microsoft.Extensions.Hosting; /// public static class ServiceDiscoveryDnsServiceCollectionExtensions { + /// + /// Adds DNS SRV service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + /// /// Adds DNS SRV service discovery to the . /// @@ -24,16 +35,26 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } + /// + /// Adds DNS service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + /// /// Adds DNS service discovery to the . /// @@ -43,11 +64,11 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 22d7e6d8327..8ec810f2c05 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,14 +54,14 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count); + var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(result.Endpoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; - foreach (var endPoint in result.EndPoints) + foreach (var endpoint in result.Endpoints) { - var addressString = endPoint.GetEndPointString(); + var addressString = endpoint.ToString()!; Uri uri; if (!addressString.Contains("://")) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index fdb61ef59f4..48c922a85b3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed partial class ConfigurationServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndpointProvider { private sealed partial class Log { @@ -19,10 +19,10 @@ private sealed partial class Log [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); - [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] - internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); + [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndpoints}.", EventName = "ConfiguredEndpoints")] + internal static partial void ConfiguredEndpoints(ILogger logger, string serviceName, string path, string configuredEndpoints); - internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) + internal static void ConfiguredEndpoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -40,8 +40,8 @@ internal static void ConfiguredEndPoints(ILogger logger, string serviceName, str endpointValues.Append(endpoints[i].ToString()); } - var configuredEndPoints = endpointValues.ToString(); - ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + var configuredEndpoints = endpointValues.ToString(); + ConfiguredEndpoints(logger, serviceName, path, configuredEndpoints); } [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs similarity index 76% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 9604ec0c201..37078e01969 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -10,36 +10,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// A service endpoint resolver that uses configuration to resolve resolved. +/// A service endpoint provider that uses configuration to resolve resolved. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature +internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEndpointProvider, IHostNameFeature { - private const string DefaultEndPointName = "default"; + private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; private readonly string[] _schemes; private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IOptions _options; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The query. /// The configuration. /// The logger. - /// Configuration resolver options. + /// Configuration provider options. /// Service discovery options. - public ConfigurationServiceEndPointResolver( - ServiceEndPointQuery query, + public ConfigurationServiceEndpointProvider( + ServiceEndpointQuery query, IConfiguration configuration, - ILogger logger, - IOptions options, + ILogger logger, + IOptions options, IOptions serviceDiscoveryOptions) { _serviceName = query.ServiceName; - _endpointName = query.EndPointName; - _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); + _endpointName = query.EndpointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -49,10 +49,10 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { // Only add resolved to the collection if a previous provider (eg, an override) did not add them. - if (endPoints.EndPoints.Count != 0) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return default; @@ -62,12 +62,12 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - endPoints.AddChangeToken(_configuration.GetReloadToken()); + endpoints.AddChangeToken(_configuration.GetReloadToken()); Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); return default; } - endPoints.AddChangeToken(section.GetReloadToken()); + endpoints.AddChangeToken(section.GetReloadToken()); // Find an appropriate configuration section based on the input. IConfigurationSection? namedSection = null; @@ -77,7 +77,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (_schemes.Length == 0) { // Use the section named "default". - endpointName = DefaultEndPointName; + endpointName = DefaultEndpointName; namedSection = section.GetSection(endpointName); } else @@ -112,14 +112,14 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo return default; } - List resolved = []; + List resolved = []; Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); // Account for both the single and multi-value cases. if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - AddEndPoint(resolved, namedSection, endpointName); + AddEndpoint(resolved, namedSection, endpointName); } else { @@ -131,7 +131,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - AddEndPoint(resolved, child, endpointName); + AddEndpoint(resolved, child, endpointName); } } @@ -158,13 +158,13 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (index >= 0 && index <= minIndex) { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } else { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } @@ -174,7 +174,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo } else { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); } return default; @@ -182,7 +182,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo string IHostNameFeature.HostName => _serviceName; - private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) + private void AddEndpoint(List endpoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) @@ -190,7 +190,7 @@ private void AddEndPoint(List endPoints, IConfigurationSection throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } - endPoints.Add(CreateEndPoint(endPoint)); + endpoints.Add(CreateEndpoint(endPoint)); } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -220,16 +220,16 @@ private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPo return true; } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + private ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (_options.Value.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } public override string ToString() => "Configuration"; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs similarity index 58% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs index 032c50b6f27..a966cd44794 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs @@ -9,18 +9,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// -internal sealed class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndpointProviderFactory( IConfiguration configuration, - IOptions options, + IOptions options, IOptions serviceDiscoveryOptions, - ILogger logger) : IServiceEndPointProviderFactory + ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); + provider = new ConfigurationServiceEndpointProvider(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs index 91e97b5d0bc..f8092c4dd51 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs @@ -1,22 +1,22 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +internal sealed class ConfigurationServiceEndpointProviderOptionsValidator : IValidateOptions { - public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndpointProviderOptions options) { if (string.IsNullOrWhiteSpace(options.SectionName)) { return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); } - if (options.ApplyHostNameMetadata is null) + if (options.ShouldApplyHostNameMetadata is null) { - return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + return ValidateOptionsResult.Fail($"{nameof(options.ShouldApplyHostNameMetadata)} must not be null."); } return ValidateOptionsResult.Success; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs index d3b94f2f1e7..29f28e359f7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for . +/// Options for . /// -public sealed class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndpointProviderOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". @@ -18,5 +18,5 @@ public sealed class ConfigurationServiceEndPointResolverOptions /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false. /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index 44e58b0dbbb..e11f593776f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -11,13 +11,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; + private readonly ServiceEndpointWatcherFactory _watcherFactory = watcherFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -29,7 +29,7 @@ internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. - public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); if (request.RequestUri is null) @@ -47,15 +47,15 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ static (name, self) => self.CreateResolver(name), this); - var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + var (valid, endpoint) = await resolver.TryGetEndpointAsync(request, cancellationToken).ConfigureAwait(false); if (valid) { - if (endPoint is null) + if (endpoint is null) { throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); } - return endPoint; + return endpoint; } } } @@ -148,37 +148,37 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateWatcher(serviceName); - var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); - var result = new ResolverEntry(resolver, selector); - resolver.Start(); + var watcher = _watcherFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndpointSelector(); + var result = new ResolverEntry(watcher, selector); + watcher.Start(); return result; } private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver; - private readonly IServiceEndPointSelector _selector; + private readonly ServiceEndpointWatcher _watcher; + private readonly IServiceEndpointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndpointWatcher watcher, IServiceEndpointSelector selector) { - _resolver = resolver; + _watcher = watcher; _selector = selector; - _resolver.OnEndPointsUpdated += result => + _watcher.OnEndpointsUpdated += result => { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPointSource); + _selector.SetEndpoints(result.EndpointSource); } }; } - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -189,17 +189,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpoint? Endpoint)> TryGetEndpointAsync(object? context, CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - var result = _selector.GetEndPoint(context); + await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndpoint(context); return (true, result); } else @@ -246,7 +246,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs index 0febfa94815..0c5bd02d10d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Extensions.ServiceDiscovery.Http; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index bc06a031700..a0063ae476b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndpointResolver resolver, IOptions options) : HttpClientHandler { - private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly HttpServiceEndpointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; /// @@ -23,7 +23,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index daa7b8a17de..8f13bb60ab5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { - private readonly HttpServiceEndPointResolver _resolver; + private readonly HttpServiceEndpointResolver _resolver; private readonly ServiceDiscoveryOptions _options; /// @@ -19,7 +19,7 @@ internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler /// /// The endpoint resolver. /// The service discovery options. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options) { _resolver = resolver; _options = options.Value; @@ -31,7 +31,7 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt /// The endpoint resolver. /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; _options = options.Value; @@ -44,7 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } @@ -58,11 +58,11 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) + internal static Uri GetUriWithEndpoint(Uri uri, ServiceEndpoint serviceEndpoint, ServiceDiscoveryOptions options) { - var endpoint = serviceEndPoint.EndPoint; + var endPoint = serviceEndpoint.EndPoint; UriBuilder result; - if (endpoint is UriEndPoint { Uri: { } ep }) + if (endPoint is UriEndPoint { Uri: { } ep }) { result = new UriBuilder(uri) { @@ -84,7 +84,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, { string host; int port; - switch (endpoint) + switch (endPoint) { case IPEndPoint ip: host = ip.Address.ToString(); @@ -95,7 +95,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, port = dns.Port; break; default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + throw new InvalidOperationException($"Endpoints of type {endPoint.GetType()} are not supported"); } result = new UriBuilder(uri) @@ -112,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, if (uri.Scheme.IndexOf('+') > 0) { var scheme = uri.Scheme.Split('+')[0]; - if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowAllSchemes || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs index 0d3ba00122f..e5e7f7587bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -8,12 +8,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( TimeProvider timeProvider, IServiceProvider serviceProvider, - ServiceEndPointWatcherFactory factory, + ServiceEndpointWatcherFactory factory, IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory { public HttpMessageHandler CreateHandler(HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + var registry = new HttpServiceEndpointResolver(factory, serviceProvider, timeProvider); return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs index 07bffa5654b..675941bb955 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; /// /// Represents the result of service endpoint resolution. /// -/// The endpoint collection. +/// The endpoint collection. /// The exception which occurred during resolution. -internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +internal sealed class ServiceEndpointResolverResult(ServiceEndpointSource? endpointSource, Exception? exception) { /// /// Gets the exception which occurred during resolution. @@ -20,11 +20,11 @@ internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPo /// /// Gets a value indicating whether resolution completed successfully. /// - [MemberNotNullWhen(true, nameof(EndPointSource))] + [MemberNotNullWhen(true, nameof(EndpointSource))] public bool ResolvedSuccessfully => Exception is null; /// /// Gets the endpoints. /// - public ServiceEndPointSource? EndPointSource { get; } = endPointSource; + public ServiceEndpointSource? EndpointSource { get; } = endpointSource; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs index bd0172c45cf..2d81ff38601 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs @@ -6,18 +6,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -internal interface IServiceEndPointSelector +internal interface IServiceEndpointSelector { /// /// Sets the collection of endpoints which this instance will select from. /// - /// The collection of endpoints to select from. - void SetEndPoints(ServiceEndPointSource endPoints); + /// The collection of endpoints to select from. + void SetEndpoints(ServiceEndpointSource endpoints); /// - /// Selects an endpoints from the collection provided by the most recent call to . + /// Selects an endpoints from the collection provided by the most recent call to . /// /// The context. /// An endpoint. - ServiceEndPoint GetEndPoint(object? context); + ServiceEndpoint GetEndpoint(object? context); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs similarity index 63% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs index 92da7cf25bf..e7e51bc6021 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs @@ -6,21 +6,21 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndpointSelector : IServiceEndpointSelector { private uint _next; - private IReadOnlyList? _endPoints; + private IReadOnlyList? _endpoints; /// - public void SetEndPoints(ServiceEndPointSource endPoints) + public void SetEndpoints(ServiceEndpointSource endpoints) { - _endPoints = endPoints.EndPoints; + _endpoints = endpoints.Endpoints; } /// - public ServiceEndPoint GetEndPoint(object? context) + public ServiceEndpoint GetEndpoint(object? context) { - if (_endPoints is not { Count: > 0 } collection) + if (_endpoints is not { Count: > 0 } collection) { throw new InvalidOperationException("The endpoint collection contains no endpoints"); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index 570eb5e4e47..f9a984cfe4f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -5,11 +5,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; -internal sealed partial class PassThroughServiceEndPointResolver +internal sealed partial class PassThroughServiceEndpointProvider { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs index 483c08702df..478d81d42dc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs @@ -7,18 +7,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver which passes through the provided value. +/// Service endpoint provider which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider +internal sealed partial class PassThroughServiceEndpointProvider(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count == 0) + if (endpoints.Endpoints.Count == 0) { Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); + var ep = ServiceEndpoint.Create(endPoint); + ep.Features.Set(this); + endpoints.Endpoints.Add(ep); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs index 83455e0979c..2bf8c0cb481 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs @@ -8,29 +8,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver provider which passes through the provided value. +/// Service endpoint provider factory which creates pass-through providers. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory +internal sealed class PassThroughServiceEndpointProviderFactory(ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - var serviceName = query.OriginalString; + var serviceName = query.ToString()!; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); + provider = new PassThroughServiceEndpointProvider(logger, serviceName, endPoint); return true; } - private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? endPoint) { if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) { - serviceEndPoint = null; + endPoint = null; return false; } @@ -50,15 +50,15 @@ private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] ou var port = uri.Port > 0 ? uri.Port : 0; if (IPAddress.TryParse(host, out var ip)) { - serviceEndPoint = new IPEndPoint(ip, port); + endPoint = new IPEndPoint(ip, port); } else if (!string.IsNullOrEmpty(host)) { - serviceEndPoint = new DnsEndPoint(host, port); + endPoint = new DnsEndPoint(host, port); } else { - serviceEndPoint = null; + endPoint = null; return false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 8bece9644ff..04119540e10 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -6,29 +6,27 @@ In typical systems, service configuration changes over time. Service discovery a ## How it works -Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). +Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. -Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. +Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. ### Change notifications -Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndpointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). ### Extensibility using features -Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: +Service endpoints (`ServiceEndpoint` instances) and collections of service endpoints (`ServiceEndpointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by providers. Features which may be available on a `ServiceEndpoint` include: * `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). -* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. -* `IEndPointLoadFeature`: used to query estimated endpoint load. ### Resolution order -The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. +The providers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. ## Getting Started @@ -42,19 +40,19 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); ``` -Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: +Add service discovery to an individual `IHttpClientBuilder` by calling the `AddServiceDiscovery` extension method: ```csharp builder.Services.AddHttpClient(c => { c.BaseAddress = new("http://catalog")); -}).UseServiceDiscovery(); +}).AddServiceDiscovery(); ``` Alternatively, you can add service discovery to all `HttpClient` instances by default: @@ -63,14 +61,14 @@ Alternatively, you can add service discovery to all `HttpClient` instances by de builder.Services.ConfigureHttpClientDefaults(http => { // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); ``` ### Resolving service endpoints from configuration -The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. -This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The `AddServiceDiscovery` extension method adds a configuration-based endpoint provider by default. +This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: @@ -89,30 +87,30 @@ Here is an example demonstrating how to configure a endpoints for the service na The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. ### Configuration -The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: +The configuration provider is configured using the `ConfigurationServiceEndpointProviderOptions` class, which offers these configuration options: * **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. -* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. +* **`ShouldApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(options => +builder.Services.Configure(options => { options.SectionName = "MyServiceEndpoints"; // Configure the logic for applying host name metadata - options.ApplyHostNameMetadata = endpoint => + options.ShouldApplyHostNameMetadata = endpoint => { // Your custom logic here. For example: - return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + return endpoint.Endpoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); }; }); ``` @@ -121,38 +119,38 @@ This example demonstrates setting a custom section name for your service endpoin ## Resolving service endpoints using platform-provided service discovery -Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. -The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. +The pass-through provider performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndpointProvider` extension method on `IServiceCollection`. In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". ## Load-balancing with endpoint selectors -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: ```csharp builder.Services.AddHttpClient( static client => client.BaseAddress = new("http://catalog")); - .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); + .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); ``` The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` -Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. +Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. ## Service discovery in .NET Aspire -.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: @@ -184,7 +182,7 @@ In the above example, two `HttpClient`s are added: one for the core basket servi ### Named endpoints using configuration -With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": +With the configuration-based endpoint provider, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": ```json { @@ -199,7 +197,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified ### Named endpoints in .NET Aspire -.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: +.NET Aspire uses the configuration-based provider at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: ```csharp var builder = DistributedApplication.CreateBuilder(args); @@ -208,13 +206,13 @@ var basket = builder.AddProject("basket") .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") - .WithReference(basket.GetEndPoint("admin")); + .WithReference(basket.GetEndpoint("admin")); var frontend = builder.AddProject("frontend") .WithReference(basket); ``` -In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndpoint(string name)` method, as in the following example: ```csharp @@ -226,7 +224,7 @@ var frontend = builder.AddProject("frontend") ### Named endpoints in Kubernetes using DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -244,11 +242,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index b4c34ccb7c5..8a137aad4f8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -26,8 +26,8 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); + var watcherFactory = services.GetRequiredService(); + var registry = new HttpServiceEndpointResolver(watcherFactory, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 89c5a2d2eb0..02ce1af162b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -6,21 +6,19 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for service endpoint resolvers. +/// Options for service endpoint resolution. /// public sealed class ServiceDiscoveryOptions { /// - /// The value indicating that all endpoint schemes are allowed. + /// Gets or sets a value indicating whether all URI schemes for URIs resolved by the service discovery system are allowed. + /// If this value is , all URI schemes are allowed. + /// If this value is , only the schemes specified in are allowed. /// -#pragma warning disable IDE0300 // Simplify collection initialization -#pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllowAllSchemes = new string[0]; -#pragma warning restore CA1825 // Avoid zero-length array allocations -#pragma warning restore IDE0300 // Simplify collection initialization + public bool AllowAllSchemes { get; set; } = true; /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// Gets or sets the period between polling attempts for providers which do not support refresh notifications via . /// public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); @@ -28,14 +26,13 @@ public sealed class ServiceDiscoveryOptions /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". /// /// - /// When set to , all schemes are allowed. - /// Schemes are not case-sensitive. + /// When is , this property is ignored. /// - public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + public IList AllowedSchemes { get; set; } = new List(); - internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowedSchemes.Equals(AllowAllSchemes)) + if (allowAllSchemes) { if (schemes is string[] array) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index 6403b214631..a5d789b7e4e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -26,8 +26,8 @@ public static class ServiceDiscoveryServiceCollectionExtensions public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { return services.AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -36,11 +36,11 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action configureOptions) { return services.AddServiceDiscoveryCore(configureOptions: configureOptions) - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -56,16 +56,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(_ => TimeProvider.System); - services.TryAddTransient(); - services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(sp => new ServiceEndpointResolver(sp.GetRequiredService(), sp.GetRequiredService())); if (configureOptions is not null) { services.Configure(configureOptions); @@ -75,26 +75,26 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { - return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); + services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); @@ -104,14 +104,14 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS } /// - /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// Configures a service discovery endpoint provider which passes through the input without performing resolution. /// /// The service collection. /// The service collection. - public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs deleted file mode 100644 index 90f62ab0597..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs +++ /dev/null @@ -1,61 +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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.PassThrough; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates service endpoint watchers. -/// -internal sealed partial class ServiceEndPointWatcherFactory( - IEnumerable resolvers, - ILogger resolverLogger, - IOptions options, - TimeProvider timeProvider) -{ - private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers - .Where(r => r is not PassThroughServiceEndPointResolverProvider) - .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _logger = resolverLogger; - private readonly TimeProvider _timeProvider = timeProvider; - private readonly IOptions _options = options; - - /// - /// Creates a service endpoint resolver for the provided service name. - /// - public ServiceEndPointWatcher CreateWatcher(string serviceName) - { - ArgumentNullException.ThrowIfNull(serviceName); - - if (!ServiceEndPointQuery.TryParse(serviceName, out var query)) - { - throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); - } - - List? resolvers = null; - foreach (var factory in _resolverProviders) - { - if (factory.TryCreateProvider(query, out var resolver)) - { - resolvers ??= []; - resolvers.Add(resolver); - } - } - - if (resolvers is not { Count: > 0 }) - { - throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); - } - - Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointWatcher( - resolvers: [.. resolvers], - logger: _logger, - serviceName: serviceName, - timeProvider: _timeProvider, - options: _options); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs index 1a14cb961b7..947f24b2f81 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs @@ -9,9 +9,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// A mutable collection of service endpoints. /// -internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +internal sealed class ServiceEndpointBuilder : IServiceEndpointBuilder { - private readonly List _endPoints = new(); + private readonly List _endpoints = new(); private readonly List _changeTokens = new(); private readonly FeatureCollection _features = new FeatureCollection(); @@ -32,15 +32,15 @@ public void AddChangeToken(IChangeToken changeToken) /// /// Gets the endpoints. /// - public IList EndPoints => _endPoints; + public IList Endpoints => _endpoints; /// - /// Creates a from the provided instance. + /// Creates a from the provided instance. /// /// The service endpoint source. - public ServiceEndPointSource Build() + public ServiceEndpointSource Build() { - return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + return new ServiceEndpointSource(_endpoints, new CompositeChangeToken(_changeTokens), _features); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 029d2601243..92df120940d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -9,13 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves service names to collections of endpoints. /// -public sealed class ServiceEndPointResolver : IAsyncDisposable +public sealed class ServiceEndpointResolver : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverProvider; + private readonly ServiceEndpointWatcherFactory _watcherFactory; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -23,13 +23,13 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The resolver factory. + /// The watcher factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, TimeProvider timeProvider) { - _resolverProvider = resolverProvider; + _watcherFactory = watcherFactory; _timeProvider = timeProvider; } @@ -39,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, /// The service name. /// A . /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + public async ValueTask GetEndpointsAsync(string serviceName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(serviceName); ObjectDisposedException.ThrowIf(_disposed, this); @@ -54,7 +54,7 @@ public async ValueTask GetEndPointsAsync(string serviceNa static (name, self) => self.CreateResolver(name), this); - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var (valid, result) = await resolver.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); if (valid) { if (result is null) @@ -156,21 +156,21 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateWatcher(serviceName); + var resolver = _watcherFactory.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } - private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable + private sealed class ResolverEntry(ServiceEndpointWatcher watcher) : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver = resolver; + private readonly ServiceEndpointWatcher _watcher = watcher; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -181,17 +181,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpointSource? Endpoints)> GetEndpointsAsync(CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); + var endpoints = await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endpoints); } else { @@ -237,7 +237,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index 78a8f84b556..fce9f667b40 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -5,35 +5,35 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcher +partial class ServiceEndpointWatcher { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] - public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] + public static partial void ResolvingEndpoints(ILogger logger, string serviceName); [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] public static partial void ResolutionPending(ILogger logger, string serviceName); - [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] - public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); + [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {Endpoints}.", EventName = "ResolutionSucceeded")] + public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endpoints); - public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndpointSource endpointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endpointSource.Endpoints.Count, serviceName, string.Join(", ", endpointSource.Endpoints.Select(GetEndpointString))); } - static string GetEndPointString(ServiceEndPoint ep) + static string GetEndpointString(ServiceEndpoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } provider) { - return $"{ep.GetEndPointString()} ({resolver})"; + return $"{ep} ({provider})"; } - return ep.GetEndPointString(); - } + return ep.ToString()!; + } } [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index 9b1069d31e7..ba6df9b43c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -13,23 +13,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -internal sealed partial class ServiceEndPointWatcher( - IServiceEndPointProvider[] resolvers, +internal sealed partial class ServiceEndpointWatcher( + IServiceEndpointProvider[] providers, ILogger logger, string serviceName, TimeProvider timeProvider, IOptions options) : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); + private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: true); private readonly object _lock = new(); private readonly ILogger _logger = logger; private readonly TimeProvider _timeProvider = timeProvider; private readonly ServiceDiscoveryOptions _options = options.Value; - private readonly IServiceEndPointProvider[] _resolvers = resolvers; + private readonly IServiceEndpointProvider[] _providers = providers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointSource? _cachedEndPoints; + private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -41,14 +41,14 @@ internal sealed partial class ServiceEndPointWatcher( /// /// Gets or sets the action called when endpoints are updated. /// - public Action? OnEndPointsUpdated { get; set; } + public Action? OnEndpointsUpdated { get; set; } /// /// Starts the endpoint resolver. /// public void Start() { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); _ = RefreshAsync(force: false); } @@ -57,27 +57,27 @@ public void Start() /// /// A . /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) { - return new ValueTask(cached); + return new ValueTask(cached); } // Otherwise, ensure the cache is being refreshed // Wait for the cache refresh to complete and return the cached value. - return GetEndPointsInternal(cancellationToken); + return GetEndpointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { - ServiceEndPointSource? result; + ServiceEndpointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; + result = _cachedEndpoints; } while (result is null); return result; } @@ -89,7 +89,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -124,27 +124,27 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointSource? newEndPoints = null; + ServiceEndpointSource? newEndpoints = null; CacheStatus newCacheState; try { - Log.ResolvingEndPoints(_logger, ServiceName); - var builder = new ServiceEndPointBuilder(); - foreach (var resolver in _resolvers) + Log.ResolvingEndpoints(_logger, ServiceName); + var builder = new ServiceEndpointBuilder(); + foreach (var provider in _providers) { - await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } - var endPoints = builder.Build(); + var endpoints = builder.Build(); newCacheState = CacheStatus.Valid; lock (_lock) { // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) + if (endpoints.ChangeToken.ActiveChangeCallbacks) { // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); if (_pollingTimer is { } timer) { _pollingTimer = null; @@ -157,7 +157,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = endPoints; + newEndpoints = endpoints; newCacheState = CacheStatus.Valid; } } @@ -171,26 +171,26 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // To ensure coherence between the value returned by calls made to GetEndpointsAsync and value passed to the callback, // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // before receiving the updated value. An alternative approach is to lock access to _cachedEndpoints, but // that will have more overhead in the common case. if (newCacheState is CacheStatus.Valid) { - Interlocked.Exchange(ref _cachedEndPoints, null); + Interlocked.Exchange(ref _cachedEndpoints, null); } - if (OnEndPointsUpdated is { } callback) + if (OnEndpointsUpdated is { } callback) { - callback(new(newEndPoints, error)); + callback(new(newEndpoints, error)); } lock (_lock) { if (newCacheState is CacheStatus.Valid) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + Debug.Assert(newEndpoints is not null); + _cachedEndpoints = newEndpoints; } _cacheState = newCacheState; @@ -201,9 +201,9 @@ private async Task RefreshAsyncInternal() Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (newEndPoints is not null) + else if (newEndpoints is not null) { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndpoints); } } @@ -240,9 +240,9 @@ public async ValueTask DisposeAsync() await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } - foreach (var resolver in _resolvers) + foreach (var provider in _providers) { - await resolver.DisposeAsync().ConfigureAwait(false); + await provider.DisposeAsync().ConfigureAwait(false); } } @@ -253,14 +253,14 @@ private enum CacheStatus Valid } - private void ThrowIfNoResolvers() + private void ThrowIfNoProviders() { - if (_resolvers.Length == 0) + if (_providers.Length == 0) { - ThrowNoResolversConfigured(); + ThrowNoProvidersConfigured(); } } [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); + private static void ThrowNoProvidersConfigured() => throw new InvalidOperationException("No service endpoint providers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 69f565eb8e3..5f4acc89874 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -5,17 +5,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcherFactory +partial class ServiceEndpointWatcherFactory { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] - public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); + + public static void CreatingResolver(ILogger logger, string serviceName, List providers) { if (logger.IsEnabled(LogLevel.Debug)) { - ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); + ServiceEndpointProviderListCore(logger, serviceName, providers.Count, string.Join(", ", providers.Select(static r => r.ToString()))); } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs new file mode 100644 index 00000000000..6cc7cb2cbc5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates service endpoint watchers. +/// +internal sealed partial class ServiceEndpointWatcherFactory( + IEnumerable providerFactories, + ILogger logger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndpointProviderFactory[] _providerFactories = providerFactories + .Where(r => r is not PassThroughServiceEndpointProviderFactory) + .Concat(providerFactories.Where(static r => r is PassThroughServiceEndpointProviderFactory)).ToArray(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a service endpoint watcher for the provided service name. + /// + public ServiceEndpointWatcher CreateWatcher(string serviceName) + { + ArgumentNullException.ThrowIfNull(serviceName); + + if (!ServiceEndpointQuery.TryParse(serviceName, out var query)) + { + throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); + } + + List? providers = null; + foreach (var factory in _providerFactories) + { + if (factory.TryCreateProvider(query, out var provider)) + { + providers ??= []; + providers.Add(provider); + } + } + + if (providers is not { Count: > 0 }) + { + throw new InvalidOperationException($"No provider which supports the provided service name, '{serviceName}', has been configured."); + } + + Log.CreatingResolver(_logger, serviceName, providers); + return new ServiceEndpointWatcher( + providers: [.. providers], + logger: _logger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs similarity index 72% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 25cd88a1436..7cadb4e3c7f 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for and . +/// These also cover and by extension. /// -public class DnsSrvServiceEndPointResolverTests +public class DnsSrvServiceEndpointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndPoint_Dns() + public async Task ResolveServiceEndpoint_Dns() { var dnsClientMock = new FakeDnsClient { @@ -101,26 +101,26 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.EndPoints; + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -134,7 +134,7 @@ public async Task ResolveServiceEndPoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -175,28 +175,28 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => + .AddDnsSrvServiceEndpointProvider(options => { options.QuerySuffix = ".ns"; - options.ApplyHostNameMetadata = _ => true; + options.ShouldApplyHostNameMetadata = _ => true; }) - .AddConfigurationServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider(); } else { serviceCollection - .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + .AddConfigurationServiceEndpointProvider() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); @@ -205,13 +205,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.EndPoints; + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -221,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo else { // We expect only the results from the Configuration provider. - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -248,59 +248,4 @@ public void SetValues(IEnumerable> values) OnReload(); } } - - /* - [Fact] - public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() - { - var oneEndPoint = new Dictionary - { - ["services:basket:http:0:host"] = "localhost", - ["services:basket:http:0:port"] = "8080", - }; - var bothEndPoints = new Dictionary(oneEndPoint) - { - ["services:basket:http:1:host"] = "remotehost", - ["services:basket:http:1:port"] = "9090", - }; - var configSource = new MyConfigurationProvider(); - var services = new ServiceCollection() - .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) - .AddServiceDiscovery() - .AddConfigurationServiceEndPointResolver() - .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) - { - Assert.NotNull(resolver); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); - resolver.Start(); - var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialResult); - Assert.False(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); - Assert.Null(initialResult.EndPoints); - - // Update the config and check that it flows through the system. - configSource.SetValues(oneEndPoint); - - // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to - // cause an indefinite test hang. We expect the result to be published practically immediately, though. - _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); - var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - var firstEp = Assert.Single(oneEpResult); - Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); - - // Do it again to check that an updated (not cached) version is published. - configSource.SetValues(bothEndPoints); - var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.True(twoEpResult.ResolvedSuccessfully); - Assert.Equal(2, twoEpResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); - } - } - */ } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6d8091f026c..db720782107 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -12,13 +12,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class ConfigurationServiceEndPointResolverTests +public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -27,23 +27,23 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -52,7 +52,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() { // Try to resolve an http endpoint when only https is allowed. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary @@ -63,59 +63,63 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .Configure(o => o.AllowedSchemes = ["https"]) + .AddConfigurationServiceEndpointProvider() + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Empty(initialResult.EndPointSource.EndPoints); + Assert.Empty(initialResult.EndpointSource.Endpoints); } // Specifying either https or http. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } // Specifying either https or http, but in reverse. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { var configSource = new MemoryConfigurationSource { @@ -129,24 +133,24 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) + .AddConfigurationServiceEndpointProvider(options => options.ShouldApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -155,20 +159,20 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. - await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndpointSource.Endpoints[1].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -178,7 +182,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() { var configSource = new MemoryConfigurationSource { @@ -196,25 +200,25 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndpointSource.Endpoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -223,7 +227,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() { var configSource = new MemoryConfigurationSource { @@ -241,29 +245,29 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndpointSource.Endpoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndpointSource.Endpoints[1].EndPoint); // We expect the HTTPS endpoint back but not the HTTP one. - Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index 643bbfad441..e0af5c03ed4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -12,36 +12,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class PassThroughServiceEndPointResolverTests +public class PassThroughServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_PassThrough() + public async Task ResolveServiceEndpoint_PassThrough() { var services = new ServiceCollection() .AddServiceDiscoveryCore() - .AddPassThroughServiceEndPointResolver() + .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Superseded() + public async Task ResolveServiceEndpoint_Superseded() { var configSource = new MemoryConfigurationSource { @@ -55,26 +55,26 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Fallback() + public async Task ResolveServiceEndpoint_Fallback() { var configSource = new MemoryConfigurationSource { @@ -88,27 +88,27 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndpointSource.Endpoints[0].EndPoint); } } // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. [Fact] - public async Task ResolveServiceEndPoint_Fallback_NoScheme() + public async Task ResolveServiceEndpoint_Fallback_NoScheme() { var configSource = new MemoryConfigurationSource { @@ -123,8 +123,8 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); - var result = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); + var resolver = services.GetRequiredService(); + var result = await resolver.GetEndpointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.Endpoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs similarity index 62% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index f5e506a9b72..16950e67374 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -15,58 +15,58 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// -public class ServiceEndPointResolverTests +public class ServiceEndpointResolverTests { [Fact] - public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + public void ResolveServiceEndpoint_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket")); - Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); + Assert.Equal("No provider which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] - public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + public async Task ServiceEndpointResolver_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); - var exception = Assert.Throws(resolverFactory.Start); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); - exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + var watcher = new ServiceEndpointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); + var exception = Assert.Throws(watcher.Start); + Assert.Equal("No service endpoint providers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await watcher.GetEndpointsAsync()); + Assert.Equal("No service endpoint providers are configured.", exception.Message); } [Fact] - public void ResolveServiceEndPoint_NullServiceName_Throws() + public void ResolveServiceEndpoint_NullServiceName_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task AddServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoProviders_Throws() { var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery(); var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); - Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); + Assert.Equal("No provider which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory + private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(query); @@ -74,58 +74,58 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou } } - private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndpointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) => resolveAsync(endpoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } [Fact] - public async Task ResolveServiceEndPoint() + public async Task ResolveServiceEndpoint() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); - var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + Assert.Equal(2, resolverResult.EndpointSource.Endpoints.Count); + var endpoints = resolverResult.EndpointSource.Endpoints.Select(ep => ep.EndPoint).OfType().ToList(); endpoints.Sort((l, r) => l.Port - r.Port); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); @@ -133,34 +133,34 @@ public async Task ResolveServiceEndPoint() } [Fact] - public async Task ResolveServiceEndPointOneShot() + public async Task ResolveServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -169,36 +169,36 @@ public async Task ResolveServiceEndPointOneShot() } [Fact] - public async Task ResolveHttpServiceEndPointOneShot() + public async Task ResolveHttpServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var fakeResolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndpointResolver(resolverProvider, services, TimeProvider.System); Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); - var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(endPoint); - var ip = Assert.IsType(endPoint.EndPoint); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endpoint); + var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -206,12 +206,12 @@ public async Task ResolveHttpServiceEndPointOneShot() } [Fact] - public async Task ResolveServiceEndPoint_ThrowOnReload() + public async Task ResolveServiceEndpoint_ThrowOnReload() { var sem = new SemaphoreSlim(0); var cts = new[] { new CancellationTokenSource() }; var throwOnNextResolve = new[] { false }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: async (collection, ct) => { await sem.WaitAsync(ct).ConfigureAwait(false); @@ -228,25 +228,25 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); - var initialEndPoints = await initialEndPointsTask; - Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints.EndPoints); + var initialEndpoints = await initialEndpointsTask; + Assert.NotNull(initialEndpoints); + Assert.Single(initialEndpoints.Endpoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -254,21 +254,21 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var exception = await Assert.ThrowsAsync(async () => { - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); }).ConfigureAwait(false); Assert.Equal("throwing", exception.Message); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + var channel = Channel.CreateUnbounded(); + watcher.OnEndpointsUpdated = result => channel.Writer.TryWrite(result); do { cts[0].Cancel(); sem.Release(1); - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); await resolveTask.ConfigureAwait(false); var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); if (next.ResolvedSuccessfully) @@ -277,11 +277,11 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } } while (true); - var task = resolver.GetEndPointsAsync(CancellationToken.None); + var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var result = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, result); - var sep = Assert.Single(result.EndPoints); + Assert.NotSame(initialEndpoints, result); + var sep = Assert.Single(result.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); From 27dd319ccfa2560e14d9092100a45c510a6e22eb Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 12 Apr 2024 13:35:12 +1000 Subject: [PATCH 041/472] Enable public API analyzer. (#3547) * Enable public API analyzer. --- .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 28 ++++++++++++++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 32 +++++++++++++++++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 5 +++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 30 +++++++++++++++++ 8 files changed, 99 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..b55e2b696ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt @@ -0,0 +1,28 @@ +#nullable enable +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..aa1fee77235 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt @@ -0,0 +1,32 @@ +#nullable enable +Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..55d92fd4caa --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..b3be4048a2d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt @@ -0,0 +1,30 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From 6b67ac719c62ae5ba1a684a6b59614fd7665da1b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:54:03 -0700 Subject: [PATCH 042/472] Service Discovery add additional configuration tests, make scheme selection more intuitive in un-specified case (#3837) --- .../ConfigurationServiceEndpointProvider.cs | 88 +++++---- .../ServiceDiscoveryOptions.cs | 34 ++-- ...nfigurationServiceEndpointResolverTests.cs | 178 ++++++++++++++++-- 3 files changed, 225 insertions(+), 75 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 37078e01969..e8c84b69ec8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -17,6 +17,7 @@ internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEnd private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly bool _includeAllSchemes; private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -39,6 +40,7 @@ public ConfigurationServiceEndpointProvider( { _serviceName = query.ServiceName; _endpointName = query.EndpointName; + _includeAllSchemes = serviceDiscoveryOptions.Value.AllowAllSchemes && query.IncludedSchemes.Count == 0; _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; @@ -74,27 +76,17 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo string endpointName; if (string.IsNullOrWhiteSpace(_endpointName)) { - if (_schemes.Length == 0) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + endpointName = DefaultEndpointName; + ReadOnlySpan candidateNames = [DefaultEndpointName, .. _schemes]; + foreach (var scheme in candidateNames) { - // Use the section named "default". - endpointName = DefaultEndpointName; - namedSection = section.GetSection(endpointName); - } - else - { - // Set the ideal endpoint name for error messages. - endpointName = _schemes[0]; - - // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists - foreach (var scheme in _schemes) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - var candidate = section.GetSection(scheme); - if (candidate.Exists()) - { - endpointName = scheme; - namedSection = candidate; - break; - } + endpointName = scheme; + namedSection = candidate; + break; } } } @@ -135,46 +127,60 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo } } - // Filter the resolved endpoints to only include those which match the specified scheme. - var minIndex = _schemes.Length; - foreach (var ep in resolved) + int resolvedEndpointCount; + if (_includeAllSchemes) { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Include all endpoints. + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index < minIndex) - { - minIndex = index; - } + endpoints.Endpoints.Add(ep); } + + resolvedEndpointCount = resolved.Count; } - - var added = 0; - foreach (var ep in resolved) + else { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Filter the resolved endpoints to only include those which match the specified, allowed schemes. + resolvedEndpointCount = 0; + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index <= minIndex) + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) { - ++added; - endpoints.Endpoints.Add(ep); + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } } } - else + + foreach (var ep in resolved) { - ++added; - endpoints.Endpoints.Add(ep); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } + } + else + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } } } - if (added == 0) + if (resolvedEndpointCount == 0) { Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); } else { - Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, resolvedEndpointCount); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 02ce1af162b..edc652507d9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -32,29 +32,35 @@ public sealed class ServiceDiscoveryOptions internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowAllSchemes) + if (schemes.Count > 0) { - if (schemes is string[] array) + if (allowAllSchemes) { - return array; - } + if (schemes is string[] array && array.Length > 0) + { + return array; + } - return schemes.ToArray(); - } + return schemes.ToArray(); + } - List result = []; - foreach (var s in schemes) - { - foreach (var allowed in allowedSchemes) + List result = []; + foreach (var s in schemes) { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + foreach (var allowed in allowedSchemes) { - result.Add(s); - break; + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } } } + + return result.ToArray(); } - return result.ToArray(); + // If no schemes were specified, but a set of allowed schemes were specified, allow those. + return allowedSchemes.ToArray(); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index db720782107..6955cc1e8e2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndpoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -87,8 +87,23 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() Assert.Empty(initialResult.EndpointSource.Endpoints); } + // Specifying no scheme. + // We should get the HTTPS endpoint back, since it is explicitly allowed + await using ((watcher = watcherFactory.CreateWatcher("_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + // Specifying either https or http. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -103,7 +118,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } // Specifying either https or http, but in reverse. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -118,6 +133,144 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } } + [Fact] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:8080", + ["services:basket:otlp:0"] = "https://localhost:8888", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider(o => + { + o.ShouldApplyHostNameMetadata = _ => true; + }) + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Explicitly specifying https as the scheme, but the endpoint section in configuration is the default value ("default"). + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("https://basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + + Assert.All(initialResult.EndpointSource.Endpoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Not specifying the scheme or endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + + // Not specifying the scheme, but specifying the default endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("_default.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + } + + /// + /// Checks that when there is no named endpoint, configuration resolves first from the "default" section, then sections named by the scheme names. + /// + [Theory] + [InlineData(true, true, "https://basket", "https://default-host:8080")] + [InlineData(false, true, "https://basket","https://https-host:8080")] + [InlineData(true, false, "https://basket", "https://default-host:8080")] + [InlineData(true, true, "basket", "https://default-host:8080")] + [InlineData(false, true, "basket", null)] + [InlineData(true, false, "basket", "https://default-host:8080")] + [InlineData(true, true, "http+https://basket", "https://default-host:8080")] + [InlineData(false, true, "http+https://basket","https://https-host:8080")] + [InlineData(true, false, "http+https://basket", "https://default-host:8080")] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_ResolutionOrder( + bool includeDefault, + bool includeSchemeNamed, + string serviceName, + string? expectedResult) + { + var data = new Dictionary(); + if (includeDefault) + { + data["services:basket:default:0"] = "https://default-host:8080"; + } + + if (includeSchemeNamed) + { + data["services:basket:https:0"] = "https://https-host:8080"; + } + + var config = new ConfigurationBuilder().AddInMemoryCollection(data); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Scheme in query + await using ((watcher = watcherFactory.CreateWatcher(serviceName)).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + if (expectedResult is not null) + { + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + else + { + Assert.Empty(initialResult.EndpointSource.Endpoints); + } + } + } + [Fact] public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { @@ -125,8 +278,8 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:http:0"] = "http://localhost:8080", - ["services:basket:http:1"] = "http://remotehost:9090", + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -274,19 +427,4 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe }); } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } From 8ccf3386b25bbf8c7c960d913b5f0412d15ae661 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:58:54 -0700 Subject: [PATCH 043/472] Service Discovery add additional configuration tests, make scheme selection more intuitive in un-specified case (#3848) Co-authored-by: Reuben Bond --- .../ConfigurationServiceEndpointProvider.cs | 88 +++++---- .../ServiceDiscoveryOptions.cs | 34 ++-- ...nfigurationServiceEndpointResolverTests.cs | 178 ++++++++++++++++-- 3 files changed, 225 insertions(+), 75 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 37078e01969..e8c84b69ec8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -17,6 +17,7 @@ internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEnd private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly bool _includeAllSchemes; private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -39,6 +40,7 @@ public ConfigurationServiceEndpointProvider( { _serviceName = query.ServiceName; _endpointName = query.EndpointName; + _includeAllSchemes = serviceDiscoveryOptions.Value.AllowAllSchemes && query.IncludedSchemes.Count == 0; _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; @@ -74,27 +76,17 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo string endpointName; if (string.IsNullOrWhiteSpace(_endpointName)) { - if (_schemes.Length == 0) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + endpointName = DefaultEndpointName; + ReadOnlySpan candidateNames = [DefaultEndpointName, .. _schemes]; + foreach (var scheme in candidateNames) { - // Use the section named "default". - endpointName = DefaultEndpointName; - namedSection = section.GetSection(endpointName); - } - else - { - // Set the ideal endpoint name for error messages. - endpointName = _schemes[0]; - - // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists - foreach (var scheme in _schemes) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - var candidate = section.GetSection(scheme); - if (candidate.Exists()) - { - endpointName = scheme; - namedSection = candidate; - break; - } + endpointName = scheme; + namedSection = candidate; + break; } } } @@ -135,46 +127,60 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo } } - // Filter the resolved endpoints to only include those which match the specified scheme. - var minIndex = _schemes.Length; - foreach (var ep in resolved) + int resolvedEndpointCount; + if (_includeAllSchemes) { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Include all endpoints. + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index < minIndex) - { - minIndex = index; - } + endpoints.Endpoints.Add(ep); } + + resolvedEndpointCount = resolved.Count; } - - var added = 0; - foreach (var ep in resolved) + else { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Filter the resolved endpoints to only include those which match the specified, allowed schemes. + resolvedEndpointCount = 0; + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index <= minIndex) + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) { - ++added; - endpoints.Endpoints.Add(ep); + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } } } - else + + foreach (var ep in resolved) { - ++added; - endpoints.Endpoints.Add(ep); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } + } + else + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } } } - if (added == 0) + if (resolvedEndpointCount == 0) { Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); } else { - Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, resolvedEndpointCount); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 02ce1af162b..edc652507d9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -32,29 +32,35 @@ public sealed class ServiceDiscoveryOptions internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowAllSchemes) + if (schemes.Count > 0) { - if (schemes is string[] array) + if (allowAllSchemes) { - return array; - } + if (schemes is string[] array && array.Length > 0) + { + return array; + } - return schemes.ToArray(); - } + return schemes.ToArray(); + } - List result = []; - foreach (var s in schemes) - { - foreach (var allowed in allowedSchemes) + List result = []; + foreach (var s in schemes) { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + foreach (var allowed in allowedSchemes) { - result.Add(s); - break; + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } } } + + return result.ToArray(); } - return result.ToArray(); + // If no schemes were specified, but a set of allowed schemes were specified, allow those. + return allowedSchemes.ToArray(); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index db720782107..6955cc1e8e2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndpoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -87,8 +87,23 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() Assert.Empty(initialResult.EndpointSource.Endpoints); } + // Specifying no scheme. + // We should get the HTTPS endpoint back, since it is explicitly allowed + await using ((watcher = watcherFactory.CreateWatcher("_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + // Specifying either https or http. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -103,7 +118,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } // Specifying either https or http, but in reverse. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -118,6 +133,144 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } } + [Fact] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:8080", + ["services:basket:otlp:0"] = "https://localhost:8888", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider(o => + { + o.ShouldApplyHostNameMetadata = _ => true; + }) + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Explicitly specifying https as the scheme, but the endpoint section in configuration is the default value ("default"). + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("https://basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + + Assert.All(initialResult.EndpointSource.Endpoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Not specifying the scheme or endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + + // Not specifying the scheme, but specifying the default endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("_default.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + } + + /// + /// Checks that when there is no named endpoint, configuration resolves first from the "default" section, then sections named by the scheme names. + /// + [Theory] + [InlineData(true, true, "https://basket", "https://default-host:8080")] + [InlineData(false, true, "https://basket","https://https-host:8080")] + [InlineData(true, false, "https://basket", "https://default-host:8080")] + [InlineData(true, true, "basket", "https://default-host:8080")] + [InlineData(false, true, "basket", null)] + [InlineData(true, false, "basket", "https://default-host:8080")] + [InlineData(true, true, "http+https://basket", "https://default-host:8080")] + [InlineData(false, true, "http+https://basket","https://https-host:8080")] + [InlineData(true, false, "http+https://basket", "https://default-host:8080")] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_ResolutionOrder( + bool includeDefault, + bool includeSchemeNamed, + string serviceName, + string? expectedResult) + { + var data = new Dictionary(); + if (includeDefault) + { + data["services:basket:default:0"] = "https://default-host:8080"; + } + + if (includeSchemeNamed) + { + data["services:basket:https:0"] = "https://https-host:8080"; + } + + var config = new ConfigurationBuilder().AddInMemoryCollection(data); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Scheme in query + await using ((watcher = watcherFactory.CreateWatcher(serviceName)).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + if (expectedResult is not null) + { + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + else + { + Assert.Empty(initialResult.EndpointSource.Endpoints); + } + } + } + [Fact] public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { @@ -125,8 +278,8 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:http:0"] = "http://localhost:8080", - ["services:basket:http:1"] = "http://remotehost:9090", + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -274,19 +427,4 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe }); } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } From 696c6ce5c6677eaa981852c7045a41a1e7cc1bbb Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 30 Apr 2024 07:00:03 -0700 Subject: [PATCH 044/472] ServiceEndpoint.ToString(): omit zero port (#4015) --- .../Internal/ServiceEndpointImpl.cs | 4 ++ .../ServiceEndpointTests.cs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 8bfb50fe930..6f89cd8e37b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -9,9 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); + public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", + DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", _ => EndPoint.ToString()! }; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs new file mode 100644 index 00000000000..e05d0818e1a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +public class ServiceEndpointTests +{ + public static TheoryData ZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:0"), + new DnsEndPoint("microsoft.com", 0), + new UriEndPoint(new Uri("https://microsoft.com")) + }; + + public static TheoryData NonZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:8443"), + new DnsEndPoint("microsoft.com", 8443), + new UriEndPoint(new Uri("https://microsoft.com:8443")) + }; + + [Theory] + [MemberData(nameof(ZeroPortEndPoints))] + public void ServiceEndpointToStringOmitsUnspecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.DoesNotContain(":0", epString); + } + + [Theory] + [MemberData(nameof(NonZeroPortEndPoints))] + public void ServiceEndpointToStringContainsSpecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.Contains(":8443", epString); + } +} From 131f376a261f594d1a2c06c2901715c0ebb0970d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:49:07 -0700 Subject: [PATCH 045/472] ServiceEndpoint.ToString(): omit zero port (#4033) --- .../Internal/ServiceEndpointImpl.cs | 4 ++ .../ServiceEndpointTests.cs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 8bfb50fe930..6f89cd8e37b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -9,9 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); + public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", + DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", _ => EndPoint.ToString()! }; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs new file mode 100644 index 00000000000..e05d0818e1a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +public class ServiceEndpointTests +{ + public static TheoryData ZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:0"), + new DnsEndPoint("microsoft.com", 0), + new UriEndPoint(new Uri("https://microsoft.com")) + }; + + public static TheoryData NonZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:8443"), + new DnsEndPoint("microsoft.com", 8443), + new UriEndPoint(new Uri("https://microsoft.com:8443")) + }; + + [Theory] + [MemberData(nameof(ZeroPortEndPoints))] + public void ServiceEndpointToStringOmitsUnspecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.DoesNotContain(":0", epString); + } + + [Theory] + [MemberData(nameof(NonZeroPortEndPoints))] + public void ServiceEndpointToStringContainsSpecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.Contains(":8443", epString); + } +} From 7de9f20fb3b8180841d3a1bd40a62931dd00d04e Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:26:27 -0700 Subject: [PATCH 046/472] Promptly remove invalid resolvers (#3800) --- .../Http/HttpServiceEndpointResolver.cs | 5 ++ .../ServiceEndpointResolver.cs | 6 +++ .../ServiceEndpointWatcher.cs | 48 +++++++++++++------ .../DnsServiceEndpointResolverTests.cs | 35 ++++++++++++++ .../DnsSrvServiceEndpointResolverTests.cs | 19 +------- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 1 + 6 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index e11f593776f..e547ab14138 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -57,6 +57,10 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ return endpoint; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -140,6 +144,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 92df120940d..e928980700c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -49,6 +49,7 @@ public async ValueTask GetEndpointsAsync(string serviceNa while (true) { ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); var resolver = _resolvers.GetOrAdd( serviceName, static (name, self) => self.CreateResolver(name), @@ -64,6 +65,10 @@ public async ValueTask GetEndpointsAsync(string serviceNa return result; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -148,6 +153,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index ba6df9b43c4..d361202a698 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -32,6 +32,7 @@ internal sealed partial class ServiceEndpointWatcher( private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; + private IDisposable? _changeTokenRegistration; /// /// Gets the service name. @@ -60,6 +61,8 @@ public void Start() public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoProviders(); + ObjectDisposedException.ThrowIf(_disposalCancellation.IsCancellationRequested, this); + cancellationToken.ThrowIfCancellationRequested(); // If the cache is valid, return the cached value. if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) @@ -76,9 +79,11 @@ async ValueTask GetEndpointsInternal(CancellationToken ca ServiceEndpointSource? result; do { + cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; } while (result is null); + return result; } } @@ -89,7 +94,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) + if (!_disposalCancellation.IsCancellationRequested && _refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -128,10 +133,18 @@ private async Task RefreshAsyncInternal() CacheStatus newCacheState; try { + lock (_lock) + { + // Dispose the existing change token registration, if any. + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + } + Log.ResolvingEndpoints(_logger, ServiceName); var builder = new ServiceEndpointBuilder(); foreach (var provider in _providers) { + cancellationToken.ThrowIfCancellationRequested(); await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } @@ -143,13 +156,12 @@ private async Task RefreshAsyncInternal() // Check if we need to poll for updates or if we can register for change notification callbacks. if (endpoints.ChangeToken.ActiveChangeCallbacks) { - // Initiate a background refresh, if necessary. - endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + // Initiate a background refresh when the change token fires. + _changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); + + // Dispose the existing timer, if any, since we are reliant on change tokens for updates. + _pollingTimer?.Dispose(); + _pollingTimer = null; } else { @@ -211,6 +223,13 @@ private void SchedulePollingTimer() { lock (_lock) { + if (_disposalCancellation.IsCancellationRequested) + { + _pollingTimer?.Dispose(); + _pollingTimer = null; + return; + } + if (_pollingTimer is null) { _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); @@ -227,14 +246,15 @@ public async ValueTask DisposeAsync() { lock (_lock) { - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + _disposalCancellation.Cancel(); + + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + + _pollingTimer?.Dispose(); + _pollingTimer = null; } - _disposalCancellation.Cancel(); if (_refreshTask is { } task) { await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs new file mode 100644 index 00000000000..2b3a7fd7cd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServiceEndpointResolverTests +{ + [Fact] + public async Task ResolveServiceEndpoint_Dns_MultiShot() + { + var timeProvider = new FakeTimeProvider(); + var services = new ServiceCollection() + .AddSingleton(timeProvider) + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) + .BuildServiceProvider(); + var resolver = services.GetRequiredService(); + var initialResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(initialResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(7)); + var secondResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(secondResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(80)); + var thirdResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(thirdResult); + Assert.True(initialResult.Endpoints.Count > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 7cadb4e3c7f..3449c075047 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndpoint_Dns() + public async Task ResolveServiceEndpoint_DnsSrv() { var dnsClientMock = new FakeDnsClient { @@ -134,7 +134,7 @@ public async Task ResolveServiceEndpoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -233,19 +233,4 @@ public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(boo } } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index ba827640199..31fe0f9d687 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -9,6 +9,7 @@ + From b62eb8eabbf9e713c3b505a5d37f909ac7bce9b0 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 1 May 2024 08:43:39 -0700 Subject: [PATCH 047/472] Improve test coverage for YARP Service Discovery (#4036) --- .../Internal/ServiceEndpointImpl.cs | 1 + ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 4 + .../ServiceDiscoveryDestinationResolver.cs | 32 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 23 ++ .../YarpServiceDiscoveryTests.cs | 284 ++++++++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 6f89cd8e37b..151a9309338 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -14,6 +14,7 @@ internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 => $"[{ip.Address}]", IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index c5145e06528..08f6394daf8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 8ec810f2c05..3de03a6ebde 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.ServiceDiscovery; @@ -14,8 +15,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver +/// The service discovery options. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver, IOptions options) : IDestinationResolver { + private readonly ServiceDiscoveryOptions _options = options.Value; + /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) { @@ -65,13 +69,15 @@ public async ValueTask ResolveDestinationsAsync(I Uri uri; if (!addressString.Contains("://")) { - uri = new Uri($"https://{addressString}"); + var scheme = GetDefaultScheme(originalUri); + uri = new Uri($"{scheme}://{addressString}"); } else { uri = new Uri(addressString); } + uriBuilder.Scheme = uri.Scheme; uriBuilder.Host = uri.Host; uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); @@ -90,4 +96,26 @@ public async ValueTask ResolveDestinationsAsync(I return (results, result.ChangeToken); } + + private string GetDefaultScheme(Uri originalUri) + { + if (originalUri.Scheme.IndexOf('+') > 0) + { + // Use the first allowed scheme. + var specifiedSchemes = originalUri.Scheme.Split('+'); + foreach (var scheme in specifiedSchemes) + { + if (_options.AllowAllSchemes || _options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + return scheme; + } + } + + throw new InvalidOperationException($"None of the specified schemes ('{string.Join(", ", specifiedSchemes)}') are allowed by configuration."); + } + else + { + return originalUri.Scheme; + } + } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj new file mode 100644 index 00000000000..8a816222c9f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs new file mode 100644 index 00000000000..3628da0839f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -0,0 +1,284 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Yarp.ReverseProxy.Configuration; +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +/// +/// Tests for YARP with Service Discovery enabled. +/// +public class YarpServiceDiscoveryTests +{ + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://my-svc/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "https://localhost:8888", + ["services:basket:default:2"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://localhost:8888/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPreferredScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("http://localhost:1111/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .Configure(o => + { + // Allow only "https://" + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + // No results: there are no 'https' endpoints in config and 'http' is disallowed. + Assert.Equal(0, result.Destinations.Count); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Dns() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://microsoft.com", + }, + ["dest-b"] = new() + { + Address = "http://msn.com", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + Assert.NotNull(result); + Assert.NotEmpty(result.Destinations); + Assert.All(result.Destinations, d => + { + var address = d.Value.Address; + Assert.True(Uri.TryCreate(address, default, out var uri), $"Failed to parse address '{address}' as URI."); + Assert.True(uri.IsDefaultPort, "URI should use the default port when resolved via DNS."); + var expectedScheme = d.Key.StartsWith("dest-a") ? "https" : "http"; + Assert.Equal(expectedScheme, uri.Scheme); + }); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), + } + }; + + return Task.FromResult(response); + } + }; + + await using var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(3, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://10.10.10.10:8888/", a), + a => Assert.Equal("https://[::1]:9999/", a), + a => Assert.Equal("https://127.0.0.1:7777/", a)); + } + + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } +} From 40992dab676f8aa7b2c55e46226e882f1a02d9ff Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 1 May 2024 13:14:14 -0700 Subject: [PATCH 048/472] Move Cancel call outside of lock and add additional error handling (#4052) --- .../ServiceEndpointWatcher.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index d361202a698..a94b7b7a3c1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -77,8 +77,10 @@ public ValueTask GetEndpointsAsync(CancellationToken canc async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { ServiceEndpointSource? result; + var disposalToken = _disposalCancellation.Token; do { + disposalToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; @@ -194,7 +196,14 @@ private async Task RefreshAsyncInternal() if (OnEndpointsUpdated is { } callback) { - callback(new(newEndpoints, error)); + try + { + callback(new(newEndpoints, error)); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error notifying observers of updated endpoints."); + } } lock (_lock) @@ -244,10 +253,17 @@ private void SchedulePollingTimer() /// public async ValueTask DisposeAsync() { - lock (_lock) + try { _disposalCancellation.Cancel(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error cancelling disposal cancellation token."); + } + lock (_lock) + { _changeTokenRegistration?.Dispose(); _changeTokenRegistration = null; From dcd995db36b30341157645b0fdee2134832045f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 14:24:23 -0600 Subject: [PATCH 049/472] Improve test coverage for YARP Service Discovery (#4051) Co-authored-by: Reuben Bond --- .../Internal/ServiceEndpointImpl.cs | 1 + ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 4 + .../ServiceDiscoveryDestinationResolver.cs | 32 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 23 ++ .../YarpServiceDiscoveryTests.cs | 284 ++++++++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 6f89cd8e37b..151a9309338 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -14,6 +14,7 @@ internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 => $"[{ip.Address}]", IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index c5145e06528..08f6394daf8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 8ec810f2c05..3de03a6ebde 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.ServiceDiscovery; @@ -14,8 +15,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver +/// The service discovery options. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver, IOptions options) : IDestinationResolver { + private readonly ServiceDiscoveryOptions _options = options.Value; + /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) { @@ -65,13 +69,15 @@ public async ValueTask ResolveDestinationsAsync(I Uri uri; if (!addressString.Contains("://")) { - uri = new Uri($"https://{addressString}"); + var scheme = GetDefaultScheme(originalUri); + uri = new Uri($"{scheme}://{addressString}"); } else { uri = new Uri(addressString); } + uriBuilder.Scheme = uri.Scheme; uriBuilder.Host = uri.Host; uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); @@ -90,4 +96,26 @@ public async ValueTask ResolveDestinationsAsync(I return (results, result.ChangeToken); } + + private string GetDefaultScheme(Uri originalUri) + { + if (originalUri.Scheme.IndexOf('+') > 0) + { + // Use the first allowed scheme. + var specifiedSchemes = originalUri.Scheme.Split('+'); + foreach (var scheme in specifiedSchemes) + { + if (_options.AllowAllSchemes || _options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + return scheme; + } + } + + throw new InvalidOperationException($"None of the specified schemes ('{string.Join(", ", specifiedSchemes)}') are allowed by configuration."); + } + else + { + return originalUri.Scheme; + } + } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj new file mode 100644 index 00000000000..8a816222c9f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs new file mode 100644 index 00000000000..3628da0839f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -0,0 +1,284 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Yarp.ReverseProxy.Configuration; +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +/// +/// Tests for YARP with Service Discovery enabled. +/// +public class YarpServiceDiscoveryTests +{ + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://my-svc/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "https://localhost:8888", + ["services:basket:default:2"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://localhost:8888/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPreferredScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("http://localhost:1111/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .Configure(o => + { + // Allow only "https://" + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + // No results: there are no 'https' endpoints in config and 'http' is disallowed. + Assert.Equal(0, result.Destinations.Count); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Dns() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://microsoft.com", + }, + ["dest-b"] = new() + { + Address = "http://msn.com", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + Assert.NotNull(result); + Assert.NotEmpty(result.Destinations); + Assert.All(result.Destinations, d => + { + var address = d.Value.Address; + Assert.True(Uri.TryCreate(address, default, out var uri), $"Failed to parse address '{address}' as URI."); + Assert.True(uri.IsDefaultPort, "URI should use the default port when resolved via DNS."); + var expectedScheme = d.Key.StartsWith("dest-a") ? "https" : "http"; + Assert.Equal(expectedScheme, uri.Scheme); + }); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), + } + }; + + return Task.FromResult(response); + } + }; + + await using var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(3, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://10.10.10.10:8888/", a), + a => Assert.Equal("https://[::1]:9999/", a), + a => Assert.Equal("https://127.0.0.1:7777/", a)); + } + + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } +} From 3f610e1ba52b7ff7a44bce08443dc980e874b43b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 15:22:38 -0700 Subject: [PATCH 050/472] Promptly remove invalid resolvers (#4039) Co-authored-by: ReubenBond --- .../Http/HttpServiceEndpointResolver.cs | 5 ++ .../ServiceEndpointResolver.cs | 6 +++ .../ServiceEndpointWatcher.cs | 48 +++++++++++++------ .../DnsServiceEndpointResolverTests.cs | 35 ++++++++++++++ .../DnsSrvServiceEndpointResolverTests.cs | 19 +------- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 1 + 6 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index e11f593776f..e547ab14138 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -57,6 +57,10 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ return endpoint; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -140,6 +144,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 92df120940d..e928980700c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -49,6 +49,7 @@ public async ValueTask GetEndpointsAsync(string serviceNa while (true) { ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); var resolver = _resolvers.GetOrAdd( serviceName, static (name, self) => self.CreateResolver(name), @@ -64,6 +65,10 @@ public async ValueTask GetEndpointsAsync(string serviceNa return result; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -148,6 +153,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index ba6df9b43c4..d361202a698 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -32,6 +32,7 @@ internal sealed partial class ServiceEndpointWatcher( private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; + private IDisposable? _changeTokenRegistration; /// /// Gets the service name. @@ -60,6 +61,8 @@ public void Start() public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoProviders(); + ObjectDisposedException.ThrowIf(_disposalCancellation.IsCancellationRequested, this); + cancellationToken.ThrowIfCancellationRequested(); // If the cache is valid, return the cached value. if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) @@ -76,9 +79,11 @@ async ValueTask GetEndpointsInternal(CancellationToken ca ServiceEndpointSource? result; do { + cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; } while (result is null); + return result; } } @@ -89,7 +94,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) + if (!_disposalCancellation.IsCancellationRequested && _refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -128,10 +133,18 @@ private async Task RefreshAsyncInternal() CacheStatus newCacheState; try { + lock (_lock) + { + // Dispose the existing change token registration, if any. + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + } + Log.ResolvingEndpoints(_logger, ServiceName); var builder = new ServiceEndpointBuilder(); foreach (var provider in _providers) { + cancellationToken.ThrowIfCancellationRequested(); await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } @@ -143,13 +156,12 @@ private async Task RefreshAsyncInternal() // Check if we need to poll for updates or if we can register for change notification callbacks. if (endpoints.ChangeToken.ActiveChangeCallbacks) { - // Initiate a background refresh, if necessary. - endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + // Initiate a background refresh when the change token fires. + _changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); + + // Dispose the existing timer, if any, since we are reliant on change tokens for updates. + _pollingTimer?.Dispose(); + _pollingTimer = null; } else { @@ -211,6 +223,13 @@ private void SchedulePollingTimer() { lock (_lock) { + if (_disposalCancellation.IsCancellationRequested) + { + _pollingTimer?.Dispose(); + _pollingTimer = null; + return; + } + if (_pollingTimer is null) { _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); @@ -227,14 +246,15 @@ public async ValueTask DisposeAsync() { lock (_lock) { - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + _disposalCancellation.Cancel(); + + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + + _pollingTimer?.Dispose(); + _pollingTimer = null; } - _disposalCancellation.Cancel(); if (_refreshTask is { } task) { await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs new file mode 100644 index 00000000000..2b3a7fd7cd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServiceEndpointResolverTests +{ + [Fact] + public async Task ResolveServiceEndpoint_Dns_MultiShot() + { + var timeProvider = new FakeTimeProvider(); + var services = new ServiceCollection() + .AddSingleton(timeProvider) + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) + .BuildServiceProvider(); + var resolver = services.GetRequiredService(); + var initialResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(initialResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(7)); + var secondResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(secondResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(80)); + var thirdResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(thirdResult); + Assert.True(initialResult.Endpoints.Count > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 7cadb4e3c7f..3449c075047 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndpoint_Dns() + public async Task ResolveServiceEndpoint_DnsSrv() { var dnsClientMock = new FakeDnsClient { @@ -134,7 +134,7 @@ public async Task ResolveServiceEndpoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -233,19 +233,4 @@ public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(boo } } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index ba827640199..31fe0f9d687 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -9,6 +9,7 @@ + From 5e625228a817ee0cbb8ebd24a1b69666116ad6ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 23:38:28 +0000 Subject: [PATCH 051/472] Move Cancel call outside of lock and add additional error handling (#4055) Co-authored-by: Reuben Bond --- .../ServiceEndpointWatcher.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index d361202a698..a94b7b7a3c1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -77,8 +77,10 @@ public ValueTask GetEndpointsAsync(CancellationToken canc async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { ServiceEndpointSource? result; + var disposalToken = _disposalCancellation.Token; do { + disposalToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; @@ -194,7 +196,14 @@ private async Task RefreshAsyncInternal() if (OnEndpointsUpdated is { } callback) { - callback(new(newEndpoints, error)); + try + { + callback(new(newEndpoints, error)); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error notifying observers of updated endpoints."); + } } lock (_lock) @@ -244,10 +253,17 @@ private void SchedulePollingTimer() /// public async ValueTask DisposeAsync() { - lock (_lock) + try { _disposalCancellation.Cancel(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error cancelling disposal cancellation token."); + } + lock (_lock) + { _changeTokenRegistration?.Dispose(); _changeTokenRegistration = null; From 1441356999a97e54572501024eb60b11259ffb9b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 3 May 2024 09:43:48 -0700 Subject: [PATCH 052/472] YARP: add special-case for localhost when setting Host value (#4069) --- .../ServiceDiscoveryDestinationResolver.cs | 24 +++- .../YarpServiceDiscoveryTests.cs | 109 ++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 3de03a6ebde..113e243565e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -55,7 +55,6 @@ public async ValueTask ResolveDestinationsAsync(I CancellationToken cancellationToken) { var originalUri = new Uri(originalConfig.Address); - var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); @@ -90,7 +89,28 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + string? resolvedHost; + + // Use the configured 'Host' value if it is provided. + if (!string.IsNullOrEmpty(originalConfig.Host)) + { + resolvedHost = originalConfig.Host; + } + else if (uri.IsLoopback) + { + // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. + // This is to account for non-wildcard development certificate. + resolvedHost = null; + } + else + { + // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] + // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host + // i.e, use Authority and not Host. + resolvedHost = originalUri.Authority; + } + + var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 3628da0839f..f2264e46411 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -18,6 +18,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; /// public class YarpServiceDiscoveryTests { + private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider) + { + var coreResolver = serviceProvider.GetRequiredService(); + return new ServiceDiscoveryDestinationResolver( + coreResolver, + serviceProvider.GetRequiredService>()); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() { @@ -25,8 +33,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -106,6 +111,89 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref a => Assert.Equal("http://localhost:1111/", a)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost) + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:1111", + ["services:basket:default:1"] = "https://127.0.0.1:2222", + ["services:basket:default:2"] = "https://[::1]:3333", + ["services:basket:default:3"] = "https://baskets-galore.faketld", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var yarpResolver = CreateResolver(services); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://basket", + Host = configHasHost ? "my-basket-svc.faketld" : null + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(4, result.Destinations.Count); + Assert.Collection(result.Destinations.Values, + a => + { + Assert.Equal("https://localhost:1111/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://127.0.0.1:2222/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://[::1]:3333/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://baskets-galore.faketld/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + // For non-localhost values, fallback to the input address. + Assert.Equal("basket", a.Host); + } + }); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() { @@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo }) .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { From 2855a7d98b6d4a4abd4265fb8b2efdf5b959c848 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 17:26:05 -0700 Subject: [PATCH 053/472] YARP: add special-case for localhost when setting Host value (#4076) Co-authored-by: Reuben Bond --- .../ServiceDiscoveryDestinationResolver.cs | 24 +++- .../YarpServiceDiscoveryTests.cs | 109 ++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 3de03a6ebde..113e243565e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -55,7 +55,6 @@ public async ValueTask ResolveDestinationsAsync(I CancellationToken cancellationToken) { var originalUri = new Uri(originalConfig.Address); - var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); @@ -90,7 +89,28 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + string? resolvedHost; + + // Use the configured 'Host' value if it is provided. + if (!string.IsNullOrEmpty(originalConfig.Host)) + { + resolvedHost = originalConfig.Host; + } + else if (uri.IsLoopback) + { + // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. + // This is to account for non-wildcard development certificate. + resolvedHost = null; + } + else + { + // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] + // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host + // i.e, use Authority and not Host. + resolvedHost = originalUri.Authority; + } + + var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 3628da0839f..f2264e46411 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -18,6 +18,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; /// public class YarpServiceDiscoveryTests { + private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider) + { + var coreResolver = serviceProvider.GetRequiredService(); + return new ServiceDiscoveryDestinationResolver( + coreResolver, + serviceProvider.GetRequiredService>()); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() { @@ -25,8 +33,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -106,6 +111,89 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref a => Assert.Equal("http://localhost:1111/", a)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost) + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:1111", + ["services:basket:default:1"] = "https://127.0.0.1:2222", + ["services:basket:default:2"] = "https://[::1]:3333", + ["services:basket:default:3"] = "https://baskets-galore.faketld", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var yarpResolver = CreateResolver(services); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://basket", + Host = configHasHost ? "my-basket-svc.faketld" : null + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(4, result.Destinations.Count); + Assert.Collection(result.Destinations.Values, + a => + { + Assert.Equal("https://localhost:1111/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://127.0.0.1:2222/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://[::1]:3333/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://baskets-galore.faketld/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + // For non-localhost values, fallback to the input address. + Assert.Equal("basket", a.Host); + } + }); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() { @@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo }) .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { From 74cfe110cac6d8dd63e18891d2d645a829664f00 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 12 Jun 2024 11:24:23 -0700 Subject: [PATCH 054/472] Mark shipped public api as such and add infrastructure for PublicApiAnalyzers (#4467) * Add Microsoft.CodeAnalysis.PublicApiAnalyzers Marking the API we shipped stable as Shipped * Add new API added in main to Unshipped lists * Adding more new API as unshipped * Enable Package Validation on all packages to prevent breaking changes * Update Versions.props Co-authored-by: Igor Velikorossov * Add PR suggestion --------- Co-authored-by: Igor Velikorossov --- .../PublicAPI.Shipped.txt | 27 ++++++++++++++++ .../PublicAPI.Unshipped.txt | 28 +--------------- .../PublicAPI.Shipped.txt | 31 ++++++++++++++++++ .../PublicAPI.Unshipped.txt | 32 +------------------ .../PublicAPI.Shipped.txt | 4 +++ .../PublicAPI.Unshipped.txt | 5 +-- .../PublicAPI.Shipped.txt | 29 +++++++++++++++++ .../PublicAPI.Unshipped.txt | 30 +---------------- 8 files changed, 95 insertions(+), 91 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt index 7dc5c58110b..b55e2b696ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt @@ -1 +1,28 @@ #nullable enable +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt index b55e2b696ec..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt @@ -1,28 +1,2 @@ #nullable enable -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt index 7dc5c58110b..aa1fee77235 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt @@ -1 +1,32 @@ #nullable enable +Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt index aa1fee77235..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt @@ -1,32 +1,2 @@ #nullable enable -Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt index 7dc5c58110b..55d92fd4caa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt index 55d92fd4caa..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt @@ -1,5 +1,2 @@ #nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt index 7dc5c58110b..b3be4048a2d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt @@ -1 +1,30 @@ #nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt index b3be4048a2d..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt @@ -1,30 +1,2 @@ #nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + From d29308cad06d4933a6c005a5d4ffc1ec664ec514 Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Tue, 18 Jun 2024 16:56:25 +1000 Subject: [PATCH 055/472] Baseline code coverage (#4313) Resolves #4293 --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index e98dc409e76..f0edb07ec01 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -9,6 +9,10 @@ Microsoft.Extensions.ServiceDiscovery + + 82 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 0d4c3cbeac2..c49ed456b30 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,6 +8,10 @@ $(DefaultDotnetIconFullPath) + + 51 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 08f6394daf8..04154ce9f9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -10,6 +10,10 @@ $(DefaultDotnetIconFullPath) + + 72 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 9a5d67db04e..69aaefc1075 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -8,6 +8,10 @@ $(DefaultDotnetIconFullPath) + + 81 + + From cb6df3de2ea980c59f7e054ffa72cb41872fff44 Mon Sep 17 00:00:00 2001 From: Valentin Hamm <88094233+vha-schleupen@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:08:26 +0200 Subject: [PATCH 056/472] Filter additional records from DNS SRV response (#4463) --- .../DnsSrvServiceEndpointProvider.cs | 2 +- .../DnsSrvServiceEndpointResolverTests.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index dd17a7e2732..c174cda4f68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -42,7 +42,7 @@ protected override async Task ResolveAsyncCore() } var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals) + foreach (var record in result.Additionals.Where(x => x is AddressRecord or CNameRecord)) { ttl = MinTtl(record, ttl); lookupMapping[record.DomainName] = record; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 3449c075047..0a6e27974db 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -91,7 +91,8 @@ public async Task ResolveServiceEndpoint_DnsSrv() { new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), + new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) } }; @@ -152,7 +153,8 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( { new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), + new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) } }; From 037ae58abb07590890ffb4804601bc2c9f3e08e7 Mon Sep 17 00:00:00 2001 From: ZLoo Date: Fri, 9 Aug 2024 20:41:26 +0300 Subject: [PATCH 057/472] Adding public API test coverage (#5225) --- ...DiscoveryDnsServiceCollectionExtensions.cs | 20 ++++- .../DnsServicePublicApiTests.cs | 81 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 17544d09486..98b9de1fd68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -23,7 +23,12 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + return services.AddDnsSrvServiceEndpointProvider(_ => { }); + } /// /// Adds DNS SRV service discovery to the . @@ -37,6 +42,9 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.TryAddSingleton(); services.AddSingleton(); @@ -53,7 +61,12 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + return services.AddDnsServiceEndpointProvider(_ => { }); + } /// /// Adds DNS service discovery to the . @@ -66,6 +79,9 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC /// public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.AddSingleton(); var options = services.AddOptions(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs new file mode 100644 index 00000000000..e347deb9822 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServicePublicApiTests +{ + [Fact] + public void AddDnsSrvServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddDnsSrvServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddDnsSrvServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + IServiceCollection services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddDnsSrvServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddDnsServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + IServiceCollection services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } +} From 204a929fc7f94d2c289c2d31d65120294e3f3234 Mon Sep 17 00:00:00 2001 From: ZLoo Date: Wed, 14 Aug 2024 10:43:19 +0300 Subject: [PATCH 058/472] Adding public API test coverage for Microsoft.Extensions.ServiceDiscovery.Yarp --- ...ReverseProxyServiceCollectionExtensions.cs | 6 +++ .../YarpServiceDiscoveryPublicApiTests.cs | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index 9f473fd3a9b..de74dc0fc24 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs @@ -17,6 +17,8 @@ public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions /// public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.AddServiceDiscoveryCore(); builder.Services.AddSingleton(); return builder; @@ -27,6 +29,8 @@ public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this I /// public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + return services.AddHttpForwarder().AddServiceDiscoveryForwarderFactory(); } @@ -35,6 +39,8 @@ public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServ /// public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + services.AddServiceDiscoveryCore(); services.AddSingleton(); return services; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs new file mode 100644 index 00000000000..a3b694c6d70 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +#pragma warning disable IDE0200 + +public class YarpServiceDiscoveryPublicApiTests +{ + [Fact] + public void AddServiceDiscoveryDestinationResolverShouldThrowWhenBuilderIsNull() + { + IReverseProxyBuilder builder = null!; + + var action = () => builder.AddServiceDiscoveryDestinationResolver(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddHttpForwarderWithServiceDiscoveryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddHttpForwarderWithServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryForwarderFactoryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscoveryForwarderFactory(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } +} From a53b7ba2663df3e21b2e7df438e7ad9306d65fd0 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 9 Sep 2024 10:36:47 +1000 Subject: [PATCH 059/472] use static for classes with all static members (#5485) --- .../Configuration/ConfigurationServiceEndpointProvider.Log.cs | 2 +- .../PassThrough/PassThroughServiceEndpointProvider.Log.cs | 2 +- .../ServiceEndpointWatcher.Log.cs | 2 +- .../ServiceEndpointWatcherFactory.Log.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index 48c922a85b3..b27c5ea9190 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed partial class ConfigurationServiceEndpointProvider { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index f9a984cfe4f..9f6e9ce0ccb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; internal sealed partial class PassThroughServiceEndpointProvider { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index fce9f667b40..8acaa55ee73 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; partial class ServiceEndpointWatcher { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] public static partial void ResolvingEndpoints(ILogger logger, string serviceName); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 5f4acc89874..449ee6920de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; partial class ServiceEndpointWatcherFactory { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); From 3294486990cd4f26a55338f6f1faf4ab6f10f6d7 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 11 Sep 2024 14:10:43 -0400 Subject: [PATCH 060/472] Upgrade tooling for 9.0x (#5483) * Update dependencies from ".NET 9 Eng" channel Updating 'Microsoft.SourceBuild.Intermediate.source-build-reference-packages': '8.0.0-alpha.1.23516.4' => '9.0.0-alpha.1.24453.2' (from build '20240903.2' of 'https://github.com/dotnet/source-build-reference-packages') Updating 'Microsoft.SourceBuild.Intermediate.arcade': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Arcade.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Build.Tasks.Installers': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Build.Tasks.Workloads': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Helix.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.RemoteExecutor': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.SharedFramework.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.XUnitExtensions': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') * Add dotnet 8 versions for testing * [tests] Install 8.0 runtime for workload testing * Resolve xUnit2013 warning `Do not use Assert.Equal() to check for collection size. Use Assert.Single instead.xUnit2013` * Resolve xUnit2029 warning `Do not use Assert.Empty() to check if a value does not exist in a collection. Use Assert.DoesNotContain() instead.xUnit2029` * Track API change in EqualException * Ignore warning xUnit1030 `error xUnit1030: Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)` * Fix xUnit1012 analyzer warning. Warning of the form: `error xUnit1012: Null should not be used for type parameter 'secondaryApiKey' of type 'string'. Use a non-null value, or convert the parameter to a nullable type. (https://xunit.net/xunit.analyzers/rules/xUnit1012)` * Fix compile error. `error CS0121: The call is ambiguous between the following methods or properties: 'Assert.Contains(T, HashSet)' and 'Assert.Contains(T, SortedSet)'` * couple more fixes * Update nuget.config to use dotnet9 sources instead of dotnet8 * Remove unnecessary package reference to Microsoft.DotNet.XunitAssert * Address xUnit1030 warning. `error xUnit1030: Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)` * Ignore some lint warnings for eng/common/template-guidance.md from arcade * cleanup * Fix AzureFunctionsEndToEnd * Track changes for Aspire.Hosting.Azure.Functions * Fix CA2007: src\Aspire.Hosting\Health\ResourceHealthCheckScheduler.cs(38,45): error CA2007: Consider calling ConfigureAwait on the awaited task * Add dependency to Microsoft.DotNet.XliffTasks from dotnet/arcade * Add back skipping hosting.sdk, and projectTemplates packages .. in workload testing as it is required for the internal build. `Aspire.Hosting.Sdk.Msi.arm64.9.0.0-preview.4.24460.2.nupkg, Aspire.Hosting.Sdk.Msi.x64.9.0.0-preview.4.24460.2.nupkg, Aspire.Hosting.Sdk.Msi.x86.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.arm64.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.x64.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.x86.9.0.0-preview.4.24460.2.nupkg` * [tests] Increase the default timeout from 10mins to 15mins for basictests .. like the various hosting integration tests. The run time can vary especially if docker needs to fetch an image. --- ...sions.ServiceDiscovery.Abstractions.csproj | 2 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- ...crosoft.Extensions.ServiceDiscovery.csproj | 2 +- .../DnsSrvServiceEndpointResolverTests.cs | 4 +-- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 2 +- ...nfigurationServiceEndpointResolverTests.cs | 34 +++++++++---------- ...t.Extensions.ServiceDiscovery.Tests.csproj | 2 +- ...PassThroughServiceEndpointResolverTests.cs | 6 ++-- .../ServiceEndpointResolverTests.cs | 22 ++++++------ ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 2 +- .../YarpServiceDiscoveryTests.cs | 8 ++--- 12 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index f0edb07ec01..733a192e93c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index c49ed456b30..3f503049e83 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 04154ce9f9b..a29425781b2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 69aaefc1075..a9c4eaa1f8c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides extensions to HttpClient that enable service discovery based on configuration. diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 0a6e27974db..b58d9e2f4ec 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -112,7 +112,7 @@ public async Task ResolveServiceEndpoint_DnsSrv() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); @@ -199,7 +199,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); Assert.True(initialResult.ResolvedSuccessfully); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 31fe0f9d687..38a313a9249 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6955cc1e8e2..9fc8832fa68 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -37,7 +37,7 @@ public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -81,7 +81,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Empty(initialResult.EndpointSource.Endpoints); @@ -95,7 +95,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -110,7 +110,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -125,7 +125,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -165,10 +165,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => @@ -187,10 +187,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } @@ -202,10 +202,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } @@ -256,12 +256,12 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_Resol var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); if (expectedResult is not null) { - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); } else @@ -296,7 +296,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); @@ -318,7 +318,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); @@ -363,7 +363,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); @@ -408,7 +408,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 73bc9ec1eab..9fb61145211 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index e0af5c03ed4..f8cc2f282e1 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -32,7 +32,7 @@ public async Task ResolveServiceEndpoint_PassThrough() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -63,7 +63,7 @@ public async Task ResolveServiceEndpoint_Superseded() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); @@ -96,7 +96,7 @@ public async Task ResolveServiceEndpoint_Fallback() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index 16950e67374..0e08c07271e 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -109,7 +109,7 @@ public async Task ResolveServiceEndpoint() await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); - var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None); Assert.NotNull(initialResult); var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); @@ -121,7 +121,7 @@ public async Task ResolveServiceEndpoint() Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); - var resolverResult = await tcs.Task.ConfigureAwait(false); + var resolverResult = await tcs.Task; Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); Assert.Equal(2, resolverResult.EndpointSource.Endpoints.Count); @@ -158,14 +158,14 @@ public async Task ResolveServiceEndpointOneShot() var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None); Assert.NotNull(initialResult); var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); - await services.DisposeAsync().ConfigureAwait(false); + await services.DisposeAsync(); } [Fact] @@ -196,13 +196,13 @@ public async Task ResolveHttpServiceEndpointOneShot() Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); - var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None); Assert.NotNull(endpoint); var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); - await services.DisposeAsync().ConfigureAwait(false); + await services.DisposeAsync(); } [Fact] @@ -242,7 +242,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); - var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var initialEndpoints = await initialEndpointsTask; Assert.NotNull(initialEndpoints); @@ -257,7 +257,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); - }).ConfigureAwait(false); + }); Assert.Equal("throwing", exception.Message); @@ -269,8 +269,8 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() cts[0].Cancel(); sem.Release(1); var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); - await resolveTask.ConfigureAwait(false); - var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + await resolveTask; + var next = await channel.Reader.ReadAsync(CancellationToken.None); if (next.ResolvedSuccessfully) { break; @@ -279,7 +279,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); - var result = await task.ConfigureAwait(false); + var result = await task; Assert.NotSame(initialEndpoints, result); var sep = Assert.Single(result.Endpoints); var ip = Assert.IsType(sep.EndPoint); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 8a816222c9f..567c2254b81 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index f2264e46411..5efb4a98c2c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -45,7 +45,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("https://my-svc/", a)); } @@ -76,7 +76,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("https://localhost:8888/", a)); } @@ -106,7 +106,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("http://localhost:1111/", a)); } @@ -226,7 +226,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); // No results: there are no 'https' endpoints in config and 'http' is disallowed. - Assert.Equal(0, result.Destinations.Count); + Assert.Empty(result.Destinations); } [Fact] From a8c389b01f045ca9569720c43dfd745132c6fca5 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 5 Dec 2024 17:28:39 +0100 Subject: [PATCH 061/472] Do not set host if it's not explicitely set in config (#6862) --- .../ServiceDiscoveryDestinationResolver.cs | 15 +-------------- .../YarpServiceDiscoveryTests.cs | 3 +-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 113e243565e..2ca456ec911 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -89,26 +89,13 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - string? resolvedHost; + string? resolvedHost = null; // Use the configured 'Host' value if it is provided. if (!string.IsNullOrEmpty(originalConfig.Host)) { resolvedHost = originalConfig.Host; } - else if (uri.IsLoopback) - { - // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. - // This is to account for non-wildcard development certificate. - resolvedHost = null; - } - else - { - // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] - // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host - // i.e, use Authority and not Host. - resolvedHost = originalUri.Authority; - } var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 5efb4a98c2c..96fd46d47ea 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -188,8 +188,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Va } else { - // For non-localhost values, fallback to the input address. - Assert.Equal("basket", a.Host); + Assert.Null(a.Host); } }); } From cb4a39bee2f86b70d14cbc4ae5e3cfba604e1795 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Sun, 2 Feb 2025 10:16:55 -0800 Subject: [PATCH 062/472] Adding workflow to automatically compare public API surface against previous release (#7369) * Add generate-api-diffs workflow that sends PRs with updated API surface area * [create-pull-request] automated change --------- Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- ...xtensions.ServiceDiscovery.Abstractions.cs | 71 +++++++++++++++++++ ...crosoft.Extensions.ServiceDiscovery.Dns.cs | 52 ++++++++++++++ ...rosoft.Extensions.ServiceDiscovery.Yarp.cs | 19 +++++ .../Microsoft.Extensions.ServiceDiscovery.cs | 68 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs new file mode 100644 index 00000000000..a7ed4ec5404 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.ServiceDiscovery +{ + public partial interface IHostNameFeature + { + string HostName { get; } + } + + public partial interface IServiceEndpointBuilder + { + System.Collections.Generic.IList Endpoints { get; } + + AspNetCore.Http.Features.IFeatureCollection Features { get; } + + void AddChangeToken(Primitives.IChangeToken changeToken); + } + + public partial interface IServiceEndpointProvider : System.IAsyncDisposable + { + System.Threading.Tasks.ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, System.Threading.CancellationToken cancellationToken); + } + + public partial interface IServiceEndpointProviderFactory + { + bool TryCreateProvider(ServiceEndpointQuery query, out IServiceEndpointProvider? provider); + } + + public abstract partial class ServiceEndpoint + { + public abstract System.Net.EndPoint EndPoint { get; } + public abstract AspNetCore.Http.Features.IFeatureCollection Features { get; } + + public static ServiceEndpoint Create(System.Net.EndPoint endPoint, AspNetCore.Http.Features.IFeatureCollection? features = null) { throw null; } + } + + public sealed partial class ServiceEndpointQuery + { + internal ServiceEndpointQuery() { } + + public string? EndpointName { get { throw null; } } + + public System.Collections.Generic.IReadOnlyList IncludedSchemes { get { throw null; } } + + public string ServiceName { get { throw null; } } + + public override string? ToString() { throw null; } + + public static bool TryParse(string input, out ServiceEndpointQuery? query) { throw null; } + } + + [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] + public sealed partial class ServiceEndpointSource + { + public ServiceEndpointSource(System.Collections.Generic.List? endpoints, Primitives.IChangeToken changeToken, AspNetCore.Http.Features.IFeatureCollection features) { } + + public Primitives.IChangeToken ChangeToken { get { throw null; } } + + public System.Collections.Generic.IReadOnlyList Endpoints { get { throw null; } } + + public AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } + + public override string ToString() { throw null; } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs new file mode 100644 index 00000000000..15f99b179ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.Hosting +{ + public static partial class ServiceDiscoveryDnsServiceCollectionExtensions + { + public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery.Dns +{ + public partial class DnsServiceEndpointProviderOptions + { + public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } + + public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } + + public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } + + public double RetryBackOffFactor { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } + + public partial class DnsSrvServiceEndpointProviderOptions + { + public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } + + public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } + + public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } + + public string? QuerySuffix { get { throw null; } set { } } + + public double RetryBackOffFactor { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs new file mode 100644 index 00000000000..fc608f86a92 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs @@ -0,0 +1,19 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class ServiceDiscoveryReverseProxyServiceCollectionExtensions + { + public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { throw null; } + + public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { throw null; } + + public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { throw null; } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs new file mode 100644 index 00000000000..a6ba654085e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class ServiceDiscoveryHttpClientBuilderExtensions + { + public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { throw null; } + } + + public static partial class ServiceDiscoveryServiceCollectionExtensions + { + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery +{ + public sealed partial class ConfigurationServiceEndpointProviderOptions + { + public string SectionName { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } + + public sealed partial class ServiceDiscoveryOptions + { + public bool AllowAllSchemes { get { throw null; } set { } } + + public System.Collections.Generic.IList AllowedSchemes { get { throw null; } set { } } + + public System.TimeSpan RefreshPeriod { get { throw null; } set { } } + } + + public sealed partial class ServiceEndpointResolver : System.IAsyncDisposable + { + internal ServiceEndpointResolver() { } + + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + + public System.Threading.Tasks.ValueTask GetEndpointsAsync(string serviceName, System.Threading.CancellationToken cancellationToken) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery.Http +{ + public partial interface IServiceDiscoveryHttpMessageHandlerFactory + { + System.Net.Http.HttpMessageHandler CreateHandler(System.Net.Http.HttpMessageHandler handler); + } +} \ No newline at end of file From 763344cb20d62b0e429201e8cdc78eb2be94a66c Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 3 Feb 2025 15:08:59 -0600 Subject: [PATCH 063/472] Remove PublicApiAnalyzer (#7389) This is no longer necessary after #7369 --- .../PublicAPI.Shipped.txt | 28 ---------------- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 32 ------------------- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 5 --- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 30 ----------------- .../PublicAPI.Unshipped.txt | 2 -- 8 files changed, 103 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt deleted file mode 100644 index b55e2b696ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt +++ /dev/null @@ -1,28 +0,0 @@ -#nullable enable -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt deleted file mode 100644 index aa1fee77235..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable -Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt deleted file mode 100644 index 55d92fd4caa..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt +++ /dev/null @@ -1,5 +0,0 @@ -#nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt deleted file mode 100644 index b3be4048a2d..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt +++ /dev/null @@ -1,30 +0,0 @@ -#nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - From 9a34a4584a331f4fb9295117a2cb7efb09fc96db Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 3 Mar 2025 13:41:46 -0500 Subject: [PATCH 064/472] [ci] Remove code coverage reporting from the pipeline (#7857) * [ci] Remove codecoverage from the pipeline as it is not being used * Remove unneeded MinCodeCoverage property * Remove ProjectStaging.targets * remove more code coverage report references * fix build * address review feedback from @ eerhardt --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ---- 4 files changed, 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 733a192e93c..6f412779689 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -9,10 +9,6 @@ Microsoft.Extensions.ServiceDiscovery - - 82 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 3f503049e83..9854c93c01a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - 51 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index a29425781b2..74870a87668 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -10,10 +10,6 @@ $(DefaultDotnetIconFullPath) - - 72 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index a9c4eaa1f8c..59c1c31fee9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - 81 - - From ec982ceeaca866ec8be110157470c2063cddf30a Mon Sep 17 00:00:00 2001 From: ZLoo Date: Tue, 4 Mar 2025 07:02:28 +0300 Subject: [PATCH 065/472] Adding test coverage - validate arguments of public methods (#7575) * update qdrant public api tests * update redis public api tests * update python public api tests * update testing public api tests * update sql server public api tests * update rabbitMQ public api tests * update postgre sql public api tests * update nodeJs public api tests * update nats public api tests * update my sql public api tests * update mongo db public api tests * update milvus public api tests * update keycloak public api tests * update kafka public api tests * update garnet public api tests * update elasticsearch public api tests * add azure aI open aI public api tests * update azure data tables public api tests * update messaging event hub public api tests * update messaging service bus public api tests * update messaging web pub sub public api tests * update search documents public api tests * update security key vault public api tests * update storage blobs public api tests * update storage queues public api test * update confluent kafka public api test * update elastic clients elasticsearch public api test * update keycloack authentication public api test * update microsoft azure cosmos public api tests * change HostApplicationBuilder to Host.CreateEmptyApplicationBuilder * change IHostApplicationBuilder to var * update microsoft data sql client public api tests * update microsoft entity framework core cosmos public api tests * update microsoft entity framework core sql server public api tests * update milvus client public api tests * update mongo db driver public api tests * update my sql connector public api tests * update nats net public api tests * update confluent kafka public api tests * update npgsql entity framework core postgre sql public api tests * update npgsql public api tests * update open ai public api tests * update oracle entity framework core public api tests * update pomelo entity framework core my sql public api tests * update qdrant client public api tests * update rabbit mq client public api tests * update seq public api tests * update stack exchange redis distributed caching public api tests * update stack exchange redis output caching public api tests * update stack exchang redis public api tests * update extensions service discovery public api tests * fix python tests * update oracle public api tests * update valkey public api tests * update app configuration, app containers, application insights, cognitive services, cosmos db public api tests * add Aspire.Hosting.Azure.Tests * fix duplicate * Fix MR by feedback --- .../ServiceEndpoint.cs | 7 +- .../ServiceEndpointQuery.cs | 2 + .../ServiceEndpointSource.cs | 1 + ...iceDiscoveryHttpClientBuilderExtensions.cs | 2 + ...iceDiscoveryServiceCollectionExtensions.cs | 22 +- .../ExtensionsServicePublicApiTests.cs | 218 ++++++++++++++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index 238e383a957..33e0eff4d69 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs @@ -28,5 +28,10 @@ public abstract class ServiceEndpoint /// The endpoint being represented. /// Features of the endpoint. /// A newly initialized . - public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) + { + ArgumentNullException.ThrowIfNull(endPoint); + + return new ServiceEndpointImpl(endPoint, features); + } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 600dc5cc28c..20a17d0878f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -35,6 +35,8 @@ private ServiceEndpointQuery(string originalString, string[] includedSchemes, st /// if the value was successfully parsed; otherwise . public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { + ArgumentException.ThrowIfNullOrEmpty(input); + bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index fb5bff1b288..28d987a2f34 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -25,6 +25,7 @@ public sealed class ServiceEndpointSource public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); + ArgumentNullException.ThrowIfNull(features); _endpoints = endpoints; Features = features; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index 8a137aad4f8..7d5b94c10c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -21,6 +21,8 @@ public static class ServiceDiscoveryHttpClientBuilderExtensions /// The builder. public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { + ArgumentNullException.ThrowIfNull(httpClientBuilder); + var services = httpClientBuilder.Services; services.AddServiceDiscoveryCore(); httpClientBuilder.AddHttpMessageHandler(services => diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a5d789b7e4e..8de759af1f6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -24,11 +24,9 @@ public static class ServiceDiscoveryServiceCollectionExtensions /// The service collection. /// The service collection. public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) - { - return services.AddServiceDiscoveryCore() + => AddServiceDiscoveryCore(services) .AddConfigurationServiceEndpointProvider() .AddPassThroughServiceEndpointProvider(); - } /// /// Adds the core service discovery services and configures defaults. @@ -37,18 +35,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The delegate used to configure service discovery options. /// The service collection. public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action configureOptions) - { - return services.AddServiceDiscoveryCore(configureOptions: configureOptions) + => AddServiceDiscoveryCore(services, configureOptions: configureOptions) .AddConfigurationServiceEndpointProvider() .AddPassThroughServiceEndpointProvider(); - } /// /// Adds the core service discovery services. /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => AddServiceDiscoveryCore(services, configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -58,6 +54,9 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); @@ -80,9 +79,7 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection /// The service collection. /// The service collection. public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) - { - return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); - } + => AddConfigurationServiceEndpointProvider(services, configureOptions: _ => { }); /// /// Configures a service discovery endpoint provider which uses to resolve endpoints. @@ -92,6 +89,9 @@ public static IServiceCollection AddConfigurationServiceEndpointProvider(this IS /// The service collection. public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.AddSingleton(); services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); @@ -110,6 +110,8 @@ public static IServiceCollection AddConfigurationServiceEndpointProvider(this IS /// The service collection. public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + services.AddServiceDiscoveryCore(); services.AddSingleton(); return services; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs new file mode 100644 index 00000000000..31781cf6722 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +#pragma warning disable IDE0200 + +public class ExtensionsServicePublicApiTests +{ + [Fact] + public void AddServiceDiscoveryShouldThrowWhenHttpClientBuilderIsNull() + { + IHttpClientBuilder httpClientBuilder = null!; + + var action = () => httpClientBuilder.AddServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(httpClientBuilder), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddServiceDiscovery(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddServiceDiscovery(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscoveryCore(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddServiceDiscoveryCore(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddServiceDiscoveryCore(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddConfigurationServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddConfigurationServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddConfigurationServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddPassThroughServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddPassThroughServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public async Task GetEndpointsAsyncShouldThrowWhenServiceNameIsNull() + { + var serviceEndpointWatcherFactory = new ServiceEndpointWatcherFactory( + new List(), + new Logger(new NullLoggerFactory()), + Options.Options.Create(new ServiceDiscoveryOptions()), + TimeProvider.System); + + var serviceEndpointResolver = new ServiceEndpointResolver(serviceEndpointWatcherFactory, TimeProvider.System); + string serviceName = null!; + + var action = async () => await serviceEndpointResolver.GetEndpointsAsync(serviceName, CancellationToken.None); + + var exception = await Assert.ThrowsAsync(action); + Assert.Equal(nameof(serviceName), exception.ParamName); + } + + [Fact] + public void CreateShouldThrowWhenEndPointIsNull() + { + EndPoint endPoint = null!; + + var action = () => ServiceEndpoint.Create(endPoint); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(endPoint), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryParseShouldThrowWhenEndPointIsNullOrEmpty(bool isNull) + { + var input = isNull ? null! : string.Empty; + + var action = () => + { + _ = ServiceEndpointQuery.TryParse(input, out _); + }; + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(input), exception.ParamName); + } + + [Fact] + public void CtorServiceEndpointSourceShouldThrowWhenChangeTokenIsNull() + { + IChangeToken changeToken = null!; + var features = new FeatureCollection(); + List? endpoints = null; + + var action = () => new ServiceEndpointSource(endpoints, changeToken, features); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(changeToken), exception.ParamName); + } + + [Fact] + public void CtorServiceEndpointSourceShouldThrowWhenFeaturesIsNull() + { + var changeToken = NullChangeToken.Singleton; + IFeatureCollection features = null!; + List? endpoints = null; + + var action = () => new ServiceEndpointSource(endpoints, changeToken, features); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(features), exception.ParamName); + } +} From 45867cee592a77eac08db8f94cb52cb965299f23 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Wed, 2 Apr 2025 08:17:13 +0200 Subject: [PATCH 066/472] Migrate to xunit.v3 (#8403) * Update XUnit extensions from Arcade * Update packages and Arcade test runner * Move custom attributes to xunit.v3 * Fix usings * Move from ConditionalFact/ConditionalTheory to Assert.Skip * Suppress xUnit1030 * Remove unnecessary using * Few fixes * Exe * Param doc * Add OutputType * Adjust for helix * Produce binlog * Specify DOTNET_ROOT for executables to find the correct runtime * Fix build errors * Fix failing test This test used to pass with VSTest and xunit v2 because the tests were run under testhost.exe. So, DistributedApplicationOptions.Assembly was pointing to testhost. Then ResolveProjectDirectory wouldn't correctly find AppHostProjectPath. In DistributedApplicationBuilder constructor, we were falling back to _innerBuilder.Environment.ContentRootPath because ProjectDirectory is null. With xunit.v3 *or* MTP, generally when the test app is executed as normal executable, we have the right assembly and we are able to resolve project directory correctly * Fix for Arcade * Fix test * Move to props * Specify DOTNET_ROOT * Fix ExtractTestClassNames for local SDK usage as well * Set executable bit * Set xunit.analyzers version * Disable analyzer for now * Disable analyzer for now * Move to Directory.Build.props as NoWarn * Adjust * Disable more warnings * Suppress more warnings * Add back to props * fix merge * fix merge * fix merge * Remove unused properties for helix run * Disable xunit1051 from the project file for Playground, and Templates tests * add reference to issue * Update QuarantinedTestAttribute for xunit v3 * More fixes for merging main * Adjust for QuarantinedTest * More adjustments for merging main --------- Co-authored-by: Ankit Jain --- ...soft.Extensions.ServiceDiscovery.Dns.Tests.csproj | 2 +- ...icrosoft.Extensions.ServiceDiscovery.Tests.csproj | 2 +- .../ServiceEndpointTests.cs | 12 ++++++------ ...oft.Extensions.ServiceDiscovery.Yarp.Tests.csproj | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 38a313a9249..24faf1d8abe 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 9fb61145211..269081ae5d7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs index e05d0818e1a..2943074c2b3 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -10,16 +10,16 @@ public class ServiceEndpointTests { public static TheoryData ZeroPortEndPoints => new() { - IPEndPoint.Parse("127.0.0.1:0"), - new DnsEndPoint("microsoft.com", 0), - new UriEndPoint(new Uri("https://microsoft.com")) + (EndPoint)IPEndPoint.Parse("127.0.0.1:0"), + (EndPoint)new DnsEndPoint("microsoft.com", 0), + (EndPoint)new UriEndPoint(new Uri("https://microsoft.com")) }; public static TheoryData NonZeroPortEndPoints => new() { - IPEndPoint.Parse("127.0.0.1:8443"), - new DnsEndPoint("microsoft.com", 8443), - new UriEndPoint(new Uri("https://microsoft.com:8443")) + (EndPoint)IPEndPoint.Parse("127.0.0.1:8443"), + (EndPoint)new DnsEndPoint("microsoft.com", 8443), + (EndPoint)new UriEndPoint(new Uri("https://microsoft.com:8443")) }; [Theory] diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 567c2254b81..296a3dcd861 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -8,7 +8,7 @@ - + From f3feb150ba13400734336536a59983b256b2564c Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Tue, 29 Apr 2025 09:36:16 -0600 Subject: [PATCH 067/472] template updates for 9.3 (#8975) * remove 8.2 * 9.1 to 9.3 * remove 8.2 and 9.1 folders * Add 9.3 content * set concrete versions in 9.2 projects * 9.3 default * loc strings * update readmes for AppHost.cs * update tests for AppHost.cs --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 6c47ee67507..1073af1cc19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -40,7 +40,7 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. +In the _AppHost.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); From 7a2ce7bcf3db861faf9e1badb57b37e4911b8447 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 8 May 2025 08:39:35 -0400 Subject: [PATCH 068/472] Translate OpenAI refusals to ErrorContent (#6393) Refusals in OpenAI are errors reported when the service can't generate an output that matches the requested schema. Translate refusals to ErrorContent now that we have it. --- .../OpenAIChatClient.cs | 40 ++++++++++--------- .../OpenAIResponseChatClient.cs | 28 ++++++++++++- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index c78b495393b..dd4c0e28c71 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -155,21 +155,22 @@ void IDisposable.Dispose() foreach (var content in input.Contents) { - if (content is FunctionCallContent callRequest) + switch (content) { - message.ToolCalls.Add( - ChatToolCall.CreateFunctionToolCall( - callRequest.CallId, - callRequest.Name, - new(JsonSerializer.SerializeToUtf8Bytes( - callRequest.Arguments, - options.GetTypeInfo(typeof(IDictionary)))))); - } - } + case ErrorContent errorContent when errorContent.ErrorCode is nameof(message.Refusal): + message.Refusal = errorContent.Message; + break; - if (input.AdditionalProperties?.TryGetValue(nameof(message.Refusal), out string? refusal) is true) - { - message.Refusal = refusal; + case FunctionCallContent callRequest: + message.ToolCalls.Add( + ChatToolCall.CreateFunctionToolCall( + callRequest.CallId, + callRequest.Name, + new(JsonSerializer.SerializeToUtf8Bytes( + callRequest.Arguments, + options.GetTypeInfo(typeof(IDictionary)))))); + break; + } } yield return message; @@ -370,7 +371,7 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha // add it to this function calling item. if (refusal is not null) { - (responseUpdate.AdditionalProperties ??= [])[nameof(ChatMessageContentPart.Refusal)] = refusal.ToString(); + responseUpdate.Contents.Add(new ErrorContent(refusal.ToString()) { ErrorCode = "Refusal" }); } // Propagate additional relevant metadata. @@ -450,6 +451,12 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple } } + // And add error content for any refusals, which represent errors in generating output that conforms to a provided schema. + if (openAICompletion.Refusal is string refusal) + { + returnMessage.Contents.Add(new ErrorContent(refusal) { ErrorCode = nameof(openAICompletion.Refusal) }); + } + // Wrap the content in a ChatResponse to return. var response = new ChatResponse(returnMessage) { @@ -470,11 +477,6 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple (response.AdditionalProperties ??= [])[nameof(openAICompletion.ContentTokenLogProbabilities)] = contentTokenLogProbs; } - if (openAICompletion.Refusal is string refusal) - { - (response.AdditionalProperties ??= [])[nameof(openAICompletion.Refusal)] = refusal; - } - if (openAICompletion.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs) { (response.AdditionalProperties ??= [])[nameof(openAICompletion.RefusalTokenLogProbabilities)] = refusalTokenLogProbs; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 92224379c10..0c6af51bb81 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -263,6 +263,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( MessageId = lastMessageId, ModelId = modelId, ResponseId = responseId, + Role = lastRole, ConversationId = responseId, Contents = [ @@ -274,6 +275,19 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ], }; break; + + case StreamingResponseRefusalDoneUpdate refusalDone: + yield return new ChatResponseUpdate + { + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + ResponseId = responseId, + Role = lastRole, + ConversationId = responseId, + Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], + }; + break; } } } @@ -539,9 +553,15 @@ private static List ToAIContents(IEnumerable con foreach (ResponseContentPart part in contents) { - if (part.Kind == ResponseContentPartKind.OutputText) + switch (part.Kind) { - results.Add(new TextContent(part.Text)); + case ResponseContentPartKind.OutputText: + results.Add(new TextContent(part.Text)); + break; + + case ResponseContentPartKind.Refusal: + results.Add(new ErrorContent(part.Refusal) { ErrorCode = nameof(ResponseContentPartKind.Refusal) }); + break; } } @@ -572,6 +592,10 @@ private static List ToOpenAIResponsesContent(IList Date: Thu, 8 May 2025 09:18:33 -0400 Subject: [PATCH 069/472] Rename useJsonSchema parameter (#6394) * Rename useJsonSchema parameter The `GetResponseAsync` methods accept a `bool?` parameter currently called `useJsonSchema`. This is confusing, because the whole point of the method is to create and use a JSON schema from the `T`. The parameter actually controls _how_ that schema is used, whether it's included as part of the messages (false), as part of a ChatResponseFormat in the ChatOptions (true), or up to the system to decide. I've clarified it by renaming it from `useJsonSchema` to `useJsonSchemaResponseFormat`. It's wordier, but it disambiguates the intent. * Update src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs Co-authored-by: Eirik Tsarpalis --------- Co-authored-by: Eirik Tsarpalis --- .../ChatClientStructuredOutputExtensions.cs | 40 +++++++++---------- .../ChatClientIntegrationTests.cs | 2 +- ...atClientStructuredOutputExtensionsTests.cs | 2 +- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs index 0e7b20bade6..d7bc12a1a41 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs @@ -33,7 +33,7 @@ public static class ChatClientStructuredOutputExtensions /// The . /// The chat content to send. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . The default is . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. /// @@ -44,19 +44,17 @@ public static Task> GetResponseAsync( this IChatClient chatClient, IEnumerable messages, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) => - GetResponseAsync(chatClient, messages, AIJsonUtilities.DefaultOptions, options, useJsonSchema, cancellationToken); + GetResponseAsync(chatClient, messages, AIJsonUtilities.DefaultOptions, options, useJsonSchemaResponseFormat, cancellationToken); /// Sends a user chat text message, requesting a response matching the type . /// The . /// The text content for the chat message to send. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. - /// If not specified, the default value is determined by the implementation. - /// If a specific value is required, it must be specified by the caller. /// /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. @@ -66,15 +64,15 @@ public static Task> GetResponseAsync( this IChatClient chatClient, string chatMessage, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) => - GetResponseAsync(chatClient, new ChatMessage(ChatRole.User, chatMessage), options, useJsonSchema, cancellationToken); + GetResponseAsync(chatClient, new ChatMessage(ChatRole.User, chatMessage), options, useJsonSchemaResponseFormat, cancellationToken); /// Sends a chat message, requesting a response matching the type . /// The . /// The chat message to send. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . The default is . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. /// @@ -85,16 +83,16 @@ public static Task> GetResponseAsync( this IChatClient chatClient, ChatMessage chatMessage, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) => - GetResponseAsync(chatClient, [chatMessage], options, useJsonSchema, cancellationToken); + GetResponseAsync(chatClient, [chatMessage], options, useJsonSchemaResponseFormat, cancellationToken); /// Sends a user chat text message, requesting a response matching the type . /// The . /// The text content for the chat message to send. /// The JSON serialization options to use. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . The default is . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. /// @@ -106,16 +104,16 @@ public static Task> GetResponseAsync( string chatMessage, JsonSerializerOptions serializerOptions, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) => - GetResponseAsync(chatClient, new ChatMessage(ChatRole.User, chatMessage), serializerOptions, options, useJsonSchema, cancellationToken); + GetResponseAsync(chatClient, new ChatMessage(ChatRole.User, chatMessage), serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken); /// Sends a chat message, requesting a response matching the type . /// The . /// The chat message to send. /// The JSON serialization options to use. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . The default is . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. /// @@ -127,16 +125,16 @@ public static Task> GetResponseAsync( ChatMessage chatMessage, JsonSerializerOptions serializerOptions, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) => - GetResponseAsync(chatClient, [chatMessage], serializerOptions, options, useJsonSchema, cancellationToken); + GetResponseAsync(chatClient, [chatMessage], serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken); /// Sends chat messages, requesting a response matching the type . /// The . /// The chat content to send. /// The JSON serialization options to use. /// The chat options to configure the request. - /// + /// /// to set a JSON schema on the ; otherwise, . The default is . /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. /// @@ -149,7 +147,7 @@ public static async Task> GetResponseAsync( IEnumerable messages, JsonSerializerOptions serializerOptions, ChatOptions? options = null, - bool? useJsonSchema = null, + bool? useJsonSchemaResponseFormat = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(chatClient); @@ -192,8 +190,8 @@ public static async Task> GetResponseAsync( // We default to assuming that models support JSON schema because developers will normally use // GetResponseAsync only with models that do. If the model doesn't support JSON schema, it may - // throw or it may ignore the schema. In these cases developers should pass useJsonSchema: false. - if (useJsonSchema.GetValueOrDefault(true)) + // throw or it may ignore the schema. In these cases developers should pass useJsonSchemaResponseFormat: false. + if (useJsonSchemaResponseFormat ?? true) { // When using native structured output, we don't add any additional prompt, because // the LLM backend is meant to do whatever's needed to explain the schema to the LLM. diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 22531e14e22..0e4311a227a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -942,7 +942,7 @@ public virtual async Task GetResponseAsync_StructuredOutput_NonNative() var response = await captureOutputChatClient.GetResponseAsync(""" Supply an object to represent Jimbo Smith from Cardiff. - """, useJsonSchema: false); + """, useJsonSchemaResponseFormat: false); Assert.Equal("Jimbo Smith", response.Result.FullName); Assert.Contains("Cardiff", response.Result.HomeTown); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs index 557eecc3c29..aae985c4c4b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs @@ -139,7 +139,7 @@ public async Task SuccessUsage_NoJsonSchema() }; var chatHistory = new List { new(ChatRole.User, "Hello") }; - var response = await client.GetResponseAsync(chatHistory, useJsonSchema: false, serializerOptions: JsonContext2.Default.Options); + var response = await client.GetResponseAsync(chatHistory, useJsonSchemaResponseFormat: false, serializerOptions: JsonContext2.Default.Options); // The response contains the deserialized result and other response properties Assert.Equal(1, response.Result.Id); From 8271d4e7a3fba362915660eb81c92f39ab3364f6 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 8 May 2025 14:55:08 +0100 Subject: [PATCH 070/472] Add JSON schema transformation functionality to `AIJsonUtilities` (#6383) * Add initial schema transformation functionality and incorporate into the OpenAI leaf client. * Update all leaf client implementions, improve naming, add testing. * Remove redundant suppressions * Address feedback. --- eng/MSBuild/LegacySupport.props | 4 + src/LegacySupport/SystemIndex/Index.cs | 160 +++++++++++++ ...icrosoft.Extensions.AI.Abstractions.csproj | 1 + .../Utilities/AIJsonSchemaTransformCache.cs | 78 ++++++ .../Utilities/AIJsonSchemaTransformContext.cs | 45 ++++ .../Utilities/AIJsonSchemaTransformOptions.cs | 45 ++++ ...ma.cs => AIJsonUtilities.Schema.Create.cs} | 1 + .../AIJsonUtilities.Schema.Transform.cs | 188 +++++++++++++++ .../AzureAIInferenceChatClient.cs | 12 +- .../OllamaChatClient.cs | 9 +- .../OpenAIChatClient.cs | 15 +- .../OpenAIResponseChatClient.cs | 4 +- .../AIJsonSchemaTransformCacheTests.cs | 81 +++++++ .../Utilities/AIJsonUtilitiesTests.cs | 225 ++++++++++++++++++ 14 files changed, 860 insertions(+), 8 deletions(-) create mode 100644 src/LegacySupport/SystemIndex/Index.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs rename src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/{AIJsonUtilities.Schema.cs => AIJsonUtilities.Schema.Create.cs} (99%) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonSchemaTransformCacheTests.cs diff --git a/eng/MSBuild/LegacySupport.props b/eng/MSBuild/LegacySupport.props index 15d34725d84..6b110acaaa1 100644 --- a/eng/MSBuild/LegacySupport.props +++ b/eng/MSBuild/LegacySupport.props @@ -7,6 +7,10 @@ + + + + diff --git a/src/LegacySupport/SystemIndex/Index.cs b/src/LegacySupport/SystemIndex/Index.cs new file mode 100644 index 00000000000..7285d669e71 --- /dev/null +++ b/src/LegacySupport/SystemIndex/Index.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#pragma warning disable CS0436 // Type conflicts with imported type +#pragma warning disable S3427 // Method overloads with default parameter values should not overlap +#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text +#pragma warning disable IDE0011 // Add braces +#pragma warning disable SA1623 // Property summary documentation should match accessors +#pragma warning disable IDE0023 // Use block body for conversion operator +#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning disable LA0001 // Use the 'Microsoft.Shared.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +#pragma warning disable CA1305 // Specify IFormatProvider + +namespace System +{ + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object. + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object. + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index 27a2c5d0513..dc6896b7f53 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -28,6 +28,7 @@ true true true + true diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs new file mode 100644 index 00000000000..a1aaeff26ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs @@ -0,0 +1,78 @@ +// 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.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Defines a cache for JSON schemas transformed according to the specified policy. +/// +/// +/// +/// This cache stores weak references from AI abstractions that declare JSON schemas such as or +/// to their corresponding JSON schemas transformed according to the specified policy. It is intended for use by +/// implementations that enforce vendor-specific restrictions on what constitutes a valid JSON schema for a given function or response format. +/// +/// +/// It is recommended implementations with schema transformation requirements should create a single static instance of this cache. +/// +/// +public sealed class AIJsonSchemaTransformCache +{ + private readonly ConditionalWeakTable _functionSchemaCache = new(); + private readonly ConditionalWeakTable _responseFormatCache = new(); + + private readonly ConditionalWeakTable.CreateValueCallback _functionSchemaCreateValueCallback; + private readonly ConditionalWeakTable.CreateValueCallback _responseFormatCreateValueCallback; + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The options governing schema transformation. + public AIJsonSchemaTransformCache(AIJsonSchemaTransformOptions transformOptions) + { + _ = Throw.IfNull(transformOptions); + + if (transformOptions == AIJsonSchemaTransformOptions.Default) + { + Throw.ArgumentException(nameof(transformOptions), "The options instance does not specify any transformations."); + } + + TransformOptions = transformOptions; + _functionSchemaCreateValueCallback = function => AIJsonUtilities.TransformSchema(function.JsonSchema, TransformOptions); + _responseFormatCreateValueCallback = responseFormat => AIJsonUtilities.TransformSchema(responseFormat.Schema!.Value, TransformOptions); + } + + /// + /// Gets the options governing schema transformation. + /// + public AIJsonSchemaTransformOptions TransformOptions { get; } + + /// + /// Gets or creates a transformed JSON schema for the specified instance. + /// + /// The function whose JSON schema we want to transform. + /// The transformed JSON schema corresponding to . + public JsonElement GetOrCreateTransformedSchema(AIFunction function) + { + _ = Throw.IfNull(function); + return (JsonElement)_functionSchemaCache.GetValue(function, _functionSchemaCreateValueCallback); + } + + /// + /// Gets or creates a transformed JSON schema for the specified instance. + /// + /// The response format whose JSON schema we want to transform. + /// The transformed JSON schema corresponding to . + public JsonElement? GetOrCreateTransformedSchema(ChatResponseFormatJson responseFormat) + { + _ = Throw.IfNull(responseFormat); + return responseFormat.Schema is not null + ? (JsonElement?)_responseFormatCache.GetValue(responseFormat, _responseFormatCreateValueCallback) + : null; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformContext.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformContext.cs new file mode 100644 index 00000000000..4cfd08e160b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformContext.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +using System; + +namespace Microsoft.Extensions.AI; + +/// +/// Defines the context for transforming a schema node withing a larger schema document. +/// +/// +/// This struct is being passed to the user-provided +/// callback by the method and cannot be instantiated directly. +/// +public readonly struct AIJsonSchemaTransformContext +{ + private readonly string[] _path; + + internal AIJsonSchemaTransformContext(string[] path) + { + _path = path; + } + + /// + /// Gets the path to the schema document currently being generated. + /// + public ReadOnlySpan Path => _path; + + /// + /// Gets the containing property name if the current schema is a property of an object. + /// + public string? PropertyName => Path is [.., "properties", string name] ? name : null; + + /// + /// Gets a value indicating whether the current schema is a collection element. + /// + public bool IsCollectionElementSchema => Path is [.., "items"]; + + /// + /// Gets a value indicating whether the current schema is a dictionary value. + /// + public bool IsDictionaryValueSchema => Path is [.., "additionalProperties"]; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs new file mode 100644 index 00000000000..c7a035cbbed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S1067 // Expressions should not be too complex + +using System; +using System.Text.Json.Nodes; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides options for configuring the behavior of JSON schema transformation functionality. +/// +public sealed record class AIJsonSchemaTransformOptions +{ + /// + /// Gets a callback that is invoked for every schema that is generated within the type graph. + /// + public Func? TransformSchemaNode { get; init; } + + /// + /// Gets a value indicating whether to convert boolean schemas to equivalent object-based representations. + /// + public bool ConvertBooleanSchemas { get; init; } + + /// + /// Gets a value indicating whether to generate schemas with the additionalProperties set to false for .NET objects. + /// + public bool DisallowAdditionalProperties { get; init; } + + /// + /// Gets a value indicating whether to mark all properties as required in the schema. + /// + public bool RequireAllProperties { get; init; } + + /// + /// Gets a value indicating whether to substitute nullable "type" keywords with OpenAPI 3.0 style "nullable" keywords in the schema. + /// + public bool UseNullableKeyword { get; init; } + + /// + /// Gets the default options instance. + /// + internal static AIJsonSchemaTransformOptions Default { get; } = new(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs similarity index 99% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index a0afa66f98c..fe17a2ad449 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -34,6 +34,7 @@ public static partial class AIJsonUtilities private const string PatternPropertyName = "pattern"; private const string EnumPropertyName = "enum"; private const string PropertiesPropertyName = "properties"; + private const string ItemsPropertyName = "items"; private const string RequiredPropertyName = "required"; private const string AdditionalPropertiesPropertyName = "additionalProperties"; private const string DefaultPropertyName = "default"; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs new file mode 100644 index 00000000000..5669a3fb264 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs @@ -0,0 +1,188 @@ +// 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; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +public static partial class AIJsonUtilities +{ + /// + /// Transforms the given JSON schema based on the provided options. + /// + /// The schema document to transform. + /// The options governing schema transformation. + /// A new schema document with transformations applied. + /// The schema and any nested schemas are transformed using depth-first traversal. + public static JsonElement TransformSchema(JsonElement schema, AIJsonSchemaTransformOptions transformOptions) + { + _ = Throw.IfNull(transformOptions); + + if (transformOptions == AIJsonSchemaTransformOptions.Default) + { + Throw.ArgumentException(nameof(transformOptions), "The options instance does not specify any transformations."); + } + + JsonNode? nodeSchema = JsonSerializer.SerializeToNode(schema, JsonContext.Default.JsonElement); + List? path = transformOptions.TransformSchemaNode is not null ? [] : null; + JsonNode transformedSchema = TransformSchemaCore(nodeSchema, transformOptions, path); + return JsonSerializer.Deserialize(transformedSchema, JsonContext.Default.JsonElement); + } + + private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions, List? path) + { + switch (schema?.GetValueKind()) + { + case JsonValueKind.False: + if (transformOptions.ConvertBooleanSchemas) + { + schema = new JsonObject { [NotPropertyName] = (JsonNode)true }; + } + + break; + + case JsonValueKind.True: + if (transformOptions.ConvertBooleanSchemas) + { + schema = new JsonObject(); + } + + break; + + case JsonValueKind.Object: + JsonObject schemaObj = (JsonObject)schema; + JsonObject? properties = null; + + // Step 1. Recursively apply transformations to any nested schemas we might be able to detect. + if (schemaObj.TryGetPropertyValue(PropertiesPropertyName, out JsonNode? props) && props is JsonObject propsObj) + { + properties = propsObj; + path?.Add(PropertiesPropertyName); + foreach (var prop in properties.ToArray()) + { + path?.Add(prop.Key); + properties[prop.Key] = TransformSchemaCore(prop.Value, transformOptions, path); + path?.RemoveAt(path.Count - 1); + } + + path?.RemoveAt(path.Count - 1); + } + + if (schemaObj.TryGetPropertyValue(ItemsPropertyName, out JsonNode? itemsSchema)) + { + path?.Add(ItemsPropertyName); + schemaObj[ItemsPropertyName] = TransformSchemaCore(itemsSchema, transformOptions, path); + path?.RemoveAt(path.Count - 1); + } + + if (schemaObj.TryGetPropertyValue(AdditionalPropertiesPropertyName, out JsonNode? additionalProps) && + additionalProps?.GetValueKind() is not JsonValueKind.False) + { + path?.Add(AdditionalPropertiesPropertyName); + schemaObj[AdditionalPropertiesPropertyName] = TransformSchemaCore(additionalProps, transformOptions, path); + path?.RemoveAt(path.Count - 1); + } + + if (schemaObj.TryGetPropertyValue(NotPropertyName, out JsonNode? notSchema)) + { + path?.Add(NotPropertyName); + schemaObj[NotPropertyName] = TransformSchemaCore(notSchema, transformOptions, path); + path?.RemoveAt(path.Count - 1); + } + + // Traverse keywords that contain arrays of schemas + ReadOnlySpan combinatorKeywords = ["anyOf", "oneOf", "allOf"]; + foreach (string combinatorKeyword in combinatorKeywords) + { + if (schemaObj.TryGetPropertyValue(combinatorKeyword, out JsonNode? combinatorSchema) && combinatorSchema is JsonArray combinatorArray) + { + path?.Add(combinatorKeyword); + for (int i = 0; i < combinatorArray.Count; i++) + { + path?.Add($"[{i}]"); + JsonNode element = TransformSchemaCore(combinatorArray[i], transformOptions, path); + if (!ReferenceEquals(element, combinatorArray[i])) + { + combinatorArray[i] = element; + } + + path?.RemoveAt(path.Count - 1); + } + + path?.RemoveAt(path.Count - 1); + } + } + + // Step 2. Apply node-level transformations per the settings. + if (transformOptions.DisallowAdditionalProperties && properties is not null && !schemaObj.ContainsKey(AdditionalPropertiesPropertyName)) + { + schemaObj[AdditionalPropertiesPropertyName] = (JsonNode)false; + } + + if (transformOptions.RequireAllProperties && properties is not null) + { + JsonArray requiredProps = []; + foreach (var prop in properties) + { + requiredProps.Add((JsonNode)prop.Key); + } + + schemaObj[RequiredPropertyName] = requiredProps; + } + + if (transformOptions.UseNullableKeyword && + schemaObj.TryGetPropertyValue(TypePropertyName, out JsonNode? typeSchema) && + typeSchema is JsonArray typeArray) + { + bool isNullable = false; + string? foundType = null; + + foreach (JsonNode? typeNode in typeArray) + { + string typeString = (string)typeNode!; + if (typeString is "null") + { + isNullable = true; + continue; + } + + if (foundType is not null) + { + // The array contains more than one non-null types, abort the transformation. + foundType = null; + break; + } + + foundType = typeString; + } + + if (isNullable && foundType is not null) + { + schemaObj["type"] = (JsonNode)foundType; + schemaObj["nullable"] = (JsonNode)true; + } + } + + break; + + default: + Throw.ArgumentException(nameof(schema), "Schema must be an object or a boolean value."); + break; + } + + // Apply user-defined transformations as the final step. + if (transformOptions.TransformSchemaNode is { } transformer) + { + Debug.Assert(path != null, "Path should not be null when TransformSchemaNode is provided."); + schema = transformer(new AIJsonSchemaTransformContext(path!.ToArray()), schema); + } + + return schema; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index ea66cc191e4..97d239aaefc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -24,6 +24,14 @@ namespace Microsoft.Extensions.AI; /// Represents an for an Azure AI Inference . internal sealed class AzureAIInferenceChatClient : IChatClient { + /// Gets the JSON schema transform cache conforming to OpenAI restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. + private static AIJsonSchemaTransformCache SchemaTransformCache { get; } = new(new() + { + RequireAllProperties = true, + DisallowAdditionalProperties = true, + ConvertBooleanSchemas = true + }); + /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -358,7 +366,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon } else if (options.ResponseFormat is ChatResponseFormatJson json) { - if (json.Schema is { } schema) + if (SchemaTransformCache.GetOrCreateTransformedSchema(json) is { } schema) { var tool = JsonSerializer.Deserialize(schema, JsonContext.Default.AzureAIChatToolJson)!; result.ResponseFormat = ChatCompletionsResponseFormat.CreateJsonFormat( @@ -392,7 +400,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) { // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema, JsonContext.Default.AzureAIChatToolJson)!; + var tool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(aiFunction), JsonContext.Default.AzureAIChatToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, JsonContext.Default.AzureAIChatToolJson)); return new(new FunctionDefinition(aiFunction.Name) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs index 210d8f7c92d..28f8eb8c3ad 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs @@ -25,6 +25,11 @@ public sealed class OllamaChatClient : IChatClient { private static readonly JsonElement _schemalessJsonResponseFormatValue = JsonDocument.Parse("\"json\"").RootElement; + private static readonly AIJsonSchemaTransformCache _schemaTransformCache = new(new() + { + ConvertBooleanSchemas = true, + }); + /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -292,7 +297,7 @@ private static FunctionCallContent ToFunctionCallContent(OllamaFunctionToolCall { if (format is ChatResponseFormatJson jsonFormat) { - return jsonFormat.Schema ?? _schemalessJsonResponseFormatValue; + return _schemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) ?? _schemalessJsonResponseFormatValue; } else { @@ -483,7 +488,7 @@ private static OllamaTool ToOllamaTool(AIFunction function) { Name = function.Name, Description = function.Description, - Parameters = JsonSerializer.Deserialize(function.JsonSchema, JsonContext.Default.OllamaFunctionToolParameters)!, + Parameters = JsonSerializer.Deserialize(_schemaTransformCache.GetOrCreateTransformedSchema(function), JsonContext.Default.OllamaFunctionToolParameters)!, } }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index dd4c0e28c71..93cd20a70eb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -24,6 +24,14 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . internal sealed partial class OpenAIChatClient : IChatClient { + /// Gets the JSON schema transformer cache conforming to OpenAI restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. + internal static AIJsonSchemaTransformCache SchemaTransformCache { get; } = new(new() + { + RequireAllProperties = true, + DisallowAdditionalProperties = true, + ConvertBooleanSchemas = true + }); + /// Gets the default OpenAI endpoint. private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); @@ -612,7 +620,7 @@ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) } else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) { - result.ResponseFormat = jsonFormat.Schema is { } jsonSchema ? + result.ResponseFormat = SchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes( @@ -633,8 +641,11 @@ private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) strictObj is bool strictValue ? strictValue : null; + // Perform transformations making the schema legal per OpenAI restrictions + JsonElement jsonSchema = SchemaTransformCache.GetOrCreateTransformedSchema(aiFunction); + // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema, ChatClientJsonContext.Default.ChatToolJson)!; + var tool = JsonSerializer.Deserialize(jsonSchema, ChatClientJsonContext.Default.ChatToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, ChatClientJsonContext.Default.ChatToolJson)); return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 0c6af51bb81..a91ea9abf8f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -373,7 +373,7 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio switch (tool) { case AIFunction af: - var oaitool = JsonSerializer.Deserialize(af.JsonSchema, ResponseClientJsonContext.Default.ResponseToolJson)!; + var oaitool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(af), ResponseClientJsonContext.Default.ResponseToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(oaitool, ResponseClientJsonContext.Default.ResponseToolJson)); result.Tools.Add(ResponseTool.CreateFunctionTool(af.Name, af.Description, functionParameters)); break; @@ -428,7 +428,7 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio { result.TextOptions = new() { - TextFormat = jsonFormat.Schema is { } jsonSchema ? + TextFormat = SchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? ResponseTextFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, ResponseClientJsonContext.Default.JsonElement)), diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonSchemaTransformCacheTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonSchemaTransformCacheTests.cs new file mode 100644 index 00000000000..4233e5cdbe1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonSchemaTransformCacheTests.cs @@ -0,0 +1,81 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.AI.Utilities; + +public static class AIJsonSchemaTransformCacheTests +{ + [Fact] + public static void NullOptions_ThrowsArgumentNullException() + { + Assert.Throws(() => new AIJsonSchemaTransformCache(transformOptions: null!)); + } + + [Fact] + public static void EmptyOptions_ThrowsArgumentException() + { + Assert.Throws(() => new AIJsonSchemaTransformCache(transformOptions: new())); + } + + [Fact] + public static void TransformOptions_ReturnsExpectedValue() + { + AIJsonSchemaTransformOptions options = new() { ConvertBooleanSchemas = true }; + AIJsonSchemaTransformCache cache = new(options); + Assert.Same(options, cache.TransformOptions); + } + + [Fact] + public static void NullFunction_ThrowsArgumentNullException() + { + AIJsonSchemaTransformCache cache = new(new() { ConvertBooleanSchemas = true }); + Assert.Throws(() => cache.GetOrCreateTransformedSchema(function: null!)); + } + + [Fact] + public static void NullResponseFormat_ThrowsArgumentNullException() + { + AIJsonSchemaTransformCache cache = new(new() { ConvertBooleanSchemas = true }); + Assert.Throws(() => cache.GetOrCreateTransformedSchema(responseFormat: null!)); + } + + [Fact] + public static void FunctionSchema_ReturnsExpectedResults() + { + AIJsonSchemaTransformCache cache = new(new() { TransformSchemaNode = (_, node) => { node.AsObject().Add("myAwesomeKeyword", 42); return node; } }); + + AIFunction func = AIFunctionFactory.Create((int x, int y) => x + y); + JsonElement transformedSchema = cache.GetOrCreateTransformedSchema(func); + Assert.True(transformedSchema.TryGetProperty("myAwesomeKeyword", out _)); + + JsonElement transformedSchema2 = cache.GetOrCreateTransformedSchema(func); + Assert.Equal(transformedSchema, transformedSchema2); + } + + [Fact] + public static void ChatResponseFormat_ReturnsExpectedResults() + { + AIJsonSchemaTransformCache cache = new(new() { TransformSchemaNode = (_, node) => { node.AsObject().Add("myAwesomeKeyword", 42); return node; } }); + + JsonElement schema = JsonDocument.Parse("{}").RootElement; + ChatResponseFormatJson responseFormat = ChatResponseFormat.ForJsonSchema(schema); + JsonElement? transformedSchema = cache.GetOrCreateTransformedSchema(responseFormat); + Assert.NotNull(transformedSchema); + Assert.True(transformedSchema.Value.TryGetProperty("myAwesomeKeyword", out _)); + + JsonElement? transformedSchema2 = cache.GetOrCreateTransformedSchema(responseFormat); + Assert.Equal(transformedSchema, transformedSchema2); + } + + [Fact] + public static void ChatResponseFormat_NullFormatReturnsNullSchema() + { + AIJsonSchemaTransformCache cache = new(new() { TransformSchemaNode = (_, node) => { node.AsObject().Add("myAwesomeKeyword", 42); return node; } }); + JsonElement? transformedSchema = cache.GetOrCreateTransformedSchema(ChatResponseFormat.Json); + Assert.Null(transformedSchema); + } +} 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 c4141a4bf0d..2d1967b11c1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -566,6 +566,231 @@ public static void CreateFunctionJsonSchema_InvokesIncludeParameterCallbackForEv Assert.Contains("fifth", schemaString); } + [Fact] + public static void TransformJsonSchema_ConvertBooleanSchemas() + { + JsonElement schema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": false } }, + "baz": true + }, + "required": ["foo"] + } + """).RootElement; + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": { "not": true } } }, + "baz": { } + }, + "required": ["foo"] + } + """).RootElement; + + AIJsonSchemaTransformOptions options = new() + { + ConvertBooleanSchemas = true, + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options); + AssertDeepEquals(expectedSchema, transformedSchema); + } + + [Fact] + public static void TransformJsonSchema_RequireAllProperties() + { + JsonElement schema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": false } }, + "baz": true + }, + "required": ["foo"] + } + """).RootElement; + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": false }, "required": ["x"] }, + "baz": true + }, + "required": ["foo", "bar", "baz"] + } + """).RootElement; + + AIJsonSchemaTransformOptions options = new() + { + RequireAllProperties = true, + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options); + AssertDeepEquals(expectedSchema, transformedSchema); + } + + [Fact] + public static void TransformJsonSchema_DisallowAdditionalProperties() + { + JsonElement schema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": false } }, + "baz": true + }, + "required": ["foo"] + } + """).RootElement; + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "properties" : { + "foo": { "type": "string" }, + "bar": { "properties": { "x": false }, "additionalProperties": false }, + "baz": true + }, + "required": ["foo"], + "additionalProperties": false + } + """).RootElement; + + AIJsonSchemaTransformOptions options = new() + { + DisallowAdditionalProperties = true, + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options); + AssertDeepEquals(expectedSchema, transformedSchema); + } + + [Fact] + public static void TransformJsonSchema_UseNullableKeyword() + { + JsonElement schema = JsonDocument.Parse(""" + { + "type": ["object","null"], + "properties" : { + "foo": { "type": ["null","string"] }, + "bar": { "type":"object", "properties": { "x": false } }, + "baz": { "type" : ["string","array","null"] } + }, + "required": ["foo"] + } + """).RootElement; + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties" : { + "foo": { "type": "string", "nullable": true }, + "bar": { "type":"object", "properties": { "x": false } }, + "baz": { "type" : ["string","array","null"] } + }, + "required": ["foo"], + "nullable": true + } + """).RootElement; + + AIJsonSchemaTransformOptions options = new() + { + UseNullableKeyword = true, + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options); + AssertDeepEquals(expectedSchema, transformedSchema); + } + + [Theory] + [MemberData(nameof(TestTypes.GetTestDataUsingAllValues), MemberType = typeof(TestTypes))] + public static void TransformJsonSchema_ValidateWithTestData(ITestData testData) + { + // Stress tests the schema generation method using types from the JsonSchemaExporter test battery. + + JsonSerializerOptions options = testData.Options is { } opts + ? new(opts) { TypeInfoResolver = TestTypes.TestTypesContext.Default } + : TestTypes.TestTypesContext.Default.Options; + + JsonTypeInfo typeInfo = options.GetTypeInfo(testData.Type); + AIJsonSchemaCreateOptions? createOptions = typeInfo.Properties.Any(prop => prop.IsExtensionData) + ? new() { DisallowAdditionalProperties = false } // Do not append additionalProperties: false to the schema if the type has extension data. + : null; + + JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions); + + int totalSchemaNodes = 0; + AIJsonSchemaTransformOptions transformOptions = new() + { + ConvertBooleanSchemas = true, + RequireAllProperties = true, + DisallowAdditionalProperties = true, + UseNullableKeyword = true, + TransformSchemaNode = (context, schema) => + { + totalSchemaNodes++; + var schemaObj = Assert.IsType(schema); + schemaObj.Add("myAwesomeKeyword", (JsonNode)42); + return schemaObj; + } + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, transformOptions); + Assert.True(totalSchemaNodes > 0, "TransformSchema was not invoked."); + + int totalSchemaNodes2 = 0; + transformOptions = new() + { + TransformSchemaNode = (context, schema) => + { + totalSchemaNodes2++; + var schemaObj = Assert.IsType(schema); + Assert.Contains("myAwesomeKeyword", schemaObj); + if (schemaObj.TryGetPropertyValue("properties", out JsonNode? props)) + { + Assert.Contains("required", schemaObj); + Assert.Contains("additionalProperties", schemaObj); + Assert.Equal(((JsonArray)schemaObj["required"]!).Count, ((JsonObject)props!).Count); + } + + if (schemaObj.TryGetPropertyValue("type", out JsonNode? type) && type is JsonArray typeArray) + { + Assert.DoesNotContain("null", typeArray); + } + + return schemaObj; + } + }; + + AIJsonUtilities.TransformSchema(transformedSchema, transformOptions); + Assert.Equal(totalSchemaNodes, totalSchemaNodes2); + } + + [Fact] + public static void TransformJsonSchema_InvalidOptions_ThrowsArgumentException() + { + JsonElement schema = JsonDocument.Parse("{}").RootElement; + Assert.Throws(() => AIJsonUtilities.TransformSchema(schema, transformOptions: null!)); + Assert.Throws(() => AIJsonUtilities.TransformSchema(schema, transformOptions: new())); + } + + [Theory] + [InlineData("null")] + [InlineData("42")] + [InlineData("[1,2,3]")] + [InlineData("""{"properties":{"x": 42 }}""")] + [InlineData("""{"oneOf":[42]}""")] + public static void TransformJsonSchema_InvalidInput_ThrowsArgumentException(string invalidSchema) + { + JsonElement schema = JsonDocument.Parse(invalidSchema).RootElement; + AIJsonSchemaTransformOptions transformOptions = new() { ConvertBooleanSchemas = true }; + Assert.Throws(() => AIJsonUtilities.TransformSchema(schema, transformOptions)); + } + private class DerivedAIContent : AIContent { public int DerivedValue { get; set; } From 3b12514ae51428cad3266c9b78392195ff09e614 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Thu, 8 May 2025 11:45:42 -0700 Subject: [PATCH 071/472] Remove CacheOptions from DiskBasedResponseCache (#6395) Details for this change are available in #6387. Fixes #6387 --- .../Storage/AzureStorageResponseCache.cs | 2 +- .../CSharp/Defaults.cs | 6 +- .../CSharp/JsonSerialization/JsonUtilities.cs | 4 - .../DiskBasedResponseCache.CacheMode.cs | 31 ----- .../DiskBasedResponseCache.CacheOptions.cs | 90 -------------- .../CSharp/Storage/DiskBasedResponseCache.cs | 111 +++++------------- .../Storage/DiskBasedResponseCacheProvider.cs | 16 ++- .../CacheOptionsTests.cs | 73 ------------ .../ResponseCacheTester.cs | 5 +- 9 files changed, 49 insertions(+), 289 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheMode.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheOptions.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/CacheOptionsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs index f7115a37024..5e83b456a0b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs @@ -40,9 +40,9 @@ internal sealed partial class AzureStorageResponseCache( private const string EntryAndContentsFilesNotFound = "Cache entry file {0} and contents file {1} were not found."; private readonly string _iterationPath = $"cache/{scenarioName}/{iterationName}"; + private readonly Func _provideDateTime = provideDateTime; private readonly TimeSpan _timeToLiveForCacheEntries = timeToLiveForCacheEntries ?? Defaults.DefaultTimeToLiveForCacheEntries; - private readonly Func _provideDateTime = provideDateTime; public byte[]? Get(string key) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs index e5cd6b26ec3..095bdee575c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs @@ -31,9 +31,7 @@ public static class Defaults /// public static TimeSpan DefaultTimeToLiveForCacheEntries { get; } = TimeSpan.FromDays(14); - /// - /// Defines the version number for the reporting format. If and when the serialized format undergoes - /// breaking changes, this number will be incremented. - /// + // Defines the version number for the reporting format. If and when the serialized format undergoes + // breaking changes, this number should be incremented. internal const int ReportingFormatVersion = 1; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs index e372b7e8434..9ba74009433 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs @@ -19,7 +19,6 @@ internal static class Default internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: true); internal static JsonTypeInfo DatasetTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); - internal static JsonTypeInfo CacheOptionsTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); } @@ -29,7 +28,6 @@ internal static class Compact internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: false); internal static JsonTypeInfo DatasetTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); - internal static JsonTypeInfo CacheOptionsTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); } @@ -50,12 +48,10 @@ private static JsonSerializerOptions CreateJsonSerializerOptions(bool writeInden [JsonSerializable(typeof(EvaluationResult))] [JsonSerializable(typeof(Dataset))] [JsonSerializable(typeof(CacheEntry))] - [JsonSerializable(typeof(CacheOptions))] [JsonSourceGenerationOptions( Converters = [ typeof(CamelCaseEnumConverter), typeof(CamelCaseEnumConverter), - typeof(CamelCaseEnumConverter), typeof(TimeSpanConverter), typeof(EvaluationContextConverter) ], diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheMode.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheMode.cs deleted file mode 100644 index cbc8ca2f151..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheMode.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; - -internal partial class DiskBasedResponseCache -{ - /// - /// An enum representing the mode in which the cache is operating. - /// - internal enum CacheMode - { - /// - /// In this mode, the cache is disabled. All requests bypass the cache and are forwarded online. - /// - Disabled, - - /// - /// In this mode, the cache is enabled. Requests are handled by the cache first. If a cached response is not - /// available, then the request is forwarded online. - /// - Enabled, - - /// - /// In this mode, the cache is enabled. However, requests are never forwarded online. Instead if a cached response - /// is not available, then an exception is thrown. Additionally in this mode, the cache is considered frozen (or - /// read only) which means that all the cache artifacts (including expired entries) are preserved as is on disk. - /// - EnabledOfflineOnly - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheOptions.cs deleted file mode 100644 index 1e21c59828b..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheOptions.cs +++ /dev/null @@ -1,90 +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.Globalization; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; - -namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; - -internal partial class DiskBasedResponseCache -{ - internal sealed class CacheOptions - { - public static CacheOptions Default { get; } = new CacheOptions(); - - private const string DeserializationFailedMessage = "Unable to deserialize the cache options file at {0}."; - - public CacheOptions(CacheMode mode = CacheMode.Enabled, TimeSpan? timeToLiveForCacheEntries = null) - { - Mode = mode; - TimeToLiveForCacheEntries = timeToLiveForCacheEntries ?? Defaults.DefaultTimeToLiveForCacheEntries; - } - - [JsonConstructor] - public CacheOptions(CacheMode mode, TimeSpan timeToLiveForCacheEntries) - { - Mode = mode; - TimeToLiveForCacheEntries = timeToLiveForCacheEntries; - } - - public CacheMode Mode { get; } - - [JsonPropertyName("timeToLiveInSecondsForCacheEntries")] - public TimeSpan TimeToLiveForCacheEntries { get; } - - public static CacheOptions Read(string cacheOptionsFilePath) - { - using FileStream cacheOptionsFile = File.OpenRead(cacheOptionsFilePath); - - CacheOptions cacheOptions = - JsonSerializer.Deserialize( - cacheOptionsFile, - JsonUtilities.Default.CacheOptionsTypeInfo) ?? - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DeserializationFailedMessage, cacheOptionsFilePath)); - - return cacheOptions; - } - - public static async Task ReadAsync( - string cacheOptionsFilePath, - CancellationToken cancellationToken = default) - { - using FileStream cacheOptionsFile = File.OpenRead(cacheOptionsFilePath); - - CacheOptions cacheOptions = - await JsonSerializer.DeserializeAsync( - cacheOptionsFile, - JsonUtilities.Default.CacheOptionsTypeInfo, - cancellationToken).ConfigureAwait(false) ?? - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DeserializationFailedMessage, cacheOptionsFilePath)); - - return cacheOptions; - } - - public void Write(string cacheOptionsFilePath) - { - using FileStream cacheOptionsFile = File.Create(cacheOptionsFilePath); - JsonSerializer.Serialize(cacheOptionsFile, this, JsonUtilities.Default.CacheOptionsTypeInfo); - } - - public async Task WriteAsync( - string cacheOptionsFilePath, - CancellationToken cancellationToken = default) - { - using FileStream cacheOptionsFile = File.Create(cacheOptionsFilePath); - await JsonSerializer.SerializeAsync( - cacheOptionsFile, - this, - JsonUtilities.Default.CacheOptionsTypeInfo, - cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs index 51b96739964..d0a107d8710 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs @@ -1,6 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable S3604 +// S3604: Member initializer values should not be redundant. +// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary +// constructor syntax. + #pragma warning disable CA1725 // CA1725: Parameter names should match base declaration. // All functions on 'IDistributedCache' use the parameter name 'token' in place of 'cancellationToken'. However, @@ -26,55 +31,42 @@ internal sealed partial class DiskBasedResponseCache : IDistributedCache private readonly string _scenarioName; private readonly string _iterationName; - private readonly CacheOptions _options; private readonly string _iterationPath; private readonly Func _provideDateTime; + private readonly TimeSpan _timeToLiveForCacheEntries; - public DiskBasedResponseCache( + internal DiskBasedResponseCache( string storageRootPath, string scenarioName, string iterationName, - Func provideDateTime) + Func provideDateTime, + TimeSpan? timeToLiveForCacheEntries = null) { _scenarioName = scenarioName; _iterationName = iterationName; storageRootPath = Path.GetFullPath(storageRootPath); string cacheRootPath = GetCacheRootPath(storageRootPath); - string optionsFilePath = GetOptionsFilePath(cacheRootPath); - _options = File.Exists(optionsFilePath) ? CacheOptions.Read(optionsFilePath) : CacheOptions.Default; + _iterationPath = Path.Combine(cacheRootPath, scenarioName, iterationName); _provideDateTime = provideDateTime; + _timeToLiveForCacheEntries = timeToLiveForCacheEntries ?? Defaults.DefaultTimeToLiveForCacheEntries; } public byte[]? Get(string key) { - if (_options.Mode is CacheMode.Disabled) - { - return null; - } - (_, string entryFilePath, string contentsFilePath, bool filesExist) = GetPaths(key); + if (!filesExist) { - return _options.Mode is CacheMode.EnabledOfflineOnly - ? throw new FileNotFoundException( - string.Format( - CultureInfo.CurrentCulture, - EntryAndContentsFilesNotFound, - entryFilePath, - contentsFilePath)) - : null; + return null; } - if (_options.Mode is not CacheMode.EnabledOfflineOnly) + CacheEntry entry = CacheEntry.Read(entryFilePath); + if (entry.Expiration <= _provideDateTime()) { - CacheEntry entry = CacheEntry.Read(entryFilePath); - if (entry.Expiration <= _provideDateTime()) - { - Remove(key); - return null; - } + Remove(key); + return null; } return File.ReadAllBytes(contentsFilePath); @@ -82,34 +74,20 @@ public DiskBasedResponseCache( public async Task GetAsync(string key, CancellationToken cancellationToken = default) { - if (_options.Mode is CacheMode.Disabled) - { - return null; - } - (string _, string entryFilePath, string contentsFilePath, bool filesExist) = GetPaths(key); + if (!filesExist) { - return _options.Mode is CacheMode.EnabledOfflineOnly - ? throw new FileNotFoundException( - string.Format( - CultureInfo.CurrentCulture, - EntryAndContentsFilesNotFound, - entryFilePath, - contentsFilePath)) - : null; + return null; } - if (_options.Mode is not CacheMode.EnabledOfflineOnly) - { - CacheEntry entry = - await CacheEntry.ReadAsync(entryFilePath, cancellationToken: cancellationToken).ConfigureAwait(false); + CacheEntry entry = + await CacheEntry.ReadAsync(entryFilePath, cancellationToken: cancellationToken).ConfigureAwait(false); - if (entry.Expiration <= _provideDateTime()) - { - await RemoveAsync(key, cancellationToken).ConfigureAwait(false); - return null; - } + if (entry.Expiration <= _provideDateTime()) + { + await RemoveAsync(key, cancellationToken).ConfigureAwait(false); + return null; } #if NET @@ -162,12 +140,8 @@ await stream.ReadAsync( public void Refresh(string key) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return; - } - (_, string entryFilePath, string contentsFilePath, bool filesExist) = GetPaths(key); + if (!filesExist) { throw new FileNotFoundException( @@ -184,12 +158,8 @@ public void Refresh(string key) public async Task RefreshAsync(string key, CancellationToken cancellationToken = default) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return; - } - (_, string entryFilePath, string contentsFilePath, bool filesExist) = GetPaths(key); + if (!filesExist) { throw new FileNotFoundException( @@ -206,33 +176,20 @@ public async Task RefreshAsync(string key, CancellationToken cancellationToken = public void Remove(string key) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return; - } - (string keyPath, _, _, _) = GetPaths(key); + Directory.Delete(keyPath, recursive: true); } public Task RemoveAsync(string key, CancellationToken cancellationToken = default) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return Task.CompletedTask; - } - Remove(key); + return Task.CompletedTask; } public void Set(string key, byte[] value, DistributedCacheEntryOptions options) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return; - } - (string keyPath, string entryFilePath, string contentsFilePath, _) = GetPaths(key); _ = Directory.CreateDirectory(keyPath); @@ -249,11 +206,6 @@ public async Task SetAsync( DistributedCacheEntryOptions options, CancellationToken cancellationToken = default) { - if (_options.Mode is CacheMode.Disabled or CacheMode.EnabledOfflineOnly) - { - return; - } - (string keyPath, string entryFilePath, string contentsFilePath, _) = GetPaths(key); Directory.CreateDirectory(keyPath); @@ -334,9 +286,6 @@ await CacheEntry.ReadAsync( private static string GetCacheRootPath(string storageRootPath) => Path.Combine(storageRootPath, "cache"); - private static string GetOptionsFilePath(string cacheRootPath) - => Path.Combine(cacheRootPath, "options.json"); - private static string GetEntryFilePath(string keyPath) => Path.Combine(keyPath, "entry.json"); @@ -368,7 +317,7 @@ private static string GetContentsFilePath(string keyPath) private CacheEntry CreateEntry() { DateTime creation = _provideDateTime(); - DateTime expiration = creation.Add(_options.TimeToLiveForCacheEntries); + DateTime expiration = creation.Add(_timeToLiveForCacheEntries); return new CacheEntry(_scenarioName, _iterationName, creation, expiration); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs index d3fce0e5aff..feb75df1dba 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs @@ -20,7 +20,13 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// /// The path to a directory on disk under which the cached AI responses should be stored. /// -public sealed class DiskBasedResponseCacheProvider(string storageRootPath) : IResponseCacheProvider +/// +/// An optional that specifies the maximum amount of time that cached AI responses should +/// survive in the cache before they are considered expired and evicted. +/// +public sealed class DiskBasedResponseCacheProvider( + string storageRootPath, + TimeSpan? timeToLiveForCacheEntries = null) : IResponseCacheProvider { private readonly Func _provideDateTime = () => DateTime.UtcNow; @@ -39,7 +45,13 @@ public ValueTask GetCacheAsync( string iterationName, CancellationToken cancellationToken = default) { - var cache = new DiskBasedResponseCache(storageRootPath, scenarioName, iterationName, _provideDateTime); + var cache = + new DiskBasedResponseCache( + storageRootPath, + scenarioName, + iterationName, + _provideDateTime, + timeToLiveForCacheEntries); return new ValueTask(cache); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/CacheOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/CacheOptionsTests.cs deleted file mode 100644 index b8351f45695..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/CacheOptionsTests.cs +++ /dev/null @@ -1,73 +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.IO; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; -using Xunit; -using CacheMode = Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCache.CacheMode; -using CacheOptions = Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCache.CacheOptions; - -namespace Microsoft.Extensions.AI.Evaluation.Reporting.Tests; - -public class CacheOptionsTests -{ - [Fact] - public void SerializeCacheOptions() - { - var options = new CacheOptions(CacheMode.Disabled, TimeSpan.FromDays(300)); - - string json = JsonSerializer.Serialize(options, JsonUtilities.Default.CacheOptionsTypeInfo); - CacheOptions? deserialized = JsonSerializer.Deserialize(json, JsonUtilities.Default.CacheOptionsTypeInfo); - - Assert.NotNull(deserialized); - Assert.Equal(options.Mode, deserialized!.Mode); - Assert.Equal(options.TimeToLiveForCacheEntries, deserialized.TimeToLiveForCacheEntries); - - } - - [Fact] - public void SerializeCacheOptionsCompact() - { - var options = new CacheOptions(CacheMode.Disabled, TimeSpan.FromDays(300)); - - string json = JsonSerializer.Serialize(options, JsonUtilities.Compact.CacheOptionsTypeInfo); - CacheOptions? deserialized = JsonSerializer.Deserialize(json, JsonUtilities.Default.CacheOptionsTypeInfo); - - Assert.NotNull(deserialized); - Assert.Equal(options.Mode, deserialized!.Mode); - Assert.Equal(options.TimeToLiveForCacheEntries, deserialized.TimeToLiveForCacheEntries); - - } - - [Fact] - public void SerializeCacheOptionsToFile() - { - var options = new CacheOptions(CacheMode.Enabled, TimeSpan.FromSeconds(10)); - - string tempFilePath = Path.GetTempFileName(); - options.Write(tempFilePath); - CacheOptions deserialized = CacheOptions.Read(tempFilePath); - - Assert.NotNull(deserialized); - Assert.Equal(options.Mode, deserialized.Mode); - Assert.Equal(options.TimeToLiveForCacheEntries, deserialized.TimeToLiveForCacheEntries); - } - - [Fact] - public async Task SerializeCacheOptionsToFileAsync() - { - var options = new CacheOptions(CacheMode.Enabled, TimeSpan.FromSeconds(10)); - - string tempFilePath = Path.GetTempFileName(); - await options.WriteAsync(tempFilePath); - CacheOptions deserialized = await CacheOptions.ReadAsync(tempFilePath); - - Assert.NotNull(deserialized); - Assert.Equal(options.Mode, deserialized.Mode); - Assert.Equal(options.TimeToLiveForCacheEntries, deserialized.TimeToLiveForCacheEntries); - } - -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs index 60e4e6f21ed..76e50244b92 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.Extensions.AI.Evaluation.Reporting.Storage; using Microsoft.Extensions.Caching.Distributed; using Microsoft.TestUtilities; using Xunit; @@ -96,7 +95,7 @@ public async Task CacheEntryExpiration() cache.Set(_keyB, _responseB); Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); - now = DateTime.UtcNow + DiskBasedResponseCache.CacheOptions.Default.TimeToLiveForCacheEntries; + now = DateTime.UtcNow + Defaults.DefaultTimeToLiveForCacheEntries; Assert.Null(await cache.GetAsync(_keyA)); Assert.Null(cache.Get(_keyB)); @@ -144,7 +143,7 @@ public async Task DeleteExpiredEntries() cache.Set(_keyB, _responseB); Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); - now = DateTime.UtcNow + DiskBasedResponseCache.CacheOptions.Default.TimeToLiveForCacheEntries; + now = DateTime.UtcNow + Defaults.DefaultTimeToLiveForCacheEntries; await provider.DeleteExpiredCacheEntriesAsync(); From 91eabd67d86254f92e5ef0759b5a8c4801491266 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Thu, 8 May 2025 11:47:36 -0700 Subject: [PATCH 072/472] Add back net9.0 version of the aieval dotnet tool (#6396) In #6148, we disabled net9.0 TFM for the aieval tool to work around the race described in https://github.com/dotnet/sdk/issues/47696. The underlying issue was subsequently fixed in the SDK (via https://github.com/dotnet/sdk/pull/47788). However, this fix has not been backported to the dotnet 9 SDK yet. The SDK team is working on backporting the fix (see discussion in https://github.com/dotnet/sdk/pull/47788#issuecomment-2861167925). But in the meanwhile, we can add back the net9.0 TFM and continue to work around the race by disabling parallel build. This would help users of the aieval tool sidestep errors such as the ones described in #6388 when they dont have dotnet8 installed on the machine. We can remove this workaround, once the backported fix is available in the dotnet 9 SDK. Fixes #6388 --- ...icrosoft.Extensions.AI.Evaluation.Console.csproj | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj index 91b398ce281..a1d9252e8e0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj @@ -3,9 +3,7 @@ A command line dotnet tool for generating reports and managing evaluation data. Exe - - $(MinimumSupportedTfmForPackaging) + $(NetCoreTargetFrameworks) Microsoft.Extensions.AI.Evaluation.Console $(NoWarn);EA0000 @@ -22,6 +20,15 @@ 0 + + + false + + From 1007abd0d9a5a5171d3decbed18f1ab3ab11cb1b Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Thu, 8 May 2025 15:53:11 -0400 Subject: [PATCH 073/472] Add some additional documentation around usage of cache, and CSP properties on report (#6377) * Add documentation around proper usage of IDistributedCache * Add Content-Security-Policy to prevent page from calling into other sites. * Remove remark about IDistributedCache usage * Fix package-lock.json * Remove start tag. --- .../TypeScript/html-report/index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html index 8169711aca6..c34388e543c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html @@ -8,8 +8,7 @@ - + AI Evaluation Report From bcb90d06a5dc256aba9cabeacf9a04b6756ff2fe Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 8 May 2025 13:49:30 -0700 Subject: [PATCH 074/472] Branding updates for 9.6.0 (#6399) * Branding updates for 9.6.0 * Fixing template tests branding. --- eng/Versions.props | 2 +- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb/aichatweb.csproj | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 96ec3bac1db..c10be974073 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ 9 - 5 + 6 0 preview 1 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 2d9ade69626..a4aad331c0d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,9 +8,9 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b7748e7c995..74656777aaf 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 85c0c9cae35..1a2374ce7fe 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 93bebb61f9f..3c4139801a2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,10 +8,10 @@ - + - + From e21126c7c33fc58b4375889c7040bc72498a5c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 8 May 2025 16:35:59 -0500 Subject: [PATCH 075/472] Add ChatOptions.RawRepresentationFactory (#6319) * Look for OpenAI.ChatCompletionOptions in top-level additional properties and stop looking for individually specific additional properties * Add RawRepresentation to ChatOptions and use it in OpenAI and AzureAIInference * Remove now unused locals * Add [JsonIgnore] and update roundtrip tests * Overwirte properties only if the underlying model don't specify it already * Clone RawRepresentation * Reflection workaround for ToolChoice not being cloned * Style changes * AI.Inference: Bring back propagation of additional properties * Don't use 0.1f, it doesn't roundtrip properly in .NET Framework * Add RawRepresentationFactory instead of object? property * Augment remarks to discourage returning shared instances * Documentation feedback * AI.Inference: keep passing TopK as AdditionalProperty if not already there --- .../ChatCompletion/ChatOptions.cs | 22 + .../AzureAIInferenceChatClient.cs | 95 +-- .../OpenAIChatClient.cs | 196 ++---- .../ChatCompletion/ChatOptionsTests.cs | 10 + .../AzureAIInferenceChatClientTests.cs | 640 ++++++++++++++++-- ...xtensions.AI.AzureAIInference.Tests.csproj | 1 + .../OpenAIChatClientTests.cs | 405 +++++++++-- 7 files changed, 1065 insertions(+), 304 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index ac5fba99ae5..f2eeffe9dbf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -1,6 +1,7 @@ // 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.Serialization; @@ -121,6 +122,26 @@ public string? ChatThreadId [JsonIgnore] public IList? Tools { get; set; } + /// + /// Gets or sets a callback responsible of creating the raw representation of the chat options from an underlying implementation. + /// + /// + /// The underlying implementation may have its own representation of options. + /// When or + /// is invoked with a , that implementation may convert the provided options into + /// its own representation in order to use it while performing the operation. For situations where a consumer knows + /// which concrete is being used and how it represents options, a new instance of that + /// implementation-specific options type may be returned by this callback, for the + /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options + /// instance further based on other settings supplied on this instance or from other inputs, + /// like the enumerable of s, therefore, its **strongly recommended** to not return shared instances + /// and instead make the callback return a new instance per each call. + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// properties on . + /// + [JsonIgnore] + public Func? RawRepresentationFactory { get; set; } + /// Gets or sets any additional properties associated with the options. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } @@ -147,6 +168,7 @@ public virtual ChatOptions Clone() ModelId = ModelId, AllowMultipleToolCalls = AllowMultipleToolCalls, ToolMode = ToolMode, + RawRepresentationFactory = RawRepresentationFactory, AdditionalProperties = AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 97d239aaefc..ff62845eb0e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -281,66 +281,74 @@ private static ChatRole ToChatRole(global::Azure.AI.Inference.ChatRole role) => finishReason == CompletionsFinishReason.ToolCalls ? ChatFinishReason.ToolCalls : new(s); + private ChatCompletionsOptions CreateAzureAIOptions(IEnumerable chatContents, ChatOptions? options) => + new(ToAzureAIInferenceChatMessages(chatContents)) + { + Model = options?.ModelId ?? _metadata.DefaultModelId ?? + throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.") + }; + /// Converts an extensions options instance to an AzureAI options instance. private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatContents, ChatOptions? options) { - ChatCompletionsOptions result = new(ToAzureAIInferenceChatMessages(chatContents)) + if (options is null) { - Model = options?.ModelId ?? _metadata.DefaultModelId ?? throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.") - }; + return CreateAzureAIOptions(chatContents, options); + } + + if (options.RawRepresentationFactory?.Invoke(this) is ChatCompletionsOptions result) + { + result.Messages = ToAzureAIInferenceChatMessages(chatContents).ToList(); + result.Model ??= options.ModelId ?? _metadata.DefaultModelId ?? + throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options."); + } + else + { + result = CreateAzureAIOptions(chatContents, options); + } - if (options is not null) + result.FrequencyPenalty ??= options.FrequencyPenalty; + result.MaxTokens ??= options.MaxOutputTokens; + result.NucleusSamplingFactor ??= options.TopP; + result.PresencePenalty ??= options.PresencePenalty; + result.Temperature ??= options.Temperature; + result.Seed ??= options.Seed; + + if (options.StopSequences is { Count: > 0 } stopSequences) { - result.FrequencyPenalty = options.FrequencyPenalty; - result.MaxTokens = options.MaxOutputTokens; - result.NucleusSamplingFactor = options.TopP; - result.PresencePenalty = options.PresencePenalty; - result.Temperature = options.Temperature; - result.Seed = options.Seed; - - if (options.StopSequences is { Count: > 0 } stopSequences) + foreach (string stopSequence in stopSequences) { - foreach (string stopSequence in stopSequences) - { - result.StopSequences.Add(stopSequence); - } + result.StopSequences.Add(stopSequence); } + } + + // This property is strongly typed on ChatOptions but not on ChatCompletionsOptions. + if (options.TopK is int topK && !result.AdditionalProperties.ContainsKey("top_k")) + { + result.AdditionalProperties["top_k"] = new BinaryData(JsonSerializer.SerializeToUtf8Bytes(topK, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(int)))); + } - // These properties are strongly typed on ChatOptions but not on ChatCompletionsOptions. - if (options.TopK is int topK) + if (options.AdditionalProperties is { } props) + { + foreach (var prop in props) { - result.AdditionalProperties["top_k"] = new BinaryData(JsonSerializer.SerializeToUtf8Bytes(topK, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(int)))); + byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); + result.AdditionalProperties[prop.Key] = new BinaryData(data); } + } - if (options.AdditionalProperties is { } props) + if (options.Tools is { Count: > 0 } tools) + { + foreach (AITool tool in tools) { - foreach (var prop in props) + if (tool is AIFunction af) { - switch (prop.Key) - { - // Propagate everything else to the ChatCompletionsOptions' AdditionalProperties. - default: - if (prop.Value is not null) - { - byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); - result.AdditionalProperties[prop.Key] = new BinaryData(data); - } - - break; - } + result.Tools.Add(ToAzureAIChatTool(af)); } } - if (options.Tools is { Count: > 0 } tools) + if (result.ToolChoice is null && result.Tools.Count > 0) { - foreach (AITool tool in tools) - { - if (tool is AIFunction af) - { - result.Tools.Add(ToAzureAIChatTool(af)); - } - } - switch (options.ToolMode) { case NoneChatToolMode: @@ -359,7 +367,10 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon break; } } + } + if (result.ResponseFormat is null) + { if (options.ResponseFormat is ChatResponseFormatText) { result.ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat(); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 93cd20a70eb..c644bd77f21 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -18,6 +18,7 @@ #pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; @@ -259,7 +260,6 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha string? responseId = null; DateTimeOffset? createdAt = null; string? modelId = null; - string? fingerprint = null; // Process each update as it arrives await foreach (StreamingChatCompletionUpdate update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) @@ -270,7 +270,6 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha responseId ??= update.CompletionId; createdAt ??= update.CreatedAt; modelId ??= update.Model; - fingerprint ??= update.SystemFingerprint; // Create the response content object. ChatResponseUpdate responseUpdate = new() @@ -284,22 +283,6 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha Role = streamedRole, }; - // Populate it with any additional metadata from the OpenAI object. - if (update.ContentTokenLogProbabilities is { Count: > 0 } contentTokenLogProbs) - { - (responseUpdate.AdditionalProperties ??= [])[nameof(update.ContentTokenLogProbabilities)] = contentTokenLogProbs; - } - - if (update.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs) - { - (responseUpdate.AdditionalProperties ??= [])[nameof(update.RefusalTokenLogProbabilities)] = refusalTokenLogProbs; - } - - if (fingerprint is not null) - { - (responseUpdate.AdditionalProperties ??= [])[nameof(update.SystemFingerprint)] = fingerprint; - } - // Transfer over content update items. if (update.ContentUpdate is { Count: > 0 }) { @@ -382,12 +365,6 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha responseUpdate.Contents.Add(new ErrorContent(refusal.ToString()) { ErrorCode = "Refusal" }); } - // Propagate additional relevant metadata. - if (fingerprint is not null) - { - (responseUpdate.AdditionalProperties ??= [])[nameof(ChatCompletion.SystemFingerprint)] = fingerprint; - } - yield return responseUpdate; } } @@ -426,20 +403,7 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple "mp3" or _ => "audio/mpeg", }; - var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType) - { - AdditionalProperties = new() { [nameof(audio.ExpiresAt)] = audio.ExpiresAt }, - }; - - if (audio.Id is string id) - { - dc.AdditionalProperties[nameof(audio.Id)] = id; - } - - if (audio.Transcript is string transcript) - { - dc.AdditionalProperties[nameof(audio.Transcript)] = transcript; - } + var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType); returnMessage.Contents.Add(dc); } @@ -480,140 +444,74 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple response.Usage = FromOpenAIUsage(tokenUsage); } - if (openAICompletion.ContentTokenLogProbabilities is { Count: > 0 } contentTokenLogProbs) - { - (response.AdditionalProperties ??= [])[nameof(openAICompletion.ContentTokenLogProbabilities)] = contentTokenLogProbs; - } - - if (openAICompletion.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs) - { - (response.AdditionalProperties ??= [])[nameof(openAICompletion.RefusalTokenLogProbabilities)] = refusalTokenLogProbs; - } - - if (openAICompletion.SystemFingerprint is string systemFingerprint) - { - (response.AdditionalProperties ??= [])[nameof(openAICompletion.SystemFingerprint)] = systemFingerprint; - } - return response; } /// Converts an extensions options instance to an OpenAI options instance. - private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) + private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) { - ChatCompletionOptions result = new(); + if (options is null) + { + return new ChatCompletionOptions(); + } - if (options is not null) + if (options.RawRepresentationFactory?.Invoke(this) is not ChatCompletionOptions result) { - result.FrequencyPenalty = options.FrequencyPenalty; - result.MaxOutputTokenCount = options.MaxOutputTokens; - result.TopP = options.TopP; - result.PresencePenalty = options.PresencePenalty; - result.Temperature = options.Temperature; - result.AllowParallelToolCalls = options.AllowMultipleToolCalls; + result = new ChatCompletionOptions(); + } + + result.FrequencyPenalty ??= options.FrequencyPenalty; + result.MaxOutputTokenCount ??= options.MaxOutputTokens; + result.TopP ??= options.TopP; + result.PresencePenalty ??= options.PresencePenalty; + result.Temperature ??= options.Temperature; + result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. - result.Seed = options.Seed; + result.Seed ??= options.Seed; #pragma warning restore OPENAI001 - if (options.StopSequences is { Count: > 0 } stopSequences) + if (options.StopSequences is { Count: > 0 } stopSequences) + { + foreach (string stopSequence in stopSequences) { - foreach (string stopSequence in stopSequences) - { - result.StopSequences.Add(stopSequence); - } + result.StopSequences.Add(stopSequence); } + } - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) + if (options.Tools is { Count: > 0 } tools) + { + foreach (AITool tool in tools) { - if (additionalProperties.TryGetValue(nameof(result.AudioOptions), out ChatAudioOptions? audioOptions)) + if (tool is AIFunction af) { - result.AudioOptions = audioOptions; - } - - if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId)) - { - result.EndUserId = endUserId; - } - - if (additionalProperties.TryGetValue(nameof(result.IncludeLogProbabilities), out bool includeLogProbabilities)) - { - result.IncludeLogProbabilities = includeLogProbabilities; - } - - if (additionalProperties.TryGetValue(nameof(result.LogitBiases), out IDictionary? logitBiases)) - { - foreach (KeyValuePair kvp in logitBiases!) - { - result.LogitBiases[kvp.Key] = kvp.Value; - } - } - - if (additionalProperties.TryGetValue(nameof(result.Metadata), out IDictionary? metadata)) - { - foreach (KeyValuePair kvp in metadata) - { - result.Metadata[kvp.Key] = kvp.Value; - } - } - - if (additionalProperties.TryGetValue(nameof(result.OutputPrediction), out ChatOutputPrediction? outputPrediction)) - { - result.OutputPrediction = outputPrediction; - } - - if (additionalProperties.TryGetValue(nameof(result.ReasoningEffortLevel), out ChatReasoningEffortLevel reasoningEffortLevel)) - { - result.ReasoningEffortLevel = reasoningEffortLevel; - } - - if (additionalProperties.TryGetValue(nameof(result.ResponseModalities), out ChatResponseModalities responseModalities)) - { - result.ResponseModalities = responseModalities; - } - - if (additionalProperties.TryGetValue(nameof(result.StoredOutputEnabled), out bool storeOutputEnabled)) - { - result.StoredOutputEnabled = storeOutputEnabled; - } - - if (additionalProperties.TryGetValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt)) - { - result.TopLogProbabilityCount = topLogProbabilityCountInt; + result.Tools.Add(ToOpenAIChatTool(af)); } } - if (options.Tools is { Count: > 0 } tools) + if (result.ToolChoice is null && result.Tools.Count > 0) { - foreach (AITool tool in tools) - { - if (tool is AIFunction af) - { - result.Tools.Add(ToOpenAIChatTool(af)); - } - } - - if (result.Tools.Count > 0) + switch (options.ToolMode) { - switch (options.ToolMode) - { - case NoneChatToolMode: - result.ToolChoice = ChatToolChoice.CreateNoneChoice(); - break; - - case AutoChatToolMode: - case null: - result.ToolChoice = ChatToolChoice.CreateAutoChoice(); - break; - - case RequiredChatToolMode required: - result.ToolChoice = required.RequiredFunctionName is null ? - ChatToolChoice.CreateRequiredChoice() : - ChatToolChoice.CreateFunctionChoice(required.RequiredFunctionName); - break; - } + case NoneChatToolMode: + result.ToolChoice = ChatToolChoice.CreateNoneChoice(); + break; + + case AutoChatToolMode: + case null: + result.ToolChoice = ChatToolChoice.CreateAutoChoice(); + break; + + case RequiredChatToolMode required: + result.ToolChoice = required.RequiredFunctionName is null ? + ChatToolChoice.CreateRequiredChoice() : + ChatToolChoice.CreateFunctionChoice(required.RequiredFunctionName); + break; } } + } + if (result.ResponseFormat is null) + { if (options.ResponseFormat is ChatResponseFormatText) { result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs index 67bbfb6d3db..cdf1aab09c9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs @@ -1,6 +1,7 @@ // 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; @@ -28,6 +29,7 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(options.ToolMode); Assert.Null(options.Tools); Assert.Null(options.AdditionalProperties); + Assert.Null(options.RawRepresentationFactory); ChatOptions clone = options.Clone(); Assert.Null(clone.ConversationId); @@ -45,6 +47,7 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(clone.ToolMode); Assert.Null(clone.Tools); Assert.Null(clone.AdditionalProperties); + Assert.Null(clone.RawRepresentationFactory); } [Fact] @@ -69,6 +72,8 @@ public void Properties_Roundtrip() ["key"] = "value", }; + Func rawRepresentationFactory = (c) => null; + options.ConversationId = "12345"; options.Temperature = 0.1f; options.MaxOutputTokens = 2; @@ -83,6 +88,7 @@ public void Properties_Roundtrip() options.AllowMultipleToolCalls = true; options.ToolMode = ChatToolMode.RequireAny; options.Tools = tools; + options.RawRepresentationFactory = rawRepresentationFactory; options.AdditionalProperties = additionalProps; Assert.Equal("12345", options.ConversationId); @@ -99,6 +105,7 @@ public void Properties_Roundtrip() Assert.True(options.AllowMultipleToolCalls); Assert.Same(ChatToolMode.RequireAny, options.ToolMode); Assert.Same(tools, options.Tools); + Assert.Same(rawRepresentationFactory, options.RawRepresentationFactory); Assert.Same(additionalProps, options.AdditionalProperties); ChatOptions clone = options.Clone(); @@ -116,6 +123,7 @@ public void Properties_Roundtrip() Assert.True(clone.AllowMultipleToolCalls); Assert.Same(ChatToolMode.RequireAny, clone.ToolMode); Assert.Equal(tools, clone.Tools); + Assert.Same(rawRepresentationFactory, clone.RawRepresentationFactory); Assert.Equal(additionalProps, clone.AdditionalProperties); } @@ -153,6 +161,7 @@ public void JsonSerialization_Roundtrips() AIFunctionFactory.Create(() => 42), AIFunctionFactory.Create(() => 43), ]; + options.RawRepresentationFactory = (c) => null; options.AdditionalProperties = additionalProps; string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.ChatOptions); @@ -175,6 +184,7 @@ public void JsonSerialization_Roundtrips() Assert.False(deserialized.AllowMultipleToolCalls); Assert.Equal(ChatToolMode.RequireAny, deserialized.ToolMode); Assert.Null(deserialized.Tools); + Assert.Null(deserialized.RawRepresentationFactory); Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 788b8568607..26cd380ec83 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Azure; using Azure.AI.Inference; @@ -32,6 +33,19 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("defaultModelId", () => client.AsIChatClient(" ")); } + [Fact] + public async Task NullModel_Throws() + { + ChatCompletionsClient client = new(new("http://localhost/some/endpoint"), new AzureKeyCredential("key")); + IChatClient chatClient = client.AsIChatClient(modelId: null); + + await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("hello")); + await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("hello").GetAsyncEnumerator().MoveNextAsync().AsTask()); + + await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("hello", new ChatOptions { ModelId = null })); + await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("hello", new ChatOptions { ModelId = null }).GetAsyncEnumerator().MoveNextAsync().AsTask()); + } + [Fact] public void AsIChatClient_ProducesExpectedMetadata() { @@ -76,54 +90,54 @@ public void GetService_SuccessfullyReturnsUnderlyingClient() Assert.Null(pipeline.GetService("key")); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BasicRequestResponse_NonStreaming(bool multiContent) - { - const string Input = """ - { - "messages": [{"role":"user", "content":"hello"}], - "max_tokens":10, - "temperature":0.5, - "model":"gpt-4o-mini" - } - """; + private const string BasicInputNonStreaming = """ + { + "messages": [{"role":"user", "content":"hello"}], + "max_tokens":10, + "temperature":0.5, + "model":"gpt-4o-mini" + } + """; - const string Output = """ + private const string BasicOutputNonStreaming = """ + { + "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", + "object": "chat.completion", + "created": 1727888631, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "created": 1727888631, - "model": "gpt-4o-mini-2024-07-18", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?", - "refusal": null - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 8, - "completion_tokens": 9, - "total_tokens": 17, - "prompt_tokens_details": { - "cached_tokens": 0 + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "refusal": null }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "system_fingerprint": "fp_f85bea6784" + "logprobs": null, + "finish_reason": "stop" } - """; + ], + "usage": { + "prompt_tokens": 8, + "completion_tokens": 9, + "total_tokens": 17, + "prompt_tokens_details": { + "cached_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0 + } + }, + "system_fingerprint": "fp_f85bea6784" + } + """; - using VerbatimHttpHandler handler = new(Input, Output); + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicRequestResponse_NonStreaming(bool multiContent) + { + using VerbatimHttpHandler handler = new(BasicInputNonStreaming, BasicOutputNonStreaming); using HttpClient httpClient = new(handler); using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); @@ -153,50 +167,50 @@ [new ChatMessage(ChatRole.User, "hello".Select(c => (AIContent)new TextContent(c Assert.Equal(17, response.Usage.TotalTokenCount); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BasicRequestResponse_Streaming(bool multiContent) - { - const string Input = """ - { - "messages": [{"role":"user", "content":"hello"}], - "max_tokens":20, - "temperature":0.5, - "stream":true, - "model":"gpt-4o-mini"} - """; + private const string BasicInputStreaming = """ + { + "messages": [{"role":"user", "content":"hello"}], + "max_tokens":20, + "temperature":0.5, + "stream":true, + "model":"gpt-4o-mini"} + """; - const string Output = """ - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + private const string BasicOutputStreaming = """ + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[],"usage":{"prompt_tokens":8,"completion_tokens":9,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}} + data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[],"usage":{"prompt_tokens":8,"completion_tokens":9,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}} - data: [DONE] + data: [DONE] - """; + """; - using VerbatimHttpHandler handler = new(Input, Output); + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BasicRequestResponse_Streaming(bool multiContent) + { + using VerbatimHttpHandler handler = new(BasicInputStreaming, BasicOutputStreaming); using HttpClient httpClient = new(handler); using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); @@ -230,6 +244,420 @@ [new ChatMessage(ChatRole.User, "hello".Select(c => (AIContent)new TextContent(c } } + [Fact] + public async Task IChatClient_WithNullModel_ChatOptions_WithNotNullModel_NonStreaming() + { + using VerbatimHttpHandler handler = new(BasicInputNonStreaming, BasicOutputNonStreaming); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + + var response = await client.GetResponseAsync("hello", new ChatOptions + { + ModelId = "gpt-4o-mini", + MaxOutputTokens = 10, + Temperature = 0.5f, + }); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + [Fact] + public async Task IChatClient_WithNullModel_ChatOptions_WithNotNullModel_Streaming() + { + using VerbatimHttpHandler handler = new(BasicInputStreaming, BasicOutputStreaming); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + + string responseText = string.Empty; + await foreach (var update in client.GetStreamingResponseAsync("hello", new ChatOptions + { + ModelId = "gpt-4o-mini", + MaxOutputTokens = 20, + Temperature = 0.5f, + })) + { + responseText += update.Text; + } + + Assert.Equal("Hello! How can I assist you today?", responseText); + } + + [Fact] + public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.75, + "max_tokens":10, + "top_p":0.5, + "presence_penalty":0.5, + "temperature":0.5, + "seed":42, + "stop":["hello","world"], + "response_format":{"type":"text"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}}, + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type": "object","required": ["personName"],"properties": {"personName": {"description": "The person whose age is being requested","type": "string"}}}}} + ], + "tool_choice":"auto", + "additional_property_from_raw_representation":42, + "additional_property_from_MEAI_options":42 + } + """; + + const string Output = """ + { + "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatCompletionsOptions azureAIOptions = new() + { + Model = "gpt-4o-mini", + FrequencyPenalty = 0.75f, + MaxTokens = 10, + NucleusSamplingFactor = 0.5f, + PresencePenalty = 0.5f, + Temperature = 0.5f, + Seed = 42, + ToolChoice = ChatCompletionsToolChoice.Auto, + ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() + }; + azureAIOptions.StopSequences.Add("hello"); + azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); + azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); + + ChatOptions chatOptions = new ChatOptions + { + RawRepresentationFactory = (c) => + { + ChatCompletionsOptions azureAIOptions = new() + { + Model = "gpt-4o-mini", + FrequencyPenalty = 0.75f, + MaxTokens = 10, + NucleusSamplingFactor = 0.5f, + PresencePenalty = 0.5f, + Temperature = 0.5f, + Seed = 42, + ToolChoice = ChatCompletionsToolChoice.Auto, + ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() + }; + azureAIOptions.StopSequences.Add("hello"); + azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); + azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); + return azureAIOptions; + }, + ModelId = null, + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json, + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["additional_property_from_MEAI_options"] = 42 + } + }; + + var response = await client.GetResponseAsync("hello", chatOptions); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + [Fact] + public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_Streaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.75, + "max_tokens":10, + "top_p":0.5, + "presence_penalty":0.5, + "temperature":0.5, + "seed":42, + "stop":["hello","world"], + "response_format":{"type":"text"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}}, + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type": "object","required": ["personName"],"properties": {"personName": {"description": "The person whose age is being requested","type": "string"}}}}} + ], + "tool_choice":"auto", + "additional_property_from_raw_representation":42, + "additional_property_from_MEAI_options":42, + "stream":true + } + """; + + const string Output = """ + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} + + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} + + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} + + data: [DONE] + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new ChatOptions + { + RawRepresentationFactory = (c) => + { + ChatCompletionsOptions azureAIOptions = new() + { + Model = "gpt-4o-mini", + FrequencyPenalty = 0.75f, + MaxTokens = 10, + NucleusSamplingFactor = 0.5f, + PresencePenalty = 0.5f, + Temperature = 0.5f, + Seed = 42, + ToolChoice = ChatCompletionsToolChoice.Auto, + ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() + }; + azureAIOptions.StopSequences.Add("hello"); + azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); + azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); + return azureAIOptions; + }, + ModelId = null, + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json, + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["additional_property_from_MEAI_options"] = 42 + } + }; + + string responseText = string.Empty; + await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) + { + responseText += update.Text; + } + + Assert.Equal("Hello! How can I assist you today?", responseText); + } + + [Fact] + public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStreaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.125, + "max_tokens":1, + "top_p":0.125, + "presence_penalty":0.125, + "temperature":0.125, + "seed":1, + "stop":["world"], + "response_format":{"type":"json_object"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} + ], + "tool_choice":"none" + } + """; + + const string Output = """ + { + "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new ChatOptions + { + RawRepresentationFactory = (c) => + { + ChatCompletionsOptions azureAIOptions = new(); + Assert.Empty(azureAIOptions.Messages); + Assert.Null(azureAIOptions.Model); + Assert.Null(azureAIOptions.FrequencyPenalty); + Assert.Null(azureAIOptions.MaxTokens); + Assert.Null(azureAIOptions.NucleusSamplingFactor); + Assert.Null(azureAIOptions.PresencePenalty); + Assert.Null(azureAIOptions.Temperature); + Assert.Null(azureAIOptions.Seed); + Assert.Empty(azureAIOptions.StopSequences); + Assert.Empty(azureAIOptions.Tools); + Assert.Null(azureAIOptions.ToolChoice); + Assert.Null(azureAIOptions.ResponseFormat); + return azureAIOptions; + }, + ModelId = "gpt-4o-mini", + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await client.GetResponseAsync("hello", chatOptions); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + [Fact] + public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Streaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.125, + "max_tokens":1, + "top_p":0.125, + "presence_penalty":0.125, + "temperature":0.125, + "seed":1, + "stop":["world"], + "response_format":{"type":"json_object"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} + ], + "tool_choice":"none", + "stream":true + } + """; + + const string Output = """ + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} + + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} + + data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} + + data: [DONE] + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: null!); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new ChatOptions + { + RawRepresentationFactory = (c) => + { + ChatCompletionsOptions azureAIOptions = new(); + Assert.Empty(azureAIOptions.Messages); + Assert.Null(azureAIOptions.Model); + Assert.Null(azureAIOptions.FrequencyPenalty); + Assert.Null(azureAIOptions.MaxTokens); + Assert.Null(azureAIOptions.NucleusSamplingFactor); + Assert.Null(azureAIOptions.PresencePenalty); + Assert.Null(azureAIOptions.Temperature); + Assert.Null(azureAIOptions.Seed); + Assert.Empty(azureAIOptions.StopSequences); + Assert.Empty(azureAIOptions.Tools); + Assert.Null(azureAIOptions.ToolChoice); + Assert.Null(azureAIOptions.ResponseFormat); + return azureAIOptions; + }, + ModelId = "gpt-4o-mini", + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + string responseText = string.Empty; + await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) + { + responseText += update.Text; + } + + Assert.Equal("Hello! How can I assist you today?", responseText); + } + + /// Converts an Extensions function to an AzureAI chat tool. + private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) + { + // Map to an intermediate model so that redundant properties are skipped. + var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema)!; + var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); + return new(new FunctionDefinition(aiFunction.Name) + { + Description = aiFunction.Description, + Parameters = functionParameters, + }); + } + + /// Used to create the JSON payload for an AzureAI chat tool description. + private sealed class AzureAIChatToolJson + { + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("required")] + public List Required { get; set; } = []; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + } + [Fact] public async Task AdditionalOptions_NonStreaming() { @@ -279,10 +707,72 @@ public async Task AdditionalOptions_NonStreaming() PresencePenalty = 0.5f, Seed = 42, StopSequences = ["yes", "no"], - AdditionalProperties = new() + RawRepresentationFactory = (c) => + { + ChatCompletionsOptions azureAIOptions = new(); + azureAIOptions.AdditionalProperties.Add("something_else", new BinaryData(JsonSerializer.SerializeToUtf8Bytes("value1", typeof(object)))); + azureAIOptions.AdditionalProperties.Add("and_something_further", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(123, typeof(object)))); + return azureAIOptions; + }, + })); + } + + [Fact] + public async Task TopK_DoNotOverwrite_NonStreaming() + { + const string Input = """ + { + "messages":[{"role":"user", "content":"hello"}], + "max_tokens":10, + "temperature":0.5, + "top_p":0.5, + "stop":["yes","no"], + "presence_penalty":0.5, + "frequency_penalty":0.75, + "seed":42, + "model":"gpt-4o-mini", + "top_k":40, + "something_else":"value1", + "and_something_further":123 + } + """; + + const string Output = """ + { + "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); + + Assert.NotNull(await client.GetResponseAsync("hello", new() + { + MaxOutputTokens = 10, + Temperature = 0.5f, + TopP = 0.5f, + TopK = 20, // will be ignored because the raw representation already specifies it. + FrequencyPenalty = 0.75f, + PresencePenalty = 0.5f, + Seed = 42, + StopSequences = ["yes", "no"], + RawRepresentationFactory = (c) => { - ["something_else"] = "value1", - ["and_something_further"] = 123, + ChatCompletionsOptions azureAIOptions = new(); + azureAIOptions.AdditionalProperties.Add("top_k", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(40, typeof(object)))); + azureAIOptions.AdditionalProperties.Add("something_else", new BinaryData(JsonSerializer.SerializeToUtf8Bytes("value1", typeof(object)))); + azureAIOptions.AdditionalProperties.Add("and_something_further", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(123, typeof(object)))); + return azureAIOptions; }, })); } diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj index e129c6cf26f..a0f9abaf589 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj @@ -6,6 +6,7 @@ true + $(NoWarn);S104 diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 4fdc36b5280..9ba9c743166 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -8,6 +8,8 @@ using System.ComponentModel; using System.Linq; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; @@ -184,9 +186,6 @@ public async Task BasicRequestResponse_NonStreaming() { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -257,8 +256,6 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal(createdAt, updates[i].CreatedAt); Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); Assert.Equal(ChatRole.Assistant, updates[i].Role); - Assert.NotNull(updates[i].AdditionalProperties); - Assert.Equal("fp_f85bea6784", updates[i].AdditionalProperties![nameof(ChatCompletion.SystemFingerprint)]); Assert.Equal(i == 10 ? 0 : 1, updates[i].Contents.Count); Assert.Equal(i < 10 ? null : ChatFinishReason.Stop, updates[i].FinishReason); } @@ -280,7 +277,361 @@ public async Task BasicRequestResponse_Streaming() } [Fact] - public async Task NonStronglyTypedOptions_AllSent() + public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.75, + "max_completion_tokens":10, + "top_p":0.5, + "presence_penalty":0.5, + "temperature":0.5, + "seed":42, + "stop":["hello","world"], + "response_format":{"type":"text"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"additionalProperties":false,"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}}, + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"additionalProperties":false,"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} + ], + "tool_choice":"auto" + } + """; + + const string Output = """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: "gpt-4o-mini"); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new() + { + RawRepresentationFactory = (c) => + { + ChatCompletionOptions openAIOptions = new() + { + FrequencyPenalty = 0.75f, + MaxOutputTokenCount = 10, + TopP = 0.5f, + PresencePenalty = 0.5f, + Temperature = 0.5f, +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + Seed = 42, +#pragma warning restore OPENAI001 + ToolChoice = ChatToolChoice.CreateAutoChoice(), + ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() + }; + openAIOptions.StopSequences.Add("hello"); + openAIOptions.Tools.Add(ToOpenAIChatTool(tool)); + return openAIOptions; + }, + ModelId = null, + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await client.GetResponseAsync("hello", chatOptions); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + [Fact] + public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_Streaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.75, + "max_completion_tokens":10, + "top_p":0.5, + "presence_penalty":0.5, + "temperature":0.5, + "seed":42, + "stop":["hello","world"], + "response_format":{"type":"text"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"additionalProperties":false}}}, + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"additionalProperties":false}}} + ], + "tool_choice":"auto", + "stream":true, + "stream_options":{"include_usage":true} + } + """; + + const string Output = """ + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} + + data: [DONE] + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: "gpt-4o-mini"); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new() + { + RawRepresentationFactory = (c) => + { + ChatCompletionOptions openAIOptions = new() + { + FrequencyPenalty = 0.75f, + MaxOutputTokenCount = 10, + TopP = 0.5f, + PresencePenalty = 0.5f, + Temperature = 0.5f, +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + Seed = 42, +#pragma warning restore OPENAI001 + ToolChoice = ChatToolChoice.CreateAutoChoice(), + ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() + }; + openAIOptions.StopSequences.Add("hello"); + openAIOptions.Tools.Add(ToOpenAIChatTool(tool)); + return openAIOptions; + }, + ModelId = null, // has no effect, you cannot change the model of an OpenAI's ChatClient. + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + string responseText = string.Empty; + await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) + { + responseText += update.Text; + } + + Assert.Equal("Hello! How can I assist you today?", responseText); + } + + [Fact] + public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStreaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.125, + "max_completion_tokens":1, + "top_p":0.125, + "presence_penalty":0.125, + "temperature":0.125, + "seed":1, + "stop":["world"], + "response_format":{"type":"json_object"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"additionalProperties":false,"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} + ], + "tool_choice":"none" + } + """; + + const string Output = """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + } + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: "gpt-4o-mini"); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new() + { + RawRepresentationFactory = (c) => + { + ChatCompletionOptions openAIOptions = new(); + Assert.Null(openAIOptions.FrequencyPenalty); + Assert.Null(openAIOptions.MaxOutputTokenCount); + Assert.Null(openAIOptions.TopP); + Assert.Null(openAIOptions.PresencePenalty); + Assert.Null(openAIOptions.Temperature); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + Assert.Null(openAIOptions.Seed); +#pragma warning restore OPENAI001 + Assert.Empty(openAIOptions.StopSequences); + Assert.Empty(openAIOptions.Tools); + Assert.Null(openAIOptions.ToolChoice); + Assert.Null(openAIOptions.ResponseFormat); + return openAIOptions; + }, + ModelId = null, // has no effect, you cannot change the model of an OpenAI's ChatClient. + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await client.GetResponseAsync("hello", chatOptions); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + [Fact] + public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Streaming() + { + const string Input = """ + { + "messages":[{"role":"user","content":"hello"}], + "model":"gpt-4o-mini", + "frequency_penalty":0.125, + "max_completion_tokens":1, + "top_p":0.125, + "presence_penalty":0.125, + "temperature":0.125, + "seed":1, + "stop":["world"], + "response_format":{"type":"json_object"}, + "tools":[ + {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"additionalProperties":false,"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} + ], + "tool_choice":"none", + "stream":true, + "stream_options":{"include_usage":true} + } + """; + + const string Output = """ + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} + + data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} + + data: [DONE] + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, modelId: "gpt-4o-mini"); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new() + { + RawRepresentationFactory = (c) => + { + ChatCompletionOptions openAIOptions = new(); + Assert.Null(openAIOptions.FrequencyPenalty); + Assert.Null(openAIOptions.MaxOutputTokenCount); + Assert.Null(openAIOptions.TopP); + Assert.Null(openAIOptions.PresencePenalty); + Assert.Null(openAIOptions.Temperature); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + Assert.Null(openAIOptions.Seed); +#pragma warning restore OPENAI001 + Assert.Empty(openAIOptions.StopSequences); + Assert.Empty(openAIOptions.Tools); + Assert.Null(openAIOptions.ToolChoice); + Assert.Null(openAIOptions.ResponseFormat); + return openAIOptions; + }, + ModelId = null, + FrequencyPenalty = 0.125f, + MaxOutputTokens = 1, + TopP = 0.125f, + PresencePenalty = 0.125f, + Temperature = 0.125f, + Seed = 1, + StopSequences = ["world"], + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + string responseText = string.Empty; + await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) + { + responseText += update.Text; + } + + Assert.Equal("Hello! How can I assist you today?", responseText); + } + + /// Converts an Extensions function to an OpenAI chat tool. + private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) + { + bool? strict = + aiFunction.AdditionalProperties.TryGetValue("strictJsonSchema", 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.JsonSchema)!; + var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); + return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict); + } + + /// Used to create the JSON payload for an OpenAI chat tool description. + private sealed class ChatToolJson + { + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("required")] + public HashSet Required { get; set; } = []; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + + [JsonPropertyName("additionalProperties")] + public bool AdditionalProperties { get; set; } + } + + [Fact] + public async Task StronglyTypedOptions_AllSent() { const string Input = """ { @@ -320,17 +671,18 @@ public async Task NonStronglyTypedOptions_AllSent() Assert.NotNull(await client.GetResponseAsync("hello", new() { AllowMultipleToolCalls = false, - AdditionalProperties = new() + RawRepresentationFactory = (c) => { - ["StoredOutputEnabled"] = true, - ["Metadata"] = new Dictionary + var openAIOptions = new ChatCompletionOptions { - ["something"] = "else", - }, - ["LogitBiases"] = new Dictionary { { 12, 34 } }, - ["IncludeLogProbabilities"] = true, - ["TopLogProbabilityCount"] = 42, - ["EndUserId"] = "12345", + StoredOutputEnabled = true, + IncludeLogProbabilities = true, + TopLogProbabilityCount = 42, + EndUserId = "12345", + }; + openAIOptions.Metadata.Add("something", "else"); + openAIOptions.LogitBiases.Add(12, 34); + return openAIOptions; }, })); } @@ -446,9 +798,6 @@ public async Task MultipleMessages_NonStreaming() { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -546,9 +895,6 @@ public async Task MultiPartSystemMessage_NonStreaming() { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -647,9 +993,6 @@ public async Task EmptyAssistantMessage_NonStreaming() { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -767,9 +1110,6 @@ public async Task FunctionCallContent_NonStreaming() FunctionCallContent fcc = Assert.IsType(response.Messages.Single().Contents[0]); Assert.Equal("GetPersonAge", fcc.Name); AssertExtensions.EqualFunctionCallParameters(new Dictionary { ["personName"] = "Alice" }, fcc.Arguments); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -852,9 +1192,6 @@ public async Task UnavailableBuiltInFunctionCall_NonStreaming() Assert.Single(response.Messages.Single().Contents); TextContent fcc = Assert.IsType(response.Messages.Single().Contents[0]); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -946,8 +1283,6 @@ public async Task FunctionCallContent_Streaming() Assert.Equal(createdAt, updates[i].CreatedAt); Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); Assert.Equal(ChatRole.Assistant, updates[i].Role); - Assert.NotNull(updates[i].AdditionalProperties); - Assert.Equal("fp_f85bea6784", updates[i].AdditionalProperties![nameof(ChatCompletion.SystemFingerprint)]); Assert.Equal(i < 7 ? null : ChatFinishReason.ToolCalls, updates[i].FinishReason); } @@ -1111,9 +1446,6 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } [Fact] @@ -1229,9 +1561,6 @@ private static async Task DataContentMessage_Image_AdditionalPropertyDetail_NonS { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, }, response.Usage.AdditionalCounts); - - Assert.NotNull(response.AdditionalProperties); - Assert.Equal("fp_b705f0c291", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } private static IChatClient CreateChatClient(HttpClient httpClient, string modelId) => From cb628b8735a07aa3226c35879c74537690278572 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 19:02:39 -0700 Subject: [PATCH 076/472] Bump vite from 6.2.6 to 6.3.4 in /src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript (#6354) * Bump vite Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.4. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.3.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shyam Namboodiripad --- .../TypeScript/package-lock.json | 87 +++++++++++++++++-- .../TypeScript/package.json | 2 +- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package-lock.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package-lock.json index 3e77d76226f..6e76e88aa03 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package-lock.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package-lock.json @@ -33,7 +33,7 @@ "tfx-cli": "^0.21.0", "typescript": "^5.5.3", "typescript-eslint": "^8.27.0", - "vite": "^6.2.6", + "vite": "^6.3.4", "vite-plugin-singlefile": "^2.0.2" } }, @@ -8274,6 +8274,51 @@ "node": "*" } }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinytim": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/tinytim/-/tinytim-0.1.1.tgz", @@ -8764,14 +8809,18 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -8850,6 +8899,34 @@ "vite": "^5.4.11 || ^6.0.0" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/walkdir": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package.json index c2505b5d87a..0e32f4ae6f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/package.json @@ -34,7 +34,7 @@ "tfx-cli": "^0.21.0", "typescript": "^5.5.3", "typescript-eslint": "^8.27.0", - "vite": "^6.2.6", + "vite": "^6.3.4", "vite-plugin-singlefile": "^2.0.2" } } From 3b9599fec81d56798ec76d4f2cbc8ad5583c9627 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 8 May 2025 23:11:11 -0400 Subject: [PATCH 077/472] Avoid caching in CachingChatClient when ConversationId is set (#6400) --- .../ChatCompletion/CachingChatClient.cs | 32 +++++++++++++++-- .../DistributedCachingChatClientTest.cs | 36 +++++++++++-------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs index 5aa70e4b262..211fc39ec85 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs @@ -9,6 +9,7 @@ using Microsoft.Shared.Diagnostics; #pragma warning disable S127 // "for" loop stop conditions should be invariant +#pragma warning disable SA1202 // Elements should be ordered by access namespace Microsoft.Extensions.AI; @@ -45,11 +46,19 @@ protected CachingChatClient(IChatClient innerClient) public bool CoalesceStreamingUpdates { get; set; } = true; /// - public override async Task GetResponseAsync( + public override Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); + return UseCaching(options) ? + GetCachedResponseAsync(messages, options, cancellationToken) : + base.GetResponseAsync(messages, options, cancellationToken); + } + + private async Task GetCachedResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { // We're only storing the final result, not the in-flight task, so that we can avoid caching failures // or having problems when one of the callers cancels but others don't. This has the drawback that // concurrent callers might trigger duplicate requests, but that's acceptable. @@ -65,11 +74,19 @@ public override async Task GetResponseAsync( } /// - public override async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public override IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); + return UseCaching(options) ? + GetCachedStreamingResponseAsync(messages, options, cancellationToken) : + base.GetStreamingResponseAsync(messages, options, cancellationToken); + } + + private async IAsyncEnumerable GetCachedStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { if (CoalesceStreamingUpdates) { // When coalescing updates, we cache non-streaming results coalesced from streaming ones. That means @@ -178,4 +195,13 @@ public override async IAsyncEnumerable GetStreamingResponseA /// is . /// is . protected abstract Task WriteCacheStreamingAsync(string key, IReadOnlyList value, CancellationToken cancellationToken); + + /// Determine whether to use caching with the request. + private static bool UseCaching(ChatOptions? options) + { + // We want to skip caching if options.ConversationId is set. If it's set, that implies there's + // some state that will impact the response and that's not represented in the messages. Since + // that state could change even with the same ID, we have to assume caching isn't valid. + return options?.ConversationId is null; + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs index 374e617adba..4f2427d133c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs @@ -32,10 +32,13 @@ public void Ctor_ExpectedDefaults() Assert.True(cachingClient.CoalesceStreamingUpdates); } - [Fact] - public async Task CachesSuccessResultsAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CachesSuccessResultsAsync(bool conversationIdSet) { // Arrange + ChatOptions options = new() { ConversationId = conversationIdSet ? "123" : null }; // Verify that all the expected properties will round-trip through the cache, // even if this involves serialization @@ -82,20 +85,20 @@ public async Task CachesSuccessResultsAsync() }; // Make the initial request and do a quick sanity check - var result1 = await outer.GetResponseAsync("some input"); + var result1 = await outer.GetResponseAsync("some input", options); Assert.Same(expectedResponse, result1); Assert.Equal(1, innerCallCount); // Act - var result2 = await outer.GetResponseAsync("some input"); + var result2 = await outer.GetResponseAsync("some input", options); // Assert - Assert.Equal(1, innerCallCount); + Assert.Equal(conversationIdSet ? 2 : 1, innerCallCount); AssertResponsesEqual(expectedResponse, result2); // Act/Assert 2: Cache misses do not return cached results - await outer.GetResponseAsync("some modified input"); - Assert.Equal(2, innerCallCount); + await outer.GetResponseAsync("some modified input", options); + Assert.Equal(conversationIdSet ? 3 : 2, innerCallCount); } [Fact] @@ -207,10 +210,13 @@ public async Task DoesNotCacheCanceledResultsAsync() Assert.Equal("A good result", result2.Text); } - [Fact] - public async Task StreamingCachesSuccessResultsAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StreamingCachesSuccessResultsAsync(bool conversationIdSet) { // Arrange + ChatOptions options = new() { ConversationId = conversationIdSet ? "123" : null }; // Verify that all the expected properties will round-trip through the cache, // even if this involves serialization @@ -255,20 +261,20 @@ public async Task StreamingCachesSuccessResultsAsync() }; // Make the initial request and do a quick sanity check - var result1 = outer.GetStreamingResponseAsync("some input"); + var result1 = outer.GetStreamingResponseAsync("some input", options); await AssertResponsesEqualAsync(actualUpdate, result1); Assert.Equal(1, innerCallCount); // Act - var result2 = outer.GetStreamingResponseAsync("some input"); + var result2 = outer.GetStreamingResponseAsync("some input", options); // Assert - Assert.Equal(1, innerCallCount); - await AssertResponsesEqualAsync(expectedCachedResponse, result2); + Assert.Equal(conversationIdSet ? 2 : 1, innerCallCount); + await AssertResponsesEqualAsync(conversationIdSet ? actualUpdate : expectedCachedResponse, result2); // Act/Assert 2: Cache misses do not return cached results - await ToListAsync(outer.GetStreamingResponseAsync("some modified input")); - Assert.Equal(2, innerCallCount); + await ToListAsync(outer.GetStreamingResponseAsync("some modified input", options)); + Assert.Equal(conversationIdSet ? 3 : 2, innerCallCount); } [Theory] From d67431af8b067beef3e252f279c2b0cac625b27f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 8 May 2025 23:13:09 -0400 Subject: [PATCH 078/472] Add comment LoggingChatClient et al trace-level logging (#6391) Also fixed the name of the LoggingSpeechToTextClientBuilderExtensions type to conform to patterns used elsewhere in the library. --- .../ChatCompletion/LoggingChatClient.cs | 8 ++++++++ .../LoggingChatClientBuilderExtensions.cs | 8 ++++++++ .../Embeddings/LoggingEmbeddingGenerator.cs | 8 ++++++++ .../LoggingEmbeddingGeneratorBuilderExtensions.cs | 8 ++++++++ .../SpeechToText/LoggingSpeechToTextClient.cs | 8 ++++++++ ...s => LoggingSpeechToTextClientBuilderExtensions.cs} | 10 +++++++++- 6 files changed, 49 insertions(+), 1 deletion(-) rename src/Libraries/Microsoft.Extensions.AI/SpeechToText/{SpeechToTextClientBuilderExtensions.cs => LoggingSpeechToTextClientBuilderExtensions.cs} (79%) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs index b5f43f5385b..3937d5db59b 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs @@ -13,10 +13,18 @@ namespace Microsoft.Extensions.AI; /// A delegating chat client that logs chat operations to an . +/// /// /// The provided implementation of is thread-safe for concurrent use so long as the /// employed is also thread-safe for concurrent use. /// +/// +/// When the employed enables , the contents of +/// chat messages and options are logged. These messages and options may contain sensitive application data. +/// is disabled by default and should never be enabled in a production environment. +/// Messages and options are not logged at other logging levels. +/// +/// public partial class LoggingChatClient : DelegatingChatClient { /// An instance used for all logging. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClientBuilderExtensions.cs index d34716ed886..e2759b6b0a6 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClientBuilderExtensions.cs @@ -21,6 +21,14 @@ public static class LoggingChatClientBuilderExtensions /// An optional callback that can be used to configure the instance. /// The . /// is . + /// + /// + /// When the employed enables , the contents of + /// chat messages and options are logged. These messages and options may contain sensitive application data. + /// is disabled by default and should never be enabled in a production environment. + /// Messages and options are not logged at other logging levels. + /// + /// public static ChatClientBuilder UseLogging( this ChatClientBuilder builder, ILoggerFactory? loggerFactory = null, diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGenerator.cs index 924ee362633..97e5beb2c42 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGenerator.cs @@ -14,10 +14,18 @@ namespace Microsoft.Extensions.AI; /// A delegating embedding generator that logs embedding generation operations to an . /// Specifies the type of the input passed to the generator. /// Specifies the type of the embedding instance produced by the generator. +/// /// /// The provided implementation of is thread-safe for concurrent use /// so long as the employed is also thread-safe for concurrent use. /// +/// +/// When the employed enables , the contents of +/// values and options are logged. These values and options may contain sensitive application data. +/// is disabled by default and should never be enabled in a production environment. +/// Messages and options are not logged at other logging levels. +/// +/// public partial class LoggingEmbeddingGenerator : DelegatingEmbeddingGenerator where TEmbedding : Embedding { diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGeneratorBuilderExtensions.cs index eb472fb1e0e..a7afbdeed85 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGeneratorBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/LoggingEmbeddingGeneratorBuilderExtensions.cs @@ -23,6 +23,14 @@ public static class LoggingEmbeddingGeneratorBuilderExtensions /// An optional callback that can be used to configure the instance. /// The . /// is . + /// + /// + /// When the employed enables , the contents of + /// values and options are logged. These values and options may contain sensitive application data. + /// is disabled by default and should never be enabled in a production environment. + /// Messages and options are not logged at other logging levels. + /// + /// public static EmbeddingGeneratorBuilder UseLogging( this EmbeddingGeneratorBuilder builder, ILoggerFactory? loggerFactory = null, diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs index 6c5bf0ed929..e7bf7850a94 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClient.cs @@ -15,10 +15,18 @@ namespace Microsoft.Extensions.AI; /// A delegating speech to text client that logs speech to text operations to an . +/// /// /// The provided implementation of is thread-safe for concurrent use so long as the /// employed is also thread-safe for concurrent use. /// +/// +/// When the employed enables , the contents of +/// messages and options are logged. These messages and options may contain sensitive application data. +/// is disabled by default and should never be enabled in a production environment. +/// Messages and options are not logged at other logging levels. +/// +/// [Experimental("MEAI001")] public partial class LoggingSpeechToTextClient : DelegatingSpeechToTextClient { diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs similarity index 79% rename from src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs index 7ce2b19ac37..92a67189982 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. [Experimental("MEAI001")] -public static class SpeechToTextClientBuilderExtensions +public static class LoggingSpeechToTextClientBuilderExtensions { /// Adds logging to the audio transcription client pipeline. /// The . @@ -22,6 +22,14 @@ public static class SpeechToTextClientBuilderExtensions /// /// An optional callback that can be used to configure the instance. /// The . + /// + /// + /// When the employed enables , the contents of + /// messages and options are logged. These messages and options may contain sensitive application data. + /// is disabled by default and should never be enabled in a production environment. + /// Messages and options are not logged at other logging levels. + /// + /// public static SpeechToTextClientBuilder UseLogging( this SpeechToTextClientBuilder builder, ILoggerFactory? loggerFactory = null, From 25d2dd43a00da85e14539076074b077a5b7fc164 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 8 May 2025 23:28:53 -0400 Subject: [PATCH 079/472] Fix test validation of aggregate usage counts (#6401) --- .../ChatClientIntegrationTests.cs | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 0e4311a227a..edb6c5dd14c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -229,16 +229,7 @@ public virtual async Task FunctionInvocation_AutomaticallyInvokeFunction_Paramet }); Assert.Contains(secretNumber.ToString(), response.Text); - - // If the underlying IChatClient provides usage data, function invocation should aggregate the - // usage data across all calls to produce a single Usage value on the final response - if (response.Usage is { } finalUsage) - { - var totalInputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.input_tokens")!); - var totalOutputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.output_tokens")!); - Assert.Equal(totalInputTokens, finalUsage.InputTokenCount); - Assert.Equal(totalOutputTokens, finalUsage.OutputTokenCount); - } + AssertUsageAgainstActivities(response, activities); } [ConditionalFact] @@ -306,16 +297,7 @@ public virtual async Task FunctionInvocation_OptionalParameter() }); Assert.Contains(secretNumber.ToString(), response.Text); - - // If the underlying IChatClient provides usage data, function invocation should aggregate the - // usage data across all calls to produce a single Usage value on the final response - if (response.Usage is { } finalUsage) - { - var totalInputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.input_tokens")!); - var totalOutputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.output_tokens")!); - Assert.Equal(totalInputTokens, finalUsage.InputTokenCount); - Assert.Equal(totalOutputTokens, finalUsage.OutputTokenCount); - } + AssertUsageAgainstActivities(response, activities); } [ConditionalFact] @@ -347,15 +329,21 @@ public virtual async Task FunctionInvocation_NestedParameters() }); Assert.Contains((secretNumber + 19).ToString(), response.Text); + AssertUsageAgainstActivities(response, activities); + } + private static void AssertUsageAgainstActivities(ChatResponse response, List activities) + { // If the underlying IChatClient provides usage data, function invocation should aggregate the - // usage data across all calls to produce a single Usage value on the final response + // usage data across all calls to produce a single Usage value on the final response. + // The FunctionInvokingChatClient then itself creates a span that will also be tagged with a sum + // across all consituent calls, which means our final answer will be double. if (response.Usage is { } finalUsage) { var totalInputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.input_tokens")!); var totalOutputTokens = activities.Sum(a => (int?)a.GetTagItem("gen_ai.response.output_tokens")!); - Assert.Equal(totalInputTokens, finalUsage.InputTokenCount); - Assert.Equal(totalOutputTokens, finalUsage.OutputTokenCount); + Assert.Equal(totalInputTokens, finalUsage.InputTokenCount * 2); + Assert.Equal(totalOutputTokens, finalUsage.OutputTokenCount * 2); } } From c49594b4e6d1a6e4daa095ffc9c564541f476a37 Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Fri, 9 May 2025 15:39:40 +0200 Subject: [PATCH 080/472] Add buffering readme (#6403) Co-authored-by: evgenyfedorov2 --- .../README.md | 54 ++++++++++++++ .../Microsoft.Extensions.Telemetry/README.md | 71 ++++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/README.md b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/README.md index 0bb66555216..bf71faddf8f 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/README.md +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/README.md @@ -20,6 +20,60 @@ Or directly in the C# project file: ## Usage Example +### Log Buffering + +Provides a buffering mechanism for logs, allowing you to store logs in temporary circular buffers in memory. If the buffer is full, the oldest logs will be dropped. If you want to emit the buffered logs, you can call `Flush()` on the buffer. That way, if you don't flush buffers, all buffered logs will eventually be dropped and that makes sense - if you don't flush buffers, chances are +those logs are not important. At the same time, you can trigger a flush on the buffer when certain conditions are met, such as when an exception occurs. + +#### Per-request Buffering + +Provides HTTP request-scoped buffering for web applications: + +```csharp +// Simple configuration with log level +builder.Logging.AddPerIncomingRequestBuffer(LogLevel.Warning); // Buffer Warning and lower level logs per request + +// Configuration using options +builder.Logging.AddPerIncomingRequestBuffer(options => +{ + options.Rules.Add(new LogBufferingFilterRule(logLevel: LogLevel.Information)); // Buffer Information and lower level logs + options.Rules.Add(new LogBufferingFilterRule(categoryName: "Microsoft.*")); // Buffer logs from Microsoft namespaces +}); + +// Configuration using IConfiguration +builder.Logging.AddPerIncomingRequestBuffer(configuration.GetSection("Logging:RequestBuffering")); +``` + +Then, to flush the buffers when a bad thing happens, call the `Flush()` method on the injected `PerRequestLogBuffer` instance: + +```csharp +public class MyService +{ + private readonly PerRequestLogBuffer _perRequestLogBuffer; + + public MyService(PerRequestLogBuffer perRequestLogBuffer) + { + _perRequestLogBuffer = perRequestLogBuffer; + } + + public void DoSomething() + { + try + { + // ... + } + catch (Exception ex) + { + // Flush all buffers + _perRequestLogBuffer.Flush(); + } + } +} +``` + +Per-request buffering is especially useful for capturing all logs related to a specific HTTP request and making decisions about them collectively based on request outcomes. +Per-request buffering is tightly coupled with [Global Buffering](https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.Telemetry/README.md#log-buffering). If a log entry is supposed to be buffered to a per-request buffer, but there is no active HTTP context, it will be buffered to the global buffer instead. If buffer flush is triggered, the per-request buffer will be flushed first, followed by the global buffer. + ### Tracking HTTP Request Latency These components enable tracking and reporting the latency of HTTP request processing. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/README.md b/src/Libraries/Microsoft.Extensions.Telemetry/README.md index 07f825baadf..862a3fcc1b7 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/README.md +++ b/src/Libraries/Microsoft.Extensions.Telemetry/README.md @@ -20,7 +20,7 @@ Or directly in the C# project file: ## Usage -### Logging Sampling +### Log Sampling The library provides two types of log sampling mechanisms: **Random Probabilistic Sampling** and **Trace-based Sampling**. @@ -46,6 +46,74 @@ builder.Logging.AddRandomProbabilisticSampler(configuration.GetSection("Logging: The Random Probabilistic Sampler supports the `IOptionsMonitor` pattern, allowing for dynamic configuration updates. This means you can change the sampling rules at runtime without needing to restart your application. +### Log Buffering + +Provides a buffering mechanism for logs, allowing you to store logs in temporary circular buffers in memory. If the buffer is full, the oldest logs will be dropped. If you want to emit the buffered logs, you can call `Flush()` on the buffer. That way, if you don't flush buffers, all buffered logs will eventually be dropped and that makes sense - if you don't flush buffers, chances are +those logs are not important. At the same time, you can trigger a flush on the buffer when certain conditions are met, such as when an exception occurs. + +This library works with all logger providers, even if they do not implement the `Microsoft.Extensions.Logging.Abstractions.IBufferedLogger` interface. In that case, the library will +be calling `ILogger.Log()` method directly on every single buffered log record when flushing the buffer. + +#### Global Buffering + +Provides application-wide log buffering with configurable rules: + +```csharp +// Simple configuration with log level +builder.Logging.AddGlobalBuffer(LogLevel.Warning); // Buffer Warning and lower level logs + +// Configuration using options +builder.Logging.AddGlobalBuffer(options => +{ + options.Rules.Add(new LogBufferingFilterRule(logLevel: LogLevel.Information)); // Buffer Information and lower level logs + options.Rules.Add(new LogBufferingFilterRule(categoryName: "Microsoft.*")); // Buffer logs from Microsoft namespaces +}); + +// Configuration using IConfiguration +builder.Logging.AddGlobalBuffer(configuration.GetSection("Logging:Buffering")); +``` + +Then, to flush the global buffer when a bad thing happens, call the `Flush()` method on the injected GlobalLogBuffer instance: + +```csharp +public class MyService +{ + private readonly GlobalLogBuffer _globalLogBuffer; + + public MyService(GlobalLogBuffer globalLogBuffer) + { + _globalLogBuffer = globalLogBuffer; + } + + public void DoSomething() + { + try + { + // ... + } + catch (Exception ex) + { + // Flush the global buffer when an exception occurs + _globalLogBuffer.Flush(); + } + } +} +``` + +The Global Log Buffer supports the `IOptionsMonitor` pattern, allowing for dynamic configuration updates. This means you can change the buffering rules at runtime without needing to restart your application. + +##### Limitations + +1. This library does not preserve the order of log records. However, original timestamps are preserved. +1. The library does not support custom configuration per each logger provider. Same configuration is applied to all logger providers. +1. When buffering and then flushing buffers, not all information of the original log record is preserved. This is due to serialing/deserialing limitation, but can be +revisited in future. Namely, this library uses `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord` class when converting buffered log records to actual log records, but omits following properties: + +- `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.ActivitySpanId` +- `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.ActivityTraceId` +- `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.ManagedThreadId` +- `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.MessageTemplate` + #### Trace-Based Sampling Matches logging sampling decisions with the underlying [Distributed Tracing sampling decisions](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling): @@ -54,6 +122,7 @@ Matches logging sampling decisions with the underlying [Distributed Tracing samp // Add trace-based sampler builder.Logging.AddTraceBasedSampler(); ``` + This comes in handy when you already use OpenTelemetry .NET Tracing and would like to see sampling decisions being consistent across both logs and their underlying [`Activity`](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling). ### Service Log Enrichment From ce3913d1853aadaec0919e9151aee93c0b46e7a9 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 9 May 2025 10:04:58 -0400 Subject: [PATCH 081/472] Add BinaryEmbedding (#6398) * Add BinaryEmbedding Also: - Renames the polymorphic discriminators to conform with typical lingo for these types. - Adds an Embedding.Dimensions virtual property. --- .../Embeddings/BinaryEmbedding.cs | 111 ++++++++++++++++++ .../Embeddings/Embedding.cs | 20 +++- .../Embeddings/Embedding{T}.cs | 5 + .../Embeddings/BinaryEmbeddingTests.cs | 95 +++++++++++++++ .../Embeddings/EmbeddingTests.cs | 34 +++++- .../BinaryEmbedding.cs | 16 --- .../EmbeddingGeneratorIntegrationTests.cs | 12 +- .../QuantizationEmbeddingGenerator.cs | 5 +- 8 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/BinaryEmbedding.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/BinaryEmbeddingTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/BinaryEmbedding.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/BinaryEmbedding.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/BinaryEmbedding.cs new file mode 100644 index 00000000000..2261fd97949 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/BinaryEmbedding.cs @@ -0,0 +1,111 @@ +// 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.Buffers; +using System.Collections; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents an embedding composed of a bit vector. +public sealed class BinaryEmbedding : Embedding +{ + /// The embedding vector this embedding represents. + private BitArray _vector; + + /// Initializes a new instance of the class with the embedding vector. + /// The embedding vector this embedding represents. + /// is . + public BinaryEmbedding(BitArray vector) + { + _vector = Throw.IfNull(vector); + } + + /// Gets or sets the embedding vector this embedding represents. + [JsonConverter(typeof(VectorConverter))] + public BitArray Vector + { + get => _vector; + set => _vector = Throw.IfNull(value); + } + + /// + [JsonIgnore] + public override int Dimensions => _vector.Length; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class VectorConverter : JsonConverter + { + /// + public override BitArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + _ = Throw.IfNull(typeToConvert); + _ = Throw.IfNull(options); + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("Expected string property."); + } + + ReadOnlySpan utf8; + byte[]? tmpArray = null; + if (!reader.HasValueSequence && !reader.ValueIsEscaped) + { + utf8 = reader.ValueSpan; + } + else + { + // This path should be rare. + int length = reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length; + tmpArray = ArrayPool.Shared.Rent(length); + utf8 = tmpArray.AsSpan(0, reader.CopyString(tmpArray)); + } + + BitArray result = new(utf8.Length); + + for (int i = 0; i < utf8.Length; i++) + { + result[i] = utf8[i] switch + { + (byte)'0' => false, + (byte)'1' => true, + _ => throw new JsonException("Expected binary character sequence.") + }; + } + + if (tmpArray is not null) + { + ArrayPool.Shared.Return(tmpArray); + } + + return result; + } + + /// + public override void Write(Utf8JsonWriter writer, BitArray value, JsonSerializerOptions options) + { + _ = Throw.IfNull(writer); + _ = Throw.IfNull(value); + _ = Throw.IfNull(options); + + int length = value.Length; + + byte[] tmpArray = ArrayPool.Shared.Rent(length); + + Span utf8 = tmpArray.AsSpan(0, length); + for (int i = 0; i < utf8.Length; i++) + { + utf8[i] = value[i] ? (byte)'1' : (byte)'0'; + } + + writer.WriteStringValue(utf8); + + ArrayPool.Shared.Return(tmpArray); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding.cs index 19b8feaa182..d6596e1e53e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI; @@ -9,13 +10,15 @@ namespace Microsoft.Extensions.AI; /// Represents an embedding generated by a . /// This base class provides metadata about the embedding. Derived types provide the concrete data contained in the embedding. [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(BinaryEmbedding), typeDiscriminator: "binary")] +[JsonDerivedType(typeof(Embedding), typeDiscriminator: "uint8")] +[JsonDerivedType(typeof(Embedding), typeDiscriminator: "int8")] #if NET -[JsonDerivedType(typeof(Embedding), typeDiscriminator: "halves")] +[JsonDerivedType(typeof(Embedding), typeDiscriminator: "float16")] #endif -[JsonDerivedType(typeof(Embedding), typeDiscriminator: "floats")] -[JsonDerivedType(typeof(Embedding), typeDiscriminator: "doubles")] -[JsonDerivedType(typeof(Embedding), typeDiscriminator: "bytes")] -[JsonDerivedType(typeof(Embedding), typeDiscriminator: "sbytes")] +[JsonDerivedType(typeof(Embedding), typeDiscriminator: "float32")] +[JsonDerivedType(typeof(Embedding), typeDiscriminator: "float64")] +[DebuggerDisplay("Dimensions = {Dimensions}")] public class Embedding { /// Initializes a new instance of the class. @@ -26,6 +29,13 @@ protected Embedding() /// Gets or sets a timestamp at which the embedding was created. public DateTimeOffset? CreatedAt { get; set; } + /// Gets the dimensionality of the embedding vector. + /// + /// This value corresponds to the number of elements in the embedding vector. + /// + [JsonIgnore] + public virtual int Dimensions { get; } + /// Gets or sets the model ID using in the creation of the embedding. public string? ModelId { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding{T}.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding{T}.cs index c80e20dfda4..22bc02f2f3f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding{T}.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding{T}.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI; @@ -19,4 +20,8 @@ public Embedding(ReadOnlyMemory vector) /// Gets or sets the embedding vector this embedding represents. public ReadOnlyMemory Vector { get; set; } + + /// + [JsonIgnore] + public override int Dimensions => Vector.Length; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/BinaryEmbeddingTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/BinaryEmbeddingTests.cs new file mode 100644 index 00000000000..c75d715466e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/BinaryEmbeddingTests.cs @@ -0,0 +1,95 @@ +// 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; +using System.Linq; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class BinaryEmbeddingTests +{ + [Fact] + public void Ctor_Roundtrips() + { + BitArray vector = new BitArray(new bool[] { false, true, false, true }); + + BinaryEmbedding e = new(vector); + Assert.Same(vector, e.Vector); + Assert.Null(e.ModelId); + Assert.Null(e.CreatedAt); + Assert.Null(e.AdditionalProperties); + } + + [Fact] + public void Properties_Roundtrips() + { + BitArray vector = new BitArray(new bool[] { false, true, false, true }); + + BinaryEmbedding e = new(vector); + + Assert.Same(vector, e.Vector); + BitArray newVector = new BitArray(new bool[] { true, false, true, false }); + e.Vector = newVector; + Assert.Same(newVector, e.Vector); + + Assert.Null(e.ModelId); + e.ModelId = "text-embedding-3-small"; + Assert.Equal("text-embedding-3-small", e.ModelId); + + Assert.Null(e.CreatedAt); + DateTimeOffset createdAt = DateTimeOffset.Parse("2022-01-01T00:00:00Z"); + e.CreatedAt = createdAt; + Assert.Equal(createdAt, e.CreatedAt); + + Assert.Null(e.AdditionalProperties); + AdditionalPropertiesDictionary props = new(); + e.AdditionalProperties = props; + Assert.Same(props, e.AdditionalProperties); + } + + [Fact] + public void Serialization_Roundtrips() + { + foreach (int length in Enumerable.Range(0, 64).Concat(new[] { 10_000 })) + { + bool[] bools = new bool[length]; + Random r = new(42); + for (int i = 0; i < length; i++) + { + bools[i] = r.Next(2) != 0; + } + + BitArray vector = new BitArray(bools); + BinaryEmbedding e = new(vector); + + string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); + Assert.Equal($$"""{"$type":"binary","vector":"{{string.Concat(vector.Cast().Select(b => b ? '1' : '0'))}}"}""", json); + + BinaryEmbedding result = Assert.IsType(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); + Assert.Equal(e.Vector, result.Vector); + } + } + + [Fact] + public void Derialization_SupportsEncodedBits() + { + BinaryEmbedding result = Assert.IsType(JsonSerializer.Deserialize( + """{"$type":"binary","vector":"\u0030\u0031\u0030\u0031\u0030\u0031"}""", + TestJsonSerializerContext.Default.Embedding)); + + Assert.Equal(new BitArray(new[] { false, true, false, true, false, true }), result.Vector); + } + + [Theory] + [InlineData("""{"$type":"binary","vector":"\u0030\u0032"}""")] + [InlineData("""{"$type":"binary","vector":"02"}""")] + [InlineData("""{"$type":"binary","vector":" "}""")] + [InlineData("""{"$type":"binary","vector":10101}""")] + public void Derialization_InvalidBinaryEmbedding_Throws(string json) + { + Assert.Throws(() => JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingTests.cs index 45fcce8ba63..c3809782006 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingTests.cs @@ -14,7 +14,7 @@ public class EmbeddingTests public void Embedding_Ctor_Roundtrips() { float[] floats = [1f, 2f, 3f]; - UsageDetails usage = new(); + AdditionalPropertiesDictionary props = []; var createdAt = DateTimeOffset.Parse("2022-01-01T00:00:00Z"); const string Model = "text-embedding-3-small"; @@ -35,6 +35,32 @@ public void Embedding_Ctor_Roundtrips() Assert.Same(floats, array.Array); } + [Fact] + public void Embedding_Byte_SerializationRoundtrips() + { + byte[] bytes = [1, 2, 3]; + Embedding e = new(bytes); + + string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); + Assert.Equal("""{"$type":"uint8","vector":"AQID"}""", json); + + Embedding result = Assert.IsType>(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); + Assert.Equal(e.Vector.ToArray(), result.Vector.ToArray()); + } + + [Fact] + public void Embedding_SByte_SerializationRoundtrips() + { + sbyte[] bytes = [1, 2, 3]; + Embedding e = new(bytes); + + string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); + Assert.Equal("""{"$type":"int8","vector":[1,2,3]}""", json); + + Embedding result = Assert.IsType>(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); + Assert.Equal(e.Vector.ToArray(), result.Vector.ToArray()); + } + #if NET [Fact] public void Embedding_Half_SerializationRoundtrips() @@ -43,7 +69,7 @@ public void Embedding_Half_SerializationRoundtrips() Embedding e = new(halfs); string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); - Assert.Equal("""{"$type":"halves","vector":[1,2,3]}""", json); + Assert.Equal("""{"$type":"float16","vector":[1,2,3]}""", json); Embedding result = Assert.IsType>(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); Assert.Equal(e.Vector.ToArray(), result.Vector.ToArray()); @@ -57,7 +83,7 @@ public void Embedding_Single_SerializationRoundtrips() Embedding e = new(floats); string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); - Assert.Equal("""{"$type":"floats","vector":[1,2,3]}""", json); + Assert.Equal("""{"$type":"float32","vector":[1,2,3]}""", json); Embedding result = Assert.IsType>(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); Assert.Equal(e.Vector.ToArray(), result.Vector.ToArray()); @@ -70,7 +96,7 @@ public void Embedding_Double_SerializationRoundtrips() Embedding e = new(floats); string json = JsonSerializer.Serialize(e, TestJsonSerializerContext.Default.Embedding); - Assert.Equal("""{"$type":"doubles","vector":[1,2,3]}""", json); + Assert.Equal("""{"$type":"float64","vector":[1,2,3]}""", json); Embedding result = Assert.IsType>(JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Embedding)); Assert.Equal(e.Vector.ToArray(), result.Vector.ToArray()); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/BinaryEmbedding.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/BinaryEmbedding.cs deleted file mode 100644 index f538d1476b0..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/BinaryEmbedding.cs +++ /dev/null @@ -1,16 +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; - -namespace Microsoft.Extensions.AI; - -internal sealed class BinaryEmbedding : Embedding -{ - public BinaryEmbedding(ReadOnlyMemory bits) - { - Bits = bits; - } - - public ReadOnlyMemory Bits { get; } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs index 1188e899e4d..1504d0d2488 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +#if NET +using System.Collections; +#endif using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -148,7 +151,14 @@ public async Task Quantization_Binary_EmbeddingsCompareSuccessfully() { for (int j = 0; j < embeddings.Count; j++) { - distances[i, j] = TensorPrimitives.HammingBitDistance(embeddings[i].Bits.Span, embeddings[j].Bits.Span); + distances[i, j] = TensorPrimitives.HammingBitDistance(ToArray(embeddings[i].Vector), ToArray(embeddings[j].Vector)); + + static byte[] ToArray(BitArray array) + { + byte[] result = new byte[(array.Length + 7) / 8]; + array.CopyTo(result, 0); + return result; + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs index 3bf33988146..ea87408da38 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; #if NET @@ -46,12 +47,12 @@ private static BinaryEmbedding QuantizeToBinary(Embedding embedding) { ReadOnlySpan vector = embedding.Vector.Span; - var result = new byte[(int)Math.Ceiling(vector.Length / 8.0)]; + var result = new BitArray(vector.Length); for (int i = 0; i < vector.Length; i++) { if (vector[i] > 0) { - result[i / 8] |= (byte)(1 << (i % 8)); + result[i / 8] = true; } } From 62178f5cda527061eeae7cc57b7f9b4ff2397a79 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Fri, 9 May 2025 11:33:25 -0700 Subject: [PATCH 082/472] Some API related fixes for the evaluation libraries (#6402) * Rename IResultStore and IResponseCacheProvider IResultStore -> IEvaluationResultStore and IResponseCacheProvider -> IEvaluationResponseCacheProvider * Include missing EvaluationContextConverter in AzureStorageJsonUtilities Also use linked files to avoid the need to duplicate code. * Reorder enum members The new order goes from least desirable rating to most desirable. * Refactor extension method overloads Implement overloads that take ChatMessage by calling corresponding overloads that take ChatResponse. * Refactor AddTurnDetails to support adding details for multiple turns Adding single turns continues to be supported via a params array overload. * Add missing parameter for timeToLiveForCacheEntries to DiskBasedReportingConfiguration This was missed in an earlier PR that introduced the timeToLiveForCacheEntries on the constructor of DiskBasedResponseCacheProvider. Also reorder constructor parameters for AzureStorageReportingConfiguration so that the parameters for caching apear next to each other and so that the parameter ordering is aligned with DiskBasedReportingConfiguration. * Minor formatting changes --- .../Commands/CleanCacheCommand.cs | 2 +- .../Commands/CleanResultsCommand.cs | 2 +- .../Commands/ReportCommand.cs | 2 +- .../AzureStorageCamelCaseEnumConverter.cs | 11 -------- .../AzureStorageJsonUtilities.cs | 12 ++++---- .../AzureStorageTimeSpanConverter.cs | 17 ----------- ...sions.AI.Evaluation.Reporting.Azure.csproj | 6 ++++ .../AzureStorageReportingConfiguration.cs | 14 +++++----- .../AzureStorageResponseCacheProvider.cs | 6 ++-- .../Storage/AzureStorageResultStore.cs | 6 ++-- .../CSharp/ChatDetailsExtensions.cs | 28 +++++++++++++++---- .../CSharp/Defaults.cs | 2 +- ...cs => IEvaluationResponseCacheProvider.cs} | 26 +++++++++-------- ...sultStore.cs => IEvaluationResultStore.cs} | 2 +- .../CSharp/JsonSerialization/JsonUtilities.cs | 5 ++-- .../CSharp/ReportingConfiguration.cs | 19 +++++++------ .../CSharp/ScenarioRun.cs | 6 ++-- .../CSharp/ScenarioRunExtensions.cs | 19 ++++--------- .../DiskBasedReportingConfiguration.cs | 11 ++++++-- .../Storage/DiskBasedResponseCacheProvider.cs | 14 ++++++---- .../CSharp/Storage/DiskBasedResultStore.cs | 4 +-- .../AIContentExtensions.cs | 1 + .../ContentSafetyServicePayloadFormat.cs | 2 +- .../EvaluationRating.cs | 16 +++++------ .../EvaluatorExtensions.cs | 19 ++++--------- .../QualityEvaluatorTests.cs | 2 +- .../AzureStorage/AzureResponseCacheTests.cs | 4 +-- .../AzureStorage/AzureResultStoreTests.cs | 2 +- .../DiskBased/DiskBasedResponseCacheTests.cs | 4 +-- .../DiskBased/DiskBasedResultStoreTests.cs | 2 +- .../ResponseCacheTester.cs | 16 +++++------ .../ResultStoreTester.cs | 17 +++++------ 32 files changed, 147 insertions(+), 152 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageCamelCaseEnumConverter.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageTimeSpanConverter.cs rename src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/{IResponseCacheProvider.cs => IEvaluationResponseCacheProvider.cs} (58%) rename src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/{IResultStore.cs => IEvaluationResultStore.cs} (99%) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanCacheCommand.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanCacheCommand.cs index 08f035d55eb..b0d975edb43 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanCacheCommand.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanCacheCommand.cs @@ -18,7 +18,7 @@ internal sealed class CleanCacheCommand(ILogger logger) { internal async Task InvokeAsync(DirectoryInfo? storageRootDir, Uri? endpointUri, CancellationToken cancellationToken = default) { - IResponseCacheProvider cacheProvider; + IEvaluationResponseCacheProvider cacheProvider; if (storageRootDir is not null) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanResultsCommand.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanResultsCommand.cs index 59635dc0530..8d6617d8302 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanResultsCommand.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/CleanResultsCommand.cs @@ -23,7 +23,7 @@ internal async Task InvokeAsync( int lastN, CancellationToken cancellationToken = default) { - IResultStore resultStore; + IEvaluationResultStore resultStore; if (storageRootDir is not null) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/ReportCommand.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/ReportCommand.cs index f2466923bd8..2611695e542 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/ReportCommand.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Commands/ReportCommand.cs @@ -28,7 +28,7 @@ internal async Task InvokeAsync( Format format, CancellationToken cancellationToken = default) { - IResultStore resultStore; + IEvaluationResultStore resultStore; if (storageRootDir is not null) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageCamelCaseEnumConverter.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageCamelCaseEnumConverter.cs deleted file mode 100644 index 2ec6cdb801f..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageCamelCaseEnumConverter.cs +++ /dev/null @@ -1,11 +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 System.Text.Json.Serialization; - -namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; - -internal sealed class AzureStorageCamelCaseEnumConverter() : - JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - where TEnum : struct, System.Enum; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs index 23b2ae9c88c..b36c8d8bd56 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -11,7 +12,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; internal static partial class AzureStorageJsonUtilities { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] internal static class Default { private static JsonSerializerOptions? _options; @@ -24,6 +25,7 @@ internal static class Compact { private static JsonSerializerOptions? _options; internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: false); + internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); } @@ -45,14 +47,14 @@ private static JsonSerializerOptions CreateJsonSerializerOptions(bool writeInden [JsonSerializable(typeof(CacheEntry))] [JsonSourceGenerationOptions( Converters = [ - typeof(AzureStorageCamelCaseEnumConverter), - typeof(AzureStorageCamelCaseEnumConverter), - typeof(AzureStorageTimeSpanConverter) + typeof(CamelCaseEnumConverter), + typeof(CamelCaseEnumConverter), + typeof(TimeSpanConverter), + typeof(EvaluationContextConverter) ], WriteIndented = true, IgnoreReadOnlyProperties = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] private sealed partial class JsonContext : JsonSerializerContext; - } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageTimeSpanConverter.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageTimeSpanConverter.cs deleted file mode 100644 index 0c064ededd3..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageTimeSpanConverter.cs +++ /dev/null @@ -1,17 +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 System.Text.Json.Serialization; - -namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; - -internal sealed class AzureStorageTimeSpanConverter : JsonConverter -{ - public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => TimeSpan.FromSeconds(reader.GetDouble()); - - public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) - => writer.WriteNumberValue(value.TotalSeconds); -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj index 669bbab7556..237df014d0d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj @@ -17,6 +17,12 @@ 0 + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs index 9302107b926..fafd8639b34 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs @@ -24,10 +24,6 @@ public static class AzureStorageReportingConfiguration /// /// The set of s that should be invoked to evaluate AI responses. /// - /// - /// An optional that specifies the maximum amount of time that cached AI responses should - /// survive in the cache before they are considered expired and evicted. - /// /// /// A that specifies the that is used by AI-based /// included in the returned . Can be omitted if @@ -36,6 +32,10 @@ public static class AzureStorageReportingConfiguration /// /// to enable caching of AI responses; otherwise. /// + /// + /// An optional that specifies the maximum amount of time that cached AI responses should + /// survive in the cache before they are considered expired and evicted. + /// /// /// An optional collection of unique strings that should be hashed when generating the cache keys for cached AI /// responses. See for more information about this concept. @@ -63,21 +63,21 @@ public static class AzureStorageReportingConfiguration public static ReportingConfiguration Create( DataLakeDirectoryClient client, IEnumerable evaluators, - TimeSpan? timeToLiveForCacheEntries = null, ChatConfiguration? chatConfiguration = null, bool enableResponseCaching = true, + TimeSpan? timeToLiveForCacheEntries = null, IEnumerable? cachingKeys = null, string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, IEnumerable? tags = null) #pragma warning restore S107 { - IResponseCacheProvider? responseCacheProvider = + IEvaluationResponseCacheProvider? responseCacheProvider = chatConfiguration is not null && enableResponseCaching ? new AzureStorageResponseCacheProvider(client, timeToLiveForCacheEntries) : null; - IResultStore resultStore = new AzureStorageResultStore(client); + IEvaluationResultStore resultStore = new AzureStorageResultStore(client); return new ReportingConfiguration( evaluators, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs index a890fa80332..6c6d1431a1a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs @@ -15,8 +15,8 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// -/// An that returns an that can cache AI responses -/// for a particular under an Azure Storage container. +/// An that returns an that can cache AI +/// responses for a particular under an Azure Storage container. /// /// /// A with access to an Azure Storage container under which the cached AI @@ -28,7 +28,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// public sealed class AzureStorageResponseCacheProvider( DataLakeDirectoryClient client, - TimeSpan? timeToLiveForCacheEntries = null) : IResponseCacheProvider + TimeSpan? timeToLiveForCacheEntries = null) : IEvaluationResponseCacheProvider { private readonly Func _provideDateTime = () => DateTime.Now; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResultStore.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResultStore.cs index 70d988abe74..71682f13651 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResultStore.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResultStore.cs @@ -20,14 +20,14 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// -/// An implementation that stores s under an Azure Storage -/// container. +/// An implementation that stores s under an Azure +/// Storage container. /// /// /// A with access to an Azure Storage container under which the /// s should be stored. /// -public sealed class AzureStorageResultStore(DataLakeDirectoryClient client) : IResultStore +public sealed class AzureStorageResultStore(DataLakeDirectoryClient client) : IEvaluationResultStore { private const string ResultsRootPrefix = "results"; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetailsExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetailsExtensions.cs index 006dfc741e8..71afe53217a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetailsExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetailsExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Evaluation.Reporting; @@ -11,19 +12,36 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; public static class ChatDetailsExtensions { /// - /// Adds for a particular LLM chat conversation turn to the + /// Adds for one or more LLM chat conversation turns to the /// collection. /// /// - /// The object to which the is to be added. + /// The object to which the are to be added. /// /// - /// The for a particular LLM chat conversation turn. + /// The for one or more LLM chat conversation turns. /// - public static void AddTurnDetails(this ChatDetails chatDetails, ChatTurnDetails turnDetails) + public static void AddTurnDetails(this ChatDetails chatDetails, IEnumerable turnDetails) { _ = Throw.IfNull(chatDetails); + _ = Throw.IfNull(turnDetails); - chatDetails.TurnDetails.Add(turnDetails); + foreach (ChatTurnDetails t in turnDetails) + { + chatDetails.TurnDetails.Add(t); + } } + + /// + /// Adds for one or more LLM chat conversation turns to the + /// collection. + /// + /// + /// The object to which the are to be added. + /// + /// + /// The for one or more LLM chat conversation turns. + /// + public static void AddTurnDetails(this ChatDetails chatDetails, params ChatTurnDetails[] turnDetails) + => chatDetails.AddTurnDetails(turnDetails as IEnumerable); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs index 095bdee575c..f25fa074430 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Defaults.cs @@ -27,7 +27,7 @@ public static class Defaults /// /// Gets a that specifies the default amount of time that cached AI responses should survive - /// in the 's cache before they are considered expired and evicted. + /// in the 's cache before they are considered expired and evicted. /// public static TimeSpan DefaultTimeToLiveForCacheEntries { get; } = TimeSpan.FromDays(14); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResponseCacheProvider.cs similarity index 58% rename from src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResponseCacheProvider.cs rename to src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResponseCacheProvider.cs index 6bc8ce25432..1859124a98f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResponseCacheProvider.cs @@ -12,26 +12,28 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; /// . /// /// -/// can be used to set up caching of AI-generated responses (both the AI responses -/// under evaluation as well as the AI responses for the evaluations themselves). When caching is enabled, the AI -/// responses associated with each are stored in the that is -/// returned from this . So long as the inputs (such as the content included in the -/// requests, the AI model being invoked etc.) remain unchanged, subsequent evaluations of the same -/// use the cached responses instead of invoking the AI model to generate new ones. Bypassing -/// the AI model when the inputs remain unchanged results in faster execution at a lower cost. +/// can be used to set up caching of AI-generated responses (both the AI +/// responses under evaluation as well as the AI responses for the evaluations themselves). When caching is enabled, +/// the AI responses associated with each are stored in the +/// that is returned from this . So long as the inputs (such as the +/// content included in the requests, the AI model being invoked etc.) remain unchanged, subsequent evaluations of the +/// same use the cached responses instead of invoking the AI model to generate new ones. +/// Bypassing the AI model when the inputs remain unchanged results in faster execution at a lower cost. /// -public interface IResponseCacheProvider +public interface IEvaluationResponseCacheProvider { /// - /// Returns an that caches the AI responses associated with a particular - /// . + /// Returns an that caches all the AI responses associated with the + /// with the supplied and + /// . /// /// The . /// The . /// A that can cancel the operation. /// - /// An that caches the AI responses associated with a particular - /// . + /// An that caches all the AI responses associated with the + /// with the supplied and + /// . /// ValueTask GetCacheAsync( string scenarioName, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResultStore.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResultStore.cs similarity index 99% rename from src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResultStore.cs rename to src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResultStore.cs index 3f3dea6cc7a..202a6305cd3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IResultStore.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationResultStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; /// /// Represents a store for s. /// -public interface IResultStore +public interface IEvaluationResultStore { /// /// Returns s for s filtered by the specified diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs index 9ba74009433..3a8c2af1ce2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -12,7 +13,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; internal static partial class JsonUtilities { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] internal static class Default { private static JsonSerializerOptions? _options; @@ -45,7 +46,7 @@ private static JsonSerializerOptions CreateJsonSerializerOptions(bool writeInden return options; } - [JsonSerializable(typeof(EvaluationResult))] + [JsonSerializable(typeof(ScenarioRunResult))] [JsonSerializable(typeof(Dataset))] [JsonSerializable(typeof(CacheEntry))] [JsonSourceGenerationOptions( diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs index 68a73338d88..130586de930 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs @@ -27,9 +27,10 @@ public sealed class ReportingConfiguration public IReadOnlyList Evaluators { get; } /// - /// Gets the that should be used to persist the s. + /// Gets the that should be used to persist the + /// s. /// - public IResultStore ResultStore { get; } + public IEvaluationResultStore ResultStore { get; } /// /// Gets a that specifies the that is used by @@ -38,9 +39,9 @@ public sealed class ReportingConfiguration public ChatConfiguration? ChatConfiguration { get; } /// - /// Gets the that should be used to cache AI responses. + /// Gets the that should be used to cache AI responses. /// - public IResponseCacheProvider? ResponseCacheProvider { get; } + public IEvaluationResponseCacheProvider? ResponseCacheProvider { get; } /// /// Gets the collection of unique strings that should be hashed when generating the cache keys for cached AI @@ -101,7 +102,7 @@ public sealed class ReportingConfiguration /// The set of s that should be invoked to evaluate AI responses. /// /// - /// The that should be used to persist the s. + /// The that should be used to persist the s. /// /// /// A that specifies the that is used by @@ -109,8 +110,8 @@ public sealed class ReportingConfiguration /// none of the included are AI-based. /// /// - /// The that should be used to cache AI responses. If omitted, AI responses - /// will not be cached. + /// The that should be used to cache AI responses. If omitted, AI + /// responses will not be cached. /// /// /// An optional collection of unique strings that should be hashed when generating the cache keys for cached AI @@ -134,9 +135,9 @@ public sealed class ReportingConfiguration #pragma warning disable S107 // Methods should not have too many parameters public ReportingConfiguration( IEnumerable evaluators, - IResultStore resultStore, + IEvaluationResultStore resultStore, ChatConfiguration? chatConfiguration = null, - IResponseCacheProvider? responseCacheProvider = null, + IEvaluationResponseCacheProvider? responseCacheProvider = null, IEnumerable? cachingKeys = null, string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs index eb58685bf0c..5fa46e7e4ec 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs @@ -93,7 +93,7 @@ public sealed class ScenarioRun : IAsyncDisposable public ChatConfiguration? ChatConfiguration { get; } private readonly CompositeEvaluator _compositeEvaluator; - private readonly IResultStore _resultStore; + private readonly IEvaluationResultStore _resultStore; private readonly Func? _evaluationMetricInterpreter; private readonly ChatDetails? _chatDetails; private readonly IEnumerable? _tags; @@ -106,7 +106,7 @@ internal ScenarioRun( string iterationName, string executionName, IEnumerable evaluators, - IResultStore resultStore, + IEvaluationResultStore resultStore, ChatConfiguration? chatConfiguration = null, Func? evaluationMetricInterpreter = null, ChatDetails? chatDetails = null, @@ -189,7 +189,7 @@ await _compositeEvaluator.EvaluateAsync( /// /// Disposes the and writes the to the configured - /// . + /// . /// /// A that represents the asynchronous operation. public async ValueTask DisposeAsync() diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunExtensions.cs index 3b723a2d258..08822f18d02 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunExtensions.cs @@ -85,16 +85,11 @@ public static ValueTask EvaluateAsync( this ScenarioRun scenarioRun, ChatMessage modelResponse, IEnumerable? additionalContext = null, - CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(scenarioRun); - - return scenarioRun.EvaluateAsync( - messages: [], + CancellationToken cancellationToken = default) => + scenarioRun.EvaluateAsync( new ChatResponse(modelResponse), additionalContext, cancellationToken); - } /// /// Evaluates the supplied and returns an @@ -148,16 +143,12 @@ public static ValueTask EvaluateAsync( ChatMessage userRequest, ChatMessage modelResponse, IEnumerable? additionalContext = null, - CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(scenarioRun); - - return scenarioRun.EvaluateAsync( - messages: [userRequest], + CancellationToken cancellationToken = default) => + scenarioRun.EvaluateAsync( + userRequest, new ChatResponse(modelResponse), additionalContext, cancellationToken); - } /// /// Evaluates the supplied and returns an diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs index 94ab92e177b..e967fdd1db9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs @@ -32,6 +32,10 @@ public static class DiskBasedReportingConfiguration /// /// to enable caching of AI responses; otherwise. /// + /// + /// An optional that specifies the maximum amount of time that cached AI responses should + /// survive in the cache before they are considered expired and evicted. + /// /// /// An optional collection of unique strings that should be hashed when generating the cache keys for cached AI /// responses. See for more information about this concept. @@ -61,6 +65,7 @@ public static ReportingConfiguration Create( IEnumerable evaluators, ChatConfiguration? chatConfiguration = null, bool enableResponseCaching = true, + TimeSpan? timeToLiveForCacheEntries = null, IEnumerable? cachingKeys = null, string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, @@ -69,12 +74,12 @@ public static ReportingConfiguration Create( { storageRootPath = Path.GetFullPath(storageRootPath); - IResponseCacheProvider? responseCacheProvider = + IEvaluationResponseCacheProvider? responseCacheProvider = chatConfiguration is not null && enableResponseCaching - ? new DiskBasedResponseCacheProvider(storageRootPath) + ? new DiskBasedResponseCacheProvider(storageRootPath, timeToLiveForCacheEntries) : null; - IResultStore resultStore = new DiskBasedResultStore(storageRootPath); + IEvaluationResultStore resultStore = new DiskBasedResultStore(storageRootPath); return new ReportingConfiguration( evaluators, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs index feb75df1dba..8b60fe5a272 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs @@ -14,8 +14,9 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// -/// An that returns an that can cache AI responses -/// for a particular under the specified on disk. +/// An that returns an that can cache +/// AI responses for a particular under the specified on +/// disk. /// /// /// The path to a directory on disk under which the cached AI responses should be stored. @@ -26,15 +27,18 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// public sealed class DiskBasedResponseCacheProvider( string storageRootPath, - TimeSpan? timeToLiveForCacheEntries = null) : IResponseCacheProvider + TimeSpan? timeToLiveForCacheEntries = null) : IEvaluationResponseCacheProvider { private readonly Func _provideDateTime = () => DateTime.UtcNow; /// /// Intended for testing purposes only. /// - internal DiskBasedResponseCacheProvider(string storageRootPath, Func provideDateTime) - : this(storageRootPath) + internal DiskBasedResponseCacheProvider( + string storageRootPath, + Func provideDateTime, + TimeSpan? timeToLiveForCacheEntries = null) + : this(storageRootPath, timeToLiveForCacheEntries) { _provideDateTime = provideDateTime; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs index de1517dca99..4662857ec59 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs @@ -16,9 +16,9 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Storage; /// -/// An implementation that stores s on disk. +/// An implementation that stores s on disk. /// -public sealed class DiskBasedResultStore : IResultStore +public sealed class DiskBasedResultStore : IEvaluationResultStore { private const string DeserializationFailedMessage = "Unable to deserialize the scenario run result file at {0}."; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/AIContentExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/AIContentExtensions.cs index 6ec3793d0da..0334d6aa08c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/AIContentExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/AIContentExtensions.cs @@ -4,6 +4,7 @@ using System; namespace Microsoft.Extensions.AI.Evaluation.Safety; + internal static class AIContentExtensions { internal static bool IsTextOrUsage(this AIContent content) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadFormat.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadFormat.cs index 428940955ff..b771dc008c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadFormat.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadFormat.cs @@ -9,5 +9,5 @@ internal enum ContentSafetyServicePayloadFormat QuestionAnswer, QueryResponse, ContextCompletion, - Conversation, + Conversation } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationRating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationRating.cs index 025ef58b809..1ba8ae270e6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationRating.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationRating.cs @@ -20,14 +20,14 @@ public enum EvaluationRating Inconclusive, /// - /// A value that indicates that the is interpreted as being exceptional. + /// A value that indicates that the is interpreted as being unacceptable. /// - Exceptional, + Unacceptable, /// - /// A value that indicates that the is interpreted as being good. + /// A value that indicates that the is interpreted as being poor. /// - Good, + Poor, /// /// A value that indicates that the is interpreted as being average. @@ -35,12 +35,12 @@ public enum EvaluationRating Average, /// - /// A value that indicates that the is interpreted as being poor. + /// A value that indicates that the is interpreted as being good. /// - Poor, + Good, /// - /// A value that indicates that the is interpreted as being unacceptable. + /// A value that indicates that the is interpreted as being exceptional. /// - Unacceptable, + Exceptional } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluatorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluatorExtensions.cs index 3ffda8fb8f9..8d25085f4e0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluatorExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluatorExtensions.cs @@ -131,17 +131,12 @@ public static ValueTask EvaluateAsync( ChatMessage modelResponse, ChatConfiguration? chatConfiguration = null, IEnumerable? additionalContext = null, - CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(evaluator); - - return evaluator.EvaluateAsync( - messages: [], + CancellationToken cancellationToken = default) => + evaluator.EvaluateAsync( new ChatResponse(modelResponse), chatConfiguration, additionalContext, cancellationToken); - } /// /// Evaluates the supplied and returns an @@ -225,17 +220,13 @@ public static ValueTask EvaluateAsync( ChatMessage modelResponse, ChatConfiguration? chatConfiguration = null, IEnumerable? additionalContext = null, - CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(evaluator); - - return evaluator.EvaluateAsync( - messages: [userRequest], + CancellationToken cancellationToken = default) => + evaluator.EvaluateAsync( + userRequest, new ChatResponse(modelResponse), chatConfiguration, additionalContext, cancellationToken); - } /// /// Evaluates the supplied and returns an diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs index 90f7a2b29aa..b56a2673b60 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs @@ -71,7 +71,7 @@ static QualityEvaluatorTests() DiskBasedReportingConfiguration.Create( storageRootPath: Settings.Current.StorageRootPath, evaluators: [groundednessEvaluator, equivalenceEvaluator, completenessEvaluator, retrievalEvaluator], - chatConfiguration, + chatConfiguration: chatConfiguration, executionName: Constants.Version, tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResponseCacheTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResponseCacheTests.cs index b135a64a04c..2f936621147 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResponseCacheTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResponseCacheTests.cs @@ -47,9 +47,9 @@ public async Task DisposeAsync() internal override bool IsConfigured => Settings.Current.Configured; - internal override IResponseCacheProvider CreateResponseCacheProvider() + internal override IEvaluationResponseCacheProvider CreateResponseCacheProvider() => new AzureStorageResponseCacheProvider(_dirClient!); - internal override IResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime) + internal override IEvaluationResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime) => new AzureStorageResponseCacheProvider(_dirClient!, provideDateTime); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResultStoreTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResultStoreTests.cs index 610f6345524..62163d5e681 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResultStoreTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/AzureStorage/AzureResultStoreTests.cs @@ -47,7 +47,7 @@ public async Task DisposeAsync() public override bool IsConfigured => Settings.Current.Configured; - public override IResultStore CreateResultStore() + public override IEvaluationResultStore CreateResultStore() => new AzureStorageResultStore(_dirClient!); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResponseCacheTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResponseCacheTests.cs index e0ba0c171d1..8305fe8ddb3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResponseCacheTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResponseCacheTests.cs @@ -45,9 +45,9 @@ public Task DisposeAsync() internal override bool IsConfigured => true; - internal override IResponseCacheProvider CreateResponseCacheProvider() + internal override IEvaluationResponseCacheProvider CreateResponseCacheProvider() => new DiskBasedResponseCacheProvider(UseTempStoragePath()); - internal override IResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime) + internal override IEvaluationResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime) => new DiskBasedResponseCacheProvider(UseTempStoragePath(), provideDateTime); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResultStoreTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResultStoreTests.cs index 1fee1b9996c..77cabfd7ffd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResultStoreTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/DiskBased/DiskBasedResultStoreTests.cs @@ -44,7 +44,7 @@ public Task DisposeAsync() public override bool IsConfigured => true; - public override IResultStore CreateResultStore() + public override IEvaluationResultStore CreateResultStore() => new DiskBasedResultStore(UseTempStoragePath()); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs index 76e50244b92..b69014e631b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs @@ -18,8 +18,8 @@ public abstract class ResponseCacheTester private static readonly string _keyB = "B Key"; private static readonly byte[] _responseB = Encoding.UTF8.GetBytes("Content B"); - internal abstract IResponseCacheProvider CreateResponseCacheProvider(); - internal abstract IResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime); + internal abstract IEvaluationResponseCacheProvider CreateResponseCacheProvider(); + internal abstract IEvaluationResponseCacheProvider CreateResponseCacheProvider(Func provideDateTime); internal abstract bool IsConfigured { get; } private void SkipIfNotConfigured() @@ -37,7 +37,7 @@ public async Task AddUncachedEntry() string iterationName = "TestIteration"; - IResponseCacheProvider provider = CreateResponseCacheProvider(); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(); IDistributedCache cache = await provider.GetCacheAsync(nameof(AddUncachedEntry), iterationName); Assert.NotNull(cache); @@ -58,7 +58,7 @@ public async Task RemoveCachedEntry() string iterationName = "TestIteration"; - IResponseCacheProvider provider = CreateResponseCacheProvider(); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(); IDistributedCache cache = await provider.GetCacheAsync(nameof(RemoveCachedEntry), iterationName); Assert.NotNull(cache); @@ -85,7 +85,7 @@ public async Task CacheEntryExpiration() DateTime now = DateTime.UtcNow; DateTime provideDateTime() => now; - IResponseCacheProvider provider = CreateResponseCacheProvider(provideDateTime); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(provideDateTime); IDistributedCache cache = await provider.GetCacheAsync(nameof(RemoveCachedEntry), iterationName); Assert.NotNull(cache); @@ -106,7 +106,7 @@ public async Task MultipleCacheInstances() { SkipIfNotConfigured(); - IResponseCacheProvider provider = CreateResponseCacheProvider(); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(); IDistributedCache cache = await provider.GetCacheAsync(nameof(MultipleCacheInstances), "Async"); Assert.NotNull(cache); IDistributedCache cache2 = await provider.GetCacheAsync(nameof(MultipleCacheInstances), "Async"); @@ -133,7 +133,7 @@ public async Task DeleteExpiredEntries() DateTime now = DateTime.UtcNow; DateTime provideDateTime() => now; - IResponseCacheProvider provider = CreateResponseCacheProvider(provideDateTime); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(provideDateTime); IDistributedCache cache = await provider.GetCacheAsync(nameof(RemoveCachedEntry), iterationName); Assert.NotNull(cache); @@ -163,7 +163,7 @@ public async Task ResetCache() string iterationName = "TestIteration"; - IResponseCacheProvider provider = CreateResponseCacheProvider(); + IEvaluationResponseCacheProvider provider = CreateResponseCacheProvider(); IDistributedCache cache = await provider.GetCacheAsync(nameof(RemoveCachedEntry), iterationName); Assert.NotNull(cache); diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResultStoreTester.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResultStoreTester.cs index 1ce033b3cd7..995b77a8c5e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResultStoreTester.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResultStoreTester.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.Tests; public abstract class ResultStoreTester { - public abstract IResultStore CreateResultStore(); + public abstract IEvaluationResultStore CreateResultStore(); public abstract bool IsConfigured { get; } @@ -39,7 +39,8 @@ private static ScenarioRunResult CreateTestResult(string scenarioName, string it private static string ScenarioName(int n) => $"Test.Scenario.{n}"; private static string IterationName(int n) => $"Iteration {n}"; - private static async Task> LoadResultsAsync(int n, IResultStore resultStore) + private static async Task> + LoadResultsAsync(int n, IEvaluationResultStore resultStore) { List<(string executionName, string scenarioName, string iterationName)> results = []; await foreach (string executionName in resultStore.GetLatestExecutionNamesAsync(n)) @@ -69,7 +70,7 @@ public async Task WriteAndReadResults() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string newExecutionName = $"Test Execution {Path.GetRandomFileName()}"; @@ -108,7 +109,7 @@ public async Task WriteAndReadHistoricalResults() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string firstExecutionName = $"Test Execution {Path.GetRandomFileName()}"; @@ -152,7 +153,7 @@ public async Task DeleteExecutions() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string executionName = $"Test Execution {Path.GetRandomFileName()}"; @@ -176,7 +177,7 @@ public async Task DeleteSomeExecutions() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string executionName0 = $"Test Execution {Path.GetRandomFileName()}"; @@ -211,7 +212,7 @@ public async Task DeleteScenarios() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string executionName = $"Test Execution {Path.GetRandomFileName()}"; @@ -246,7 +247,7 @@ public async Task DeleteIterations() { SkipIfNotConfigured(); - IResultStore resultStore = CreateResultStore(); + IEvaluationResultStore resultStore = CreateResultStore(); Assert.NotNull(resultStore); string executionName = $"Test Execution {Path.GetRandomFileName()}"; From 8212672a41f1edb62465912c7f0aba8e04769def Mon Sep 17 00:00:00 2001 From: Shyam N Date: Fri, 9 May 2025 13:43:33 -0700 Subject: [PATCH 083/472] Allow image rendering in evaluation report (#6407) --- .../TypeScript/html-report/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html index c34388e543c..7f6e82be184 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/html-report/index.html @@ -8,7 +8,7 @@ - + AI Evaluation Report From 4a609d6a428621d8ec35442a97997e90beb6c916 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Fri, 9 May 2025 14:05:20 -0700 Subject: [PATCH 084/472] Fix streaming chat response example (#6408) --- src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md index 94a0c53e162..214110ed028 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md @@ -101,6 +101,7 @@ while (true) await foreach (var update in client.GetStreamingResponseAsync(history)) { Console.Write(update); + updates.Add(update); } Console.WriteLine(); From f6c4baa6fe8d76cca8832b6a433b8be0155561b5 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 9 May 2025 23:39:48 -0400 Subject: [PATCH 085/472] Move AIFunctionFactory down to M.E.AI.Abstractions (#6412) * Remove AIFunctionFactory dependency on M.E.DI This means reverting the recent changes to it that: - Special-cased KeyedServices - Special-cased IServiceProviderIsService - Used ActivatorUtilities.CreateInstance * Move AIFunctionFactory down to M.E.AI.Abstractions * Add CreateInstance delegate to AIFunctionFactoryOptions To enable use of ActivatorUtilities.CreateInstance or alternative. * Add some comments --- .../Functions/AIFunctionFactory.cs | 352 ++++++++---------- .../Functions/AIFunctionFactoryOptions.cs | 18 +- .../ChatClientStructuredOutputExtensions.cs | 26 +- .../Functions/AIFunctionFactory.Utilities.cs | 137 ------- .../Functions/AIFunctionFactoryTest.cs | 92 ++--- 5 files changed, 234 insertions(+), 391 deletions(-) rename src/Libraries/{Microsoft.Extensions.AI => Microsoft.Extensions.AI.Abstractions}/Functions/AIFunctionFactory.cs (82%) rename src/Libraries/{Microsoft.Extensions.AI => Microsoft.Extensions.AI.Abstractions}/Functions/AIFunctionFactoryOptions.cs (87%) delete mode 100644 src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.Utilities.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 4878239f35b..3f090a2ac3b 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; #if !NET using System.Linq; #endif @@ -15,23 +17,26 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; #pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable S2333 // Redundant modifiers should not be used #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable SA1118 // Parameter should not span multiple lines -#pragma warning disable SA1500 // Braces for multi-line statements should not share line namespace Microsoft.Extensions.AI; -/// Provides factory methods for creating commonly used implementations of . +/// Provides factory methods for creating commonly-used implementations of . /// Invoke .NET functions using an AI model. public static partial class AIFunctionFactory { + // NOTE: + // Unlike most library code, AIFunctionFactory uses ConfigureAwait(true) rather than ConfigureAwait(false). This is to + // enable AIFunctionFactory to be used with methods that might be context-aware, such as those employing a UI framework. + /// Holds the default options instance used when creating function. private static readonly AIFunctionFactoryOptions _defaultOptions = new(); @@ -71,25 +76,6 @@ public static partial class AIFunctionFactory /// . /// /// - /// - /// - /// By default, parameters attributed with are resolved from the - /// property and are not included in the JSON schema. If the parameter is optional, such that a default value is provided, - /// is allowed to be ; otherwise, - /// must be non-, or else the invocation will fail with an exception due to the required nature of the parameter. - /// The handling of such parameters may be overridden via . - /// - /// - /// - /// - /// When the is constructed, it may be passed an via - /// . Any parameter that can be satisfied by that - /// according to will not be included in the generated JSON schema and will be resolved - /// from the provided to via , - /// rather than from the argument collection. The handling of such parameters may be overridden via - /// . - /// - /// /// /// All other parameter types are, by default, bound from the dictionary passed into /// and are included in the generated JSON schema. This may be overridden by the provided @@ -170,23 +156,6 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio /// optional or not. /// /// - /// - /// - /// By default, parameters attributed with are resolved from the - /// property and are not included in the JSON schema. If the parameter is optional, such that a default value is provided, - /// is allowed to be ; otherwise, - /// must be non-, or else the invocation will fail with an exception due to the required nature of the parameter. - /// - /// - /// - /// - /// When the is constructed, it may be passed an via - /// . Any parameter that can be satisfied by that - /// according to will not be included in the generated JSON schema and will be resolved - /// from the provided to via , - /// rather than from the argument collection. - /// - /// /// /// All other parameter types are bound from the dictionary passed into /// and are included in the generated JSON schema. @@ -270,25 +239,6 @@ public static AIFunction Create(Delegate method, string? name = null, string? de /// . /// /// - /// - /// - /// By default, parameters attributed with are resolved from the - /// property and are not included in the JSON schema. If the parameter is optional, such that a default value is provided, - /// is allowed to be ; otherwise, - /// must be non-, or else the invocation will fail with an exception due to the required nature of the parameter. - /// The handling of such parameters may be overridden via . - /// - /// - /// - /// - /// When the is constructed, it may be passed an via - /// . Any parameter that can be satisfied by that - /// according to will not be included in the generated JSON schema and will be resolved - /// from the provided to via , - /// rather than from the argument collection. The handling of such parameters may be overridden via - /// . - /// - /// /// /// All other parameter types are, by default, bound from the dictionary passed into /// and are included in the generated JSON schema. This may be overridden by the provided @@ -379,23 +329,6 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// optional or not. /// /// - /// - /// - /// By default, parameters attributed with are resolved from the - /// property and are not included in the JSON schema. If the parameter is optional, such that a default value is provided, - /// is allowed to be ; otherwise, - /// must be non-, or else the invocation will fail with an exception due to the required nature of the parameter. - /// - /// - /// - /// When the is constructed, it may be passed an via - /// . Any parameter that can be satisfied by that - /// according to will not be included in the generated JSON schema and will be resolved - /// from the provided to via , - /// rather than from the argument collection. - /// - /// - /// /// /// All other parameter types are bound from the dictionary passed into /// and are included in the generated JSON schema. @@ -447,10 +380,9 @@ public static AIFunction Create(MethodInfo method, object? target, string? name /// The instance method to be represented via the created . /// /// The to construct an instance of on which to invoke when - /// the resulting is invoked. If is provided, - /// will be used to construct the instance using those services; otherwise, - /// is used, utilizing the type's public parameterless constructor. - /// If an instance can't be constructed, an exception is thrown during the function's invocation. + /// the resulting is invoked. is used, + /// utilizing the type's public parameterless constructor. If an instance can't be constructed, an exception is + /// thrown during the function's invocation. /// /// Metadata to use to override defaults inferred from . /// The created for invoking . @@ -494,25 +426,6 @@ public static AIFunction Create(MethodInfo method, object? target, string? name /// . /// /// - /// - /// - /// By default, parameters attributed with are resolved from the - /// property and are not included in the JSON schema. If the parameter is optional, such that a default value is provided, - /// is allowed to be ; otherwise, - /// must be non-, or else the invocation will fail with an exception due to the required nature of the parameter. - /// The handling of such parameters may be overridden via . - /// - /// - /// - /// - /// When the is constructed, it may be passed an via - /// . Any parameter that can be satisfied by that - /// according to will not be included in the generated JSON schema and will be resolved - /// from the provided to via , - /// rather than from the argument collection. The handling of such parameters may be overridden via - /// . - /// - /// /// /// All other parameter types are, by default, bound from the dictionary passed into /// and are included in the generated JSON schema. This may be overridden by the provided @@ -627,6 +540,7 @@ private ReflectionAIFunction( { FunctionDescriptor = functionDescriptor; TargetType = targetType; + CreateInstance = options.CreateInstance; AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance; } @@ -634,6 +548,8 @@ private ReflectionAIFunction( public object? Target { get; } [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type? TargetType { get; } + public Func? CreateInstance { get; } + public override IReadOnlyDictionary AdditionalProperties { get; } public override string Name => FunctionDescriptor.Name; public override string Description => FunctionDescriptor.Description; @@ -654,9 +570,14 @@ private ReflectionAIFunction( Debug.Assert(target is null, "Expected target to be null when we have a non-null target type"); Debug.Assert(!FunctionDescriptor.Method.IsStatic, "Expected an instance method"); - target = arguments.Services is { } services ? - ActivatorUtilities.CreateInstance(services, targetType!) : + target = CreateInstance is not null ? + CreateInstance(targetType, arguments) : Activator.CreateInstance(targetType); + if (target is null) + { + Throw.InvalidOperationException("Unable to create an instance of the target type."); + } + disposeTarget = true; } @@ -669,7 +590,7 @@ private ReflectionAIFunction( } return await FunctionDescriptor.ReturnParameterMarshaller( - ReflectionInvoke(FunctionDescriptor.Method, target, args), cancellationToken); + ReflectionInvoke(FunctionDescriptor.Method, target, args), cancellationToken).ConfigureAwait(true); } finally { @@ -677,7 +598,7 @@ private ReflectionAIFunction( { if (target is IAsyncDisposable ad) { - await ad.DisposeAsync(); + await ad.DisposeAsync().ConfigureAwait(true); } else if (target is IDisposable d) { @@ -709,7 +630,7 @@ public static ReflectionAIFunctionDescriptor GetOrCreate(MethodInfo method, AIFu serializerOptions.MakeReadOnly(); ConcurrentDictionary innerCache = _descriptorCache.GetOrCreateValue(serializerOptions); - DescriptorKey key = new(method, options.Name, options.Description, options.ConfigureParameterBinding, options.MarshalResult, options.Services, schemaOptions); + DescriptorKey key = new(method, options.Name, options.Description, options.ConfigureParameterBinding, options.MarshalResult, schemaOptions); if (innerCache.TryGetValue(key, out ReflectionAIFunctionDescriptor? descriptor)) { return descriptor; @@ -736,8 +657,6 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions } } - IServiceProviderIsService? serviceProviderIsService = key.Services?.GetService(); - // Use that binding information to impact the schema generation. AIJsonSchemaCreateOptions schemaOptions = key.SchemaOptions with { @@ -757,21 +676,6 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions return false; } - // If the parameter is attributed as [FromKeyedServices], exclude it, as we'll instead - // get its value from the IServiceProvider. - if (parameterInfo.GetCustomAttribute(inherit: true) is not null) - { - return false; - } - - // We assume that if the services used to create the function support a particular type, - // so too do the services that will be passed into InvokeAsync. This is the same basic assumption - // made in ASP.NET. - if (serviceProviderIsService?.IsService(parameterInfo.ParameterType) is true) - { - return false; - } - // If there was an existing IncludeParameter delegate, now defer to it as we've // excluded everything we need to exclude. if (key.SchemaOptions.IncludeParameter is { } existingIncludeParameter) @@ -793,7 +697,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions options = default; } - ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i], serviceProviderIsService); + ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i]); } // Get a marshaling delegate for the return value. @@ -863,8 +767,7 @@ static bool IsAsyncMethod(MethodInfo method) private static Func GetParameterMarshaller( JsonSerializerOptions serializerOptions, AIFunctionFactoryOptions.ParameterBindingOptions bindingOptions, - ParameterInfo parameter, - IServiceProviderIsService? serviceProviderIsService) + ParameterInfo parameter) { if (string.IsNullOrWhiteSpace(parameter.Name)) { @@ -911,56 +814,6 @@ static bool IsAsyncMethod(MethodInfo method) }; } - // For [FromKeyedServices] parameters, we resolve from the services passed to InvokeAsync via AIFunctionArguments. - if (parameter.GetCustomAttribute(inherit: true) is { } keyedAttr) - { - return (arguments, _) => - { - if ((arguments.Services as IKeyedServiceProvider)?.GetKeyedService(parameterType, keyedAttr.Key) is { } service) - { - return service; - } - - if (!parameter.HasDefaultValue) - { - if (arguments.Services is null) - { - ThrowNullServices(parameter.Name); - } - - Throw.ArgumentException(nameof(arguments), $"No service of type '{parameterType}' with key '{keyedAttr.Key}' was found for parameter '{parameter.Name}'."); - } - - return parameter.DefaultValue; - }; - } - - // For any parameters that are satisfiable from the IServiceProvider, we resolve from the services passed to InvokeAsync - // via AIFunctionArguments. This is determined by the same same IServiceProviderIsService instance used to determine whether - // the parameter should be included in the schema. - if (serviceProviderIsService?.IsService(parameterType) is true) - { - return (arguments, _) => - { - if (arguments.Services?.GetService(parameterType) is { } service) - { - return service; - } - - if (!parameter.HasDefaultValue) - { - if (arguments.Services is null) - { - ThrowNullServices(parameter.Name); - } - - Throw.ArgumentException(nameof(arguments), $"No service of type '{parameterType}' was found for parameter '{parameter.Name}'."); - } - - return parameter.DefaultValue; - }; - } - // For all other parameters, create a marshaller that tries to extract the value from the arguments dictionary. // Resolve the contract used to marshal the value from JSON -- can throw if not supported or not found. JsonTypeInfo? typeInfo = serializerOptions.GetTypeInfo(parameterType); @@ -1037,14 +890,14 @@ static void ThrowNullServices(string parameterName) => { return async (result, cancellationToken) => { - await ((Task)ThrowIfNullResult(result)); - return await marshalResult(null, null, cancellationToken); + await ((Task)ThrowIfNullResult(result)).ConfigureAwait(true); + return await marshalResult(null, null, cancellationToken).ConfigureAwait(true); }; } return async static (result, _) => { - await ((Task)ThrowIfNullResult(result)); + await ((Task)ThrowIfNullResult(result)).ConfigureAwait(true); return null; }; } @@ -1056,14 +909,14 @@ static void ThrowNullServices(string parameterName) => { return async (result, cancellationToken) => { - await ((ValueTask)ThrowIfNullResult(result)); - return await marshalResult(null, null, cancellationToken); + await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(true); + return await marshalResult(null, null, cancellationToken).ConfigureAwait(true); }; } return async static (result, _) => { - await ((ValueTask)ThrowIfNullResult(result)); + await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(true); return null; }; } @@ -1078,18 +931,18 @@ static void ThrowNullServices(string parameterName) => { return async (taskObj, cancellationToken) => { - await ((Task)ThrowIfNullResult(taskObj)); + await ((Task)ThrowIfNullResult(taskObj)).ConfigureAwait(true); object? result = ReflectionInvoke(taskResultGetter, taskObj, null); - return await marshalResult(result, taskResultGetter.ReturnType, cancellationToken); + return await marshalResult(result, taskResultGetter.ReturnType, cancellationToken).ConfigureAwait(true); }; } returnTypeInfo = serializerOptions.GetTypeInfo(taskResultGetter.ReturnType); return async (taskObj, cancellationToken) => { - await ((Task)ThrowIfNullResult(taskObj)); + await ((Task)ThrowIfNullResult(taskObj)).ConfigureAwait(true); object? result = ReflectionInvoke(taskResultGetter, taskObj, null); - return await SerializeResultAsync(result, returnTypeInfo, cancellationToken); + return await SerializeResultAsync(result, returnTypeInfo, cancellationToken).ConfigureAwait(true); }; } @@ -1104,9 +957,9 @@ static void ThrowNullServices(string parameterName) => return async (taskObj, cancellationToken) => { var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(taskObj), null)!; - await task; + await task.ConfigureAwait(true); object? result = ReflectionInvoke(asTaskResultGetter, task, null); - return await marshalResult(result, asTaskResultGetter.ReturnType, cancellationToken); + return await marshalResult(result, asTaskResultGetter.ReturnType, cancellationToken).ConfigureAwait(true); }; } @@ -1114,9 +967,9 @@ static void ThrowNullServices(string parameterName) => return async (taskObj, cancellationToken) => { var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(taskObj), null)!; - await task; + await task.ConfigureAwait(true); object? result = ReflectionInvoke(asTaskResultGetter, task, null); - return await SerializeResultAsync(result, returnTypeInfo, cancellationToken); + return await SerializeResultAsync(result, returnTypeInfo, cancellationToken).ConfigureAwait(true); }; } } @@ -1140,7 +993,7 @@ static void ThrowNullServices(string parameterName) => // Serialize asynchronously to support potential IAsyncEnumerable responses. using PooledMemoryStream stream = new(); - await JsonSerializer.SerializeAsync(stream, result, returnTypeInfo, cancellationToken); + await JsonSerializer.SerializeAsync(stream, result, returnTypeInfo, cancellationToken).ConfigureAwait(true); Utf8JsonReader reader = new(stream.GetBuffer()); return JsonElement.ParseValue(ref reader); } @@ -1169,7 +1022,126 @@ private record struct DescriptorKey( string? Description, Func? GetBindParameterOptions, Func>? MarshalResult, - IServiceProvider? Services, AIJsonSchemaCreateOptions SchemaOptions); } + + /// + /// Removes characters from a .NET member name that shouldn't be used in an AI function name. + /// + /// The .NET member name that should be sanitized. + /// + /// Replaces non-alphanumeric characters in the identifier with the underscore character. + /// Primarily intended to remove characters produced by compiler-generated method name mangling. + /// + private static string SanitizeMemberName(string memberName) => + InvalidNameCharsRegex().Replace(memberName, "_"); + + /// Regex that flags any character other than ASCII digits or letters or the underscore. +#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 + + /// Invokes the MethodInfo with the specified target object and arguments. + private static object? ReflectionInvoke(MethodInfo method, object? target, object?[]? arguments) + { +#if NET + return method.Invoke(target, BindingFlags.DoNotWrapExceptions, binder: null, arguments, culture: null); +#else + try + { + return method.Invoke(target, BindingFlags.Default, binder: null, arguments, culture: null); + } + catch (TargetInvocationException e) when (e.InnerException is not null) + { + // If we're targeting .NET Framework, such that BindingFlags.DoNotWrapExceptions + // is ignored, the original exception will be wrapped in a TargetInvocationException. + // Unwrap it and throw that original exception, maintaining its stack information. + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw(); + throw; + } +#endif + } + + /// + /// Implements a simple write-only memory stream that uses pooled buffers. + /// + private sealed class PooledMemoryStream : Stream + { + private const int DefaultBufferSize = 4096; + private byte[] _buffer; + private int _position; + + public PooledMemoryStream(int initialCapacity = DefaultBufferSize) + { + _buffer = ArrayPool.Shared.Rent(initialCapacity); + _position = 0; + } + + public ReadOnlySpan GetBuffer() => _buffer.AsSpan(0, _position); + public override bool CanWrite => true; + public override bool CanRead => false; + public override bool CanSeek => false; + public override long Length => _position; + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + EnsureNotDisposed(); + EnsureCapacity(_position + count); + + Buffer.BlockCopy(buffer, offset, _buffer, _position, count); + _position += count; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (_buffer is not null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + + base.Dispose(disposing); + } + + private void EnsureCapacity(int requiredCapacity) + { + if (requiredCapacity <= _buffer.Length) + { + return; + } + + int newCapacity = Math.Max(requiredCapacity, _buffer.Length * 2); + byte[] newBuffer = ArrayPool.Shared.Rent(newCapacity); + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _position); + + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + private void EnsureNotDisposed() + { + if (_buffer is null) + { + Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(PooledMemoryStream)); + } + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs similarity index 87% rename from src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index 80c8b485c59..80ff394359d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -107,14 +107,22 @@ public AIFunctionFactoryOptions() public Func>? MarshalResult { get; set; } /// - /// Gets or sets optional services used in the construction of the . + /// Gets or sets a delegate used with to create the receiver instance. /// /// - /// These services will be used to determine which parameters should be satisifed from dependency injection. As such, - /// what services are satisfied via this provider should match what's satisfied via the provider passed into - /// via . + /// + /// creates instances that invoke an + /// instance method on the specified . This delegate is used to create the instance of the type that will be used to invoke the method. + /// By default if is , is used. If + /// is non-, the delegate is invoked with the to be instantiated and the + /// provided to the method. + /// + /// + /// Each created instance will be used for a single invocation. If the object is or , it will + /// be disposed of after the invocation completes. + /// /// - public IServiceProvider? Services { get; set; } + public Func? CreateInstance { get; set; } /// Provides configuration options produced by the delegate. public readonly record struct ParameterBindingOptions diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs index d7bc12a1a41..e35f8b87949 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs @@ -7,11 +7,13 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; #pragma warning disable SA1118 // Parameter should not span multiple lines +#pragma warning disable S2333 // Redundant modifiers should not be used namespace Microsoft.Extensions.AI; @@ -19,7 +21,7 @@ namespace Microsoft.Extensions.AI; /// Provides extension methods on that simplify working with structured output. /// /// Request a response with structured output. -public static class ChatClientStructuredOutputExtensions +public static partial class ChatClientStructuredOutputExtensions { private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new() { @@ -197,7 +199,7 @@ public static async Task> GetResponseAsync( // the LLM backend is meant to do whatever's needed to explain the schema to the LLM. options.ResponseFormat = ChatResponseFormat.ForJsonSchema( schema, - schemaName: AIFunctionFactory.SanitizeMemberName(typeof(T).Name), + schemaName: SanitizeMemberName(typeof(T).Name), schemaDescription: typeof(T).GetCustomAttribute()?.Description); } else @@ -246,4 +248,24 @@ private static bool SchemaRepresentsObject(JsonElement schemaElement) _ => JsonValue.Create(element) }; } + + /// + /// Removes characters from a .NET member name that shouldn't be used in an AI function name. + /// + /// The .NET member name that should be sanitized. + /// + /// Replaces non-alphanumeric characters in the identifier with the underscore character. + /// Primarily intended to remove characters produced by compiler-generated method name mangling. + /// + private static string SanitizeMemberName(string memberName) => + InvalidNameCharsRegex().Replace(memberName, "_"); + + /// Regex that flags any character other than ASCII digits or letters or the underscore. +#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 } diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.Utilities.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.Utilities.cs deleted file mode 100644 index cbafe78e5d3..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.Utilities.cs +++ /dev/null @@ -1,137 +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.Buffers; -using System.IO; -using System.Reflection; -using System.Text.RegularExpressions; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -public static partial class AIFunctionFactory -{ - /// - /// Removes characters from a .NET member name that shouldn't be used in an AI function name. - /// - /// The .NET member name that should be sanitized. - /// - /// Replaces non-alphanumeric characters in the identifier with the underscore character. - /// Primarily intended to remove characters produced by compiler-generated method name mangling. - /// - internal static string SanitizeMemberName(string memberName) - { - _ = Throw.IfNull(memberName); - return InvalidNameCharsRegex().Replace(memberName, "_"); - } - - /// Regex that flags any character other than ASCII digits or letters or the underscore. -#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 - - /// Invokes the MethodInfo with the specified target object and arguments. - private static object? ReflectionInvoke(MethodInfo method, object? target, object?[]? arguments) - { -#if NET - return method.Invoke(target, BindingFlags.DoNotWrapExceptions, binder: null, arguments, culture: null); -#else - try - { - return method.Invoke(target, BindingFlags.Default, binder: null, arguments, culture: null); - } - catch (TargetInvocationException e) when (e.InnerException is not null) - { - // If we're targeting .NET Framework, such that BindingFlags.DoNotWrapExceptions - // is ignored, the original exception will be wrapped in a TargetInvocationException. - // Unwrap it and throw that original exception, maintaining its stack information. - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw(); - throw; - } -#endif - } - - /// - /// Implements a simple write-only memory stream that uses pooled buffers. - /// - private sealed class PooledMemoryStream : Stream - { - private const int DefaultBufferSize = 4096; - private byte[] _buffer; - private int _position; - - public PooledMemoryStream(int initialCapacity = DefaultBufferSize) - { - _buffer = ArrayPool.Shared.Rent(initialCapacity); - _position = 0; - } - - public ReadOnlySpan GetBuffer() => _buffer.AsSpan(0, _position); - public override bool CanWrite => true; - public override bool CanRead => false; - public override bool CanSeek => false; - public override long Length => _position; - public override long Position - { - get => _position; - set => throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - EnsureNotDisposed(); - EnsureCapacity(_position + count); - - Buffer.BlockCopy(buffer, offset, _buffer, _position, count); - _position += count; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); - - protected override void Dispose(bool disposing) - { - if (_buffer is not null) - { - ArrayPool.Shared.Return(_buffer); - _buffer = null!; - } - - base.Dispose(disposing); - } - - private void EnsureCapacity(int requiredCapacity) - { - if (requiredCapacity <= _buffer.Length) - { - return; - } - - int newCapacity = Math.Max(requiredCapacity, _buffer.Length * 2); - byte[] newBuffer = ArrayPool.Shared.Rent(newCapacity); - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _position); - - ArrayPool.Shared.Return(_buffer); - _buffer = newBuffer; - } - - private void EnsureNotDisposed() - { - if (_buffer is null) - { - Throw(); - static void Throw() => throw new ObjectDisposedException(nameof(PooledMemoryStream)); - } - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 4b5ff9a0600..4f5037fc92d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -299,55 +299,6 @@ public async Task AIFunctionArguments_MissingServicesMayBeOptional() Assert.Equal("", result?.ToString()); } - [Fact] - public async Task IServiceProvider_ServicesInOptionsImpactsFunctionCreation() - { - ServiceCollection sc = new(); - sc.AddSingleton(new MyService(123)); - IServiceProvider sp = sc.BuildServiceProvider(); - - AIFunction func; - - // Services not provided to Create, non-optional argument - if (JsonSerializer.IsReflectionEnabledByDefault) - { - func = AIFunctionFactory.Create((MyService myService) => myService.Value); - Assert.Contains("myService", func.JsonSchema.ToString()); - await Assert.ThrowsAsync("arguments", () => func.InvokeAsync(new()).AsTask()); - await Assert.ThrowsAsync("arguments", () => func.InvokeAsync(new() { Services = sp }).AsTask()); - } - else - { - Assert.Throws(() => AIFunctionFactory.Create((MyService myService) => myService.Value)); - } - - // Services not provided to Create, optional argument - if (JsonSerializer.IsReflectionEnabledByDefault) - { - func = AIFunctionFactory.Create((MyService? myService = null) => myService?.Value ?? 456); - Assert.Contains("myService", func.JsonSchema.ToString()); - Assert.Contains("456", (await func.InvokeAsync(new()))?.ToString()); - Assert.Contains("456", (await func.InvokeAsync(new() { Services = sp }))?.ToString()); - } - else - { - Assert.Throws(() => AIFunctionFactory.Create((MyService myService) => myService.Value)); - } - - // Services provided to Create, non-optional argument - func = AIFunctionFactory.Create((MyService myService) => myService.Value, new() { Services = sp }); - Assert.DoesNotContain("myService", func.JsonSchema.ToString()); - await Assert.ThrowsAsync("arguments.Services", () => func.InvokeAsync(new()).AsTask()); - await Assert.ThrowsAsync("arguments", () => func.InvokeAsync(new() { Services = new ServiceCollection().BuildServiceProvider() }).AsTask()); - Assert.Contains("123", (await func.InvokeAsync(new() { Services = sp }))?.ToString()); - - // Services provided to Create, optional argument - func = AIFunctionFactory.Create((MyService? myService = null) => myService?.Value ?? 456, new() { Services = sp }); - Assert.DoesNotContain("myService", func.JsonSchema.ToString()); - Assert.Contains("456", (await func.InvokeAsync(new()))?.ToString()); - Assert.Contains("123", (await func.InvokeAsync(new() { Services = sp }))?.ToString()); - } - [Fact] public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable() { @@ -364,6 +315,11 @@ public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable( typeof(MyFunctionTypeWithOneArg), new() { + CreateInstance = (type, arguments) => + { + Assert.NotNull(arguments.Services); + return ActivatorUtilities.CreateInstance(arguments.Services, type); + }, MarshalResult = (result, type, cancellationToken) => new ValueTask(result), }); @@ -398,7 +354,7 @@ public async Task Create_NoInstance_ThrowsWhenCantConstructInstance() typeof(MyFunctionTypeWithOneArg)); Assert.NotNull(func); - await Assert.ThrowsAsync(async () => await func.InvokeAsync(new() { Services = sp })); + await Assert.ThrowsAsync(async () => await func.InvokeAsync(new() { Services = sp })); } [Fact] @@ -485,13 +441,13 @@ public async Task FromKeyedServices_ResolvesFromServiceProvider() sc.AddKeyedSingleton("key", service); IServiceProvider sp = sc.BuildServiceProvider(); - AIFunction f = AIFunctionFactory.Create(([FromKeyedServices("key")] MyService service, int myInteger) => service.Value + myInteger); + AIFunction f = AIFunctionFactory.Create(([FromKeyedServices("key")] MyService service, int myInteger) => service.Value + myInteger, + CreateKeyedServicesSupportOptions()); Assert.Contains("myInteger", f.JsonSchema.ToString()); Assert.DoesNotContain("service", f.JsonSchema.ToString()); - Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); - Assert.Contains("Services are required", e.Message); + Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); var result = await f.InvokeAsync(new() { ["myInteger"] = 1, Services = sp }); Assert.Contains("43", result?.ToString()); @@ -506,13 +462,13 @@ public async Task FromKeyedServices_NullKeysBindToNonKeyedServices() sc.AddSingleton(service); IServiceProvider sp = sc.BuildServiceProvider(); - AIFunction f = AIFunctionFactory.Create(([FromKeyedServices(null!)] MyService service, int myInteger) => service.Value + myInteger); + AIFunction f = AIFunctionFactory.Create(([FromKeyedServices(null!)] MyService service, int myInteger) => service.Value + myInteger, + CreateKeyedServicesSupportOptions()); Assert.Contains("myInteger", f.JsonSchema.ToString()); Assert.DoesNotContain("service", f.JsonSchema.ToString()); - Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); - Assert.Contains("Services are required", e.Message); + Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); var result = await f.InvokeAsync(new() { ["myInteger"] = 1, Services = sp }); Assert.Contains("43", result?.ToString()); @@ -528,7 +484,8 @@ public async Task FromKeyedServices_OptionalDefaultsToNull() IServiceProvider sp = sc.BuildServiceProvider(); AIFunction f = AIFunctionFactory.Create(([FromKeyedServices("key")] MyService? service = null, int myInteger = 0) => - service is null ? "null " + 1 : (service.Value + myInteger).ToString()); + service is null ? "null " + 1 : (service.Value + myInteger).ToString(), + CreateKeyedServicesSupportOptions()); Assert.Contains("myInteger", f.JsonSchema.ToString()); Assert.DoesNotContain("service", f.JsonSchema.ToString()); @@ -891,6 +848,27 @@ public StructWithDefaultCtor() } } + private static AIFunctionFactoryOptions CreateKeyedServicesSupportOptions() => + new AIFunctionFactoryOptions + { + ConfigureParameterBinding = p => + { + if (p.GetCustomAttribute() is { } attr) + { + return new() + { + BindParameter = (p, a) => + (a.Services as IKeyedServiceProvider)?.GetKeyedService(p.ParameterType, attr.Key) is { } s ? s : + p.HasDefaultValue ? p.DefaultValue : + throw new ArgumentException($"Unable to resolve argument for '{p.Name}'.", "arguments.Services"), + ExcludeFromSchema = true + }; + } + + return default; + }, + }; + [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] [JsonSerializable(typeof(string))] From 49af89ec4ea246bc7fa1e6386c76a3f69ec084e9 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 10 May 2025 02:42:50 -0400 Subject: [PATCH 086/472] Fix handling of tool calls with some OpenAI endpoints (#6405) * Fix handling of tool calls with some endpoints Most assistant messages containing tool calls don't contain text as well (though some can). In such a case, we were still creating the assistant with empty text. While OpenAI's service permits that, some other endpoints are more finicky about it. This avoids doing so. * Reduce to single iteration through assistant content --- .../OpenAIChatClient.cs | 119 +++++++++++------- .../ChatClientIntegrationTests.cs | 6 +- .../OpenAIResponseClientIntegrationTests.cs | 2 + 3 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index c644bd77f21..98cf49fd696 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -157,30 +157,54 @@ void IDisposable.Dispose() } else if (input.Role == ChatRole.Assistant) { - AssistantChatMessage message = new(ToOpenAIChatContent(input.Contents)) - { - ParticipantName = input.AuthorName - }; - + List? contentParts = null; + List? toolCalls = null; + string? refusal = null; foreach (var content in input.Contents) { switch (content) { - case ErrorContent errorContent when errorContent.ErrorCode is nameof(message.Refusal): - message.Refusal = errorContent.Message; + case ErrorContent ec when ec.ErrorCode == nameof(AssistantChatMessage.Refusal): + refusal = ec.Message; break; - case FunctionCallContent callRequest: - message.ToolCalls.Add( - ChatToolCall.CreateFunctionToolCall( - callRequest.CallId, - callRequest.Name, - new(JsonSerializer.SerializeToUtf8Bytes( - callRequest.Arguments, - options.GetTypeInfo(typeof(IDictionary)))))); + case FunctionCallContent fc: + (toolCalls ??= []).Add( + ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes( + fc.Arguments, options.GetTypeInfo(typeof(IDictionary)))))); break; + + default: + if (ToChatMessageContentPart(content) is { } part) + { + (contentParts ??= []).Add(part); + } + + break; + } + } + + AssistantChatMessage message; + if (contentParts is not null) + { + message = new(contentParts); + if (toolCalls is not null) + { + foreach (var toolCall in toolCalls) + { + message.ToolCalls.Add(toolCall); + } } } + else + { + message = toolCalls is not null ? + new(toolCalls) : + new(ChatMessageContentPart.CreateTextPart(string.Empty)); + } + + message.ParticipantName = input.AuthorName; + message.Refusal = refusal; yield return message; } @@ -191,38 +215,12 @@ void IDisposable.Dispose() private static List ToOpenAIChatContent(IList contents) { List parts = []; + foreach (var content in contents) { - switch (content) + if (ToChatMessageContentPart(content) is { } part) { - case TextContent textContent: - parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text)); - break; - - case UriContent uriContent when uriContent.HasTopLevelMediaType("image"): - parts.Add(ChatMessageContentPart.CreateImagePart(uriContent.Uri, GetImageDetail(content))); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): - parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, GetImageDetail(content))); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"): - var audioData = BinaryData.FromBytes(dataContent.Data); - if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase)) - { - parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Mp3)); - } - else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase)) - { - parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav)); - } - - break; - - case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): - parts.Add(ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf")); - break; + parts.Add(part); } } @@ -234,6 +232,39 @@ private static List ToOpenAIChatContent(IList return parts; } + private static ChatMessageContentPart? ToChatMessageContentPart(AIContent content) + { + switch (content) + { + case TextContent textContent: + return ChatMessageContentPart.CreateTextPart(textContent.Text); + + case UriContent uriContent when uriContent.HasTopLevelMediaType("image"): + return ChatMessageContentPart.CreateImagePart(uriContent.Uri, GetImageDetail(content)); + + case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): + return ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, GetImageDetail(content)); + + case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"): + var audioData = BinaryData.FromBytes(dataContent.Data); + if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase)) + { + return ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Mp3); + } + else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase)) + { + return ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav); + } + + break; + + case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): + return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"); + } + + return null; + } + private static ChatImageDetailLevel? GetImageDetail(AIContent content) { if (content.AdditionalProperties?.TryGetValue("detail", out object? value) is true) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index edb6c5dd14c..994fef47517 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -77,7 +77,9 @@ public virtual async Task GetResponseAsync_WithEmptyMessage() var response = await _chatClient.GetResponseAsync( [ + new(ChatRole.System, []), new(ChatRole.User, []), + new(ChatRole.Assistant, []), new(ChatRole.User, "What is 1 + 2? Reply with a single number."), ]); @@ -618,9 +620,11 @@ public virtual async Task Caching_AfterFunctionInvocation_FunctionOutputUnchange var secondResponse = await chatClient.GetResponseAsync([message]); Assert.Equal(response.Text, secondResponse.Text); Assert.Equal(2, functionCallCount); - Assert.Equal(2, llmCallCount!.CallCount); + Assert.Equal(FunctionInvokingChatClientSetsConversationId ? 3 : 2, llmCallCount!.CallCount); } + public virtual bool FunctionInvokingChatClientSetsConversationId => false; + [ConditionalFact] public virtual async Task Caching_AfterFunctionInvocation_FunctionOutputChangedAsync() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index bbfad1c571d..2c1d6cdc80e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -9,4 +9,6 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests IntegrationTestHelpers.GetOpenAIClient() ?.GetOpenAIResponseClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini") .AsIChatClient(); + + public override bool FunctionInvokingChatClientSetsConversationId => true; } From 90dd3fdbb6056d8ae177ab102b779e3922a88981 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sat, 10 May 2025 13:10:27 -0700 Subject: [PATCH 087/472] Delete Microsoft.Extensions.AI.Abstractions APIs marked [Obsolete] during preview (#6414) --- .../ChatCompletion/ChatOptions.cs | 10 --- .../ChatCompletion/ChatResponse.cs | 19 ------ .../ChatCompletion/ChatResponseUpdate.cs | 18 ------ .../EmbeddingGeneratorExtensions.cs | 62 ------------------- 4 files changed, 109 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index f2eeffe9dbf..6cf99b01821 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -11,16 +11,6 @@ namespace Microsoft.Extensions.AI; /// Provide options. public class ChatOptions { - /// Gets or sets an optional identifier used to associate a request with an existing conversation. - /// This property is obsolete. Use instead. - [System.Obsolete("Use ConversationId instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public string? ChatThreadId - { - get => ConversationId; - set => ConversationId = value; - } - /// Gets or sets an optional identifier used to associate a request with an existing conversation. /// Stateless vs. stateful clients. public string? ConversationId { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs index 5e0e80beac9..a342ef1e69e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs @@ -63,25 +63,6 @@ public IList Messages /// Gets or sets the ID of the chat response. public string? ResponseId { get; set; } - /// Gets or sets an identifier for the state of the conversation. - /// - /// Some implementations are capable of storing the state for a conversation, such that - /// the input messages supplied to need only be the additional messages beyond - /// what's already stored. If this property is non-, it represents an identifier for that state, - /// and it should be used in a subsequent instead of supplying the same messages - /// (and this 's message) as part of the messages parameter. Note that the value may - /// or may not differ on every response, depending on whether the underlying provider uses a fixed ID for each conversation - /// or updates it for each message. - /// - /// This method is obsolete. Use instead. - [Obsolete("Use ConversationId instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public string? ChatThreadId - { - get => ConversationId; - set => ConversationId = value; - } - /// Gets or sets an identifier for the state of the conversation. /// /// Some implementations are capable of storing the state for a conversation, such that diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index bdc584596b0..63dbfbc0d7d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -116,24 +116,6 @@ public IList Contents /// public string? MessageId { get; set; } - /// Gets or sets an identifier for the state of the conversation of which this update is a part. - /// - /// Some implementations are capable of storing the state for a conversation, such that - /// the input messages supplied to need only be the additional messages beyond - /// what's already stored. If this property is non-, it represents an identifier for that state, - /// and it should be used in a subsequent instead of supplying the same messages - /// (and this streaming message) as part of the messages parameter. Note that the value may or may not differ on every - /// response, depending on whether the underlying provider uses a fixed ID for each conversation or updates it for each message. - /// - /// This method is obsolete. Use instead. - [Obsolete("Use ConversationId instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public string? ChatThreadId - { - get => ConversationId; - set => ConversationId = value; - } - /// Gets or sets an identifier for the state of the conversation of which this update is a part. /// /// Some implementations are capable of storing the state for a conversation, such that diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs index 31f58772abf..895b7bf7ea7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs @@ -87,35 +87,6 @@ public static TService GetRequiredService( return service; } - /// Generates an embedding vector from the specified . - /// The type from which embeddings will be generated. - /// The numeric type of the embedding data. - /// The embedding generator. - /// A value from which an embedding will be generated. - /// The embedding generation options to configure the request. - /// The to monitor for cancellation requests. The default is . - /// The generated embedding for the specified . - /// is . - /// is . - /// The generator did not produce exactly one embedding. - /// - /// This operation is equivalent to using and returning the - /// resulting 's property. - /// - /// - /// This method is obsolete. Use instead. - /// - [Obsolete("Use GenerateVectorAsync instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public static async Task> GenerateEmbeddingVectorAsync( - this IEmbeddingGenerator> generator, - TInput value, - EmbeddingGenerationOptions? options = null, - CancellationToken cancellationToken = default) - { - return await GenerateVectorAsync(generator, value, options, cancellationToken).ConfigureAwait(false); - } - /// Generates an embedding vector from the specified . /// The type from which embeddings will be generated. /// The numeric type of the embedding data. @@ -141,39 +112,6 @@ public static async Task> GenerateVectorAsync< return embedding.Vector; } - /// Generates an embedding from the specified . - /// The type from which embeddings will be generated. - /// The type of embedding to generate. - /// The embedding generator. - /// A value from which an embedding will be generated. - /// The embedding generation options to configure the request. - /// The to monitor for cancellation requests. The default is . - /// - /// The generated embedding for the specified . - /// - /// is . - /// is . - /// The generator did not produce exactly one embedding. - /// - /// This operations is equivalent to using with a - /// collection composed of the single and then returning the first embedding element from the - /// resulting collection. - /// - /// - /// This method is obsolete. Use instead. - /// - [Obsolete("Use GenerateAsync instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public static async Task GenerateEmbeddingAsync( - this IEmbeddingGenerator generator, - TInput value, - EmbeddingGenerationOptions? options = null, - CancellationToken cancellationToken = default) - where TEmbedding : Embedding - { - return await GenerateAsync(generator, value, options, cancellationToken).ConfigureAwait(false); - } - /// Generates an embedding from the specified . /// The type from which embeddings will be generated. /// The type of embedding to generate. From 26e409696ff5699863bc69ce3b5fe0dfc43fe413 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sun, 11 May 2025 21:05:31 -0700 Subject: [PATCH 088/472] Bump versions of dependencies needed for the 9.4.3 template (#6356) (#6410) --- eng/Versions.props | 20 +++++++++---------- src/ProjectTemplates/GeneratedContent.targets | 3 ++- .../ChatWithCustomData-CSharp.Web/README.md | 10 ++++++++++ .../src/ChatWithCustomData/README.Aspire.md | 9 +++------ .../aichatweb/aichatweb.csproj | 2 +- .../aichatweb/README.md | 8 ++++++++ .../aichatweb.AppHost.csproj | 6 +++--- .../aichatweb.Web/aichatweb.Web.csproj | 6 +++--- .../aichatweb/aichatweb.csproj | 4 ++-- 9 files changed, 42 insertions(+), 26 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index c10be974073..409d8c0d9aa 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -154,21 +154,21 @@ 4.8.0 3.3.4 - 9.1.0 - 9.1.0-preview.1.25121.10 + 9.2.1 + 9.2.1-preview.1.25222.1 1.0.0-beta.6 2.2.0-beta.4 1.13.2 11.6.0 - 9.3.1-beta.260 - 9.3.1-beta.260 - 9.3.1-beta.260 - 9.3.1-beta.260 + 9.4.1-beta.277 + 9.4.1-beta.277 + 9.4.1-beta.277 + 9.4.1-beta.277 9.2.0 - 1.45.0-preview - 1.45.0-preview - 1.45.0 - 5.1.12 + 1.47.0-preview + 1.47.0-preview + 1.47.0 + 5.1.13 1.9.0 0.1.9 6.0.1 diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 4767af46a31..832d533e66a 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -21,10 +21,11 @@ false + false $(TemplatePinnedRepoPackagesVersion) - $(TemplatePinnedRepoAIPackagesVersion) + $(TemplatePinnedRepoAIPackagesVersion) $(TemplatePinnedMicrosoftEntityFrameworkCoreSqliteVersion) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md index 44decea800c..37f10b83ce2 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md @@ -9,6 +9,16 @@ This project is an AI chat application that demonstrates how to chat with custom ### Prerequisites To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). +#### ---#endif +#### ---#if (IsOllama) +### Known Issues + +#### Errors running Ollama or Docker + +A recent incompatibility was found between Ollama and Docker Desktop. This issue results in runtime errors when connecting to Ollama, and the workaround for that can lead to Docker not working for Aspire projects. + +This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See [ollama/ollama#9509](https://github.com/ollama/ollama/issues/9509#issuecomment-2842461831) for more information and a link to install the version of Docker Desktop with the fix. + #### ---#endif # Configure the AI Model Provider #### ---#if (IsGHModels) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index d1677b7ba78..f7c944dacc8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -10,17 +10,14 @@ This project is an AI chat application that demonstrates how to chat with custom To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). #### ---#endif -#### ---#if (UseQdrant) ### Known Issues -#### Errors After Updating to Aspire Version 9.2.0 -This project is not currently compatible with Aspire 9.2.0, and all Aspire package versions are set to 9.1.0. Updating [Aspire.Qdrant.Client](https://www.nuget.org/packages/Aspire.Qdrant.Client) to version 9.2.0 causes an incompatibility with [Microsoft.SemanticKernel.Connectors.Qdrant](https://www.nuget.org/packages/Microsoft.SemanticKernel.Connectors.Qdrant) where different versions of [Qdrant.Client](https://www.nuget.org/packages/Qdrant.Client) are required. Attempting to run the project with `Aspire.Qdrant.Client` version 9.2.0 will result in the following exception: +#### Errors running Ollama or Docker -> System.MissingMethodException: Method not found: 'Qdrant.Client.Grpc.Vectors Qdrant.Client.Grpc.ScoredPoint.get_Vectors()' +A recent incompatibility was found between Ollama and Docker Desktop. This issue results in runtime errors when connecting to Ollama, and the workaround for that can lead to Docker not working for Aspire projects. -Once a version of `Microsoft.SemanticKernel.Connectors.Qdrant` is published with a dependency on `Qdrant.Client` version `>= 1.13.0`, the Aspire packages can also be updated to version 9.2.0. +This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See [ollama/ollama#9509](https://github.com/ollama/ollama/issues/9509#issuecomment-2842461831) for more information and a link to install the version of Docker Desktop with the fix. -#### ---#endif # Configure the AI Model Provider #### ---#if (IsGHModels) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index a4aad331c0d..a6069df327b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md index 0a467b898bd..c05c18281ef 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md @@ -5,6 +5,14 @@ This project is an AI chat application that demonstrates how to chat with custom >[!NOTE] > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. +### Known Issues + +#### Errors running Ollama or Docker + +A recent incompatibility was found between Ollama and Docker Desktop. This issue results in runtime errors when connecting to Ollama, and the workaround for that can lead to Docker not working for Aspire projects. + +This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See [ollama/ollama#9509](https://github.com/ollama/ollama/issues/9509#issuecomment-2842461831) for more information and a link to install the version of Docker Desktop with the fix. + # Configure the AI Model Provider ## Using GitHub Models diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 09439522151..a74ef7b7f3b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,8 +12,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 1a2374ce7fe..70da05797de 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,11 +8,11 @@ - + - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 3c4139801a2..502d1f84c95 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -12,11 +12,11 @@ - + - + From 5c851a4118864e2d474a00cba2ab55197e65302f Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 12 May 2025 10:42:50 +0200 Subject: [PATCH 089/472] Fixes in Microsoft.Extensions.Telemetry README.md (#6406) Co-authored-by: evgenyfedorov2 --- .../Microsoft.Extensions.Telemetry/README.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/README.md b/src/Libraries/Microsoft.Extensions.Telemetry/README.md index 862a3fcc1b7..d9f34b42eab 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/README.md +++ b/src/Libraries/Microsoft.Extensions.Telemetry/README.md @@ -46,6 +46,17 @@ builder.Logging.AddRandomProbabilisticSampler(configuration.GetSection("Logging: The Random Probabilistic Sampler supports the `IOptionsMonitor` pattern, allowing for dynamic configuration updates. This means you can change the sampling rules at runtime without needing to restart your application. +#### Trace-Based Sampling + +Matches logging sampling decisions with the underlying [Distributed Tracing sampling decisions](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling): + +```csharp +// Add trace-based sampler +builder.Logging.AddTraceBasedSampler(); +``` + +This comes in handy when you already use OpenTelemetry .NET Tracing and would like to see sampling decisions being consistent across both logs and their underlying [`Activity`](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling). + ### Log Buffering Provides a buffering mechanism for logs, allowing you to store logs in temporary circular buffers in memory. If the buffer is full, the oldest logs will be dropped. If you want to emit the buffered logs, you can call `Flush()` on the buffer. That way, if you don't flush buffers, all buffered logs will eventually be dropped and that makes sense - if you don't flush buffers, chances are @@ -102,11 +113,12 @@ public class MyService The Global Log Buffer supports the `IOptionsMonitor` pattern, allowing for dynamic configuration updates. This means you can change the buffering rules at runtime without needing to restart your application. -##### Limitations +#### Limitations 1. This library does not preserve the order of log records. However, original timestamps are preserved. 1. The library does not support custom configuration per each logger provider. Same configuration is applied to all logger providers. -1. When buffering and then flushing buffers, not all information of the original log record is preserved. This is due to serialing/deserialing limitation, but can be +1. Log scopes are not supported. This means that if you use `ILogger.BeginScope()` method, the buffered log records will not be associated with the scope. +1. When buffering and then flushing buffers, not all information of the original log record is preserved. This is due to serializing/deserializing limitation, but can be revisited in future. Namely, this library uses `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord` class when converting buffered log records to actual log records, but omits following properties: - `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.ActivitySpanId` @@ -114,17 +126,6 @@ revisited in future. Namely, this library uses `Microsoft.Extensions.Logging.Abs - `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.ManagedThreadId` - `Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord.MessageTemplate` -#### Trace-Based Sampling - -Matches logging sampling decisions with the underlying [Distributed Tracing sampling decisions](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling): - -```csharp -// Add trace-based sampler -builder.Logging.AddTraceBasedSampler(); -``` - -This comes in handy when you already use OpenTelemetry .NET Tracing and would like to see sampling decisions being consistent across both logs and their underlying [`Activity`](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing-concepts#sampling). - ### Service Log Enrichment Enriches logs with application-specific information based on `ApplicationMetadata` information. The bellow calls will add the service log enricher to the service collection. From ed01fe1fc067c78747cb0d4968c2411e0bf2f170 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 12 May 2025 06:24:48 -0400 Subject: [PATCH 090/472] Add missing [DebuggerDisplay] on AIFunctionArguments (#6422) --- .../Functions/AIFunctionArguments.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs index 8fa34e52c08..3238b88e532 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; #pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter #pragma warning disable SA1112 // Closing parenthesis should be on line of opening parenthesis @@ -24,6 +25,7 @@ namespace Microsoft.Extensions.AI; /// an if it needs to resolve any services from a dependency injection /// container. /// +[DebuggerDisplay("Count = {Count}")] public class AIFunctionArguments : IDictionary, IReadOnlyDictionary { /// The nominal arguments. From e6a03e27db7e72a6ee83c04458ae489ce25e7822 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 12 May 2025 07:05:11 -0400 Subject: [PATCH 091/472] Add WriteAsync overrides to stream helper in AIFunctionFactory (#6419) We use JsonSerializer.SerializeAsync but were missing the async overrides. As with MemoryStream, these don't need to queue. --- .../Functions/AIFunctionFactory.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 3f090a2ac3b..6534e041a7c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -26,6 +26,7 @@ #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable S2333 // Redundant modifiers should not be used #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable SA1202 // Public members should come before private members namespace Microsoft.Extensions.AI; @@ -1105,6 +1106,34 @@ public override void Flush() { } + public override Task FlushAsync(CancellationToken cancellationToken) => + Task.CompletedTask; + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + +#if NET + public override +#else + private +#endif + ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + EnsureNotDisposed(); + + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + EnsureCapacity(_position + buffer.Length); + + buffer.Span.CopyTo(_buffer.AsSpan(_position)); + _position += buffer.Length; + + return default; + } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); From 0313177a0f7deef3d0e13f25ff90c9e78c017878 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 12 May 2025 07:16:58 -0400 Subject: [PATCH 092/472] Update CHANGELOGs for M.E.AI (#6416) * Update CHANGELOGs for M.E.AI --- .../Microsoft.Extensions.AI.Abstractions/CHANGELOG.md | 9 +++++++++ .../CHANGELOG.md | 5 +++++ .../Microsoft.Extensions.AI.Ollama/CHANGELOG.md | 4 ++++ .../Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 6 ++++++ src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index c6639273d70..b4fe9d69a66 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,5 +1,14 @@ # Release History +## 9.4.4-preview.1.25259.16 + +- Added `AIJsonUtilities.TransformSchema` and supporting types. +- Added `BinaryEmbedding` for bit embeddings. +- Added `ChatOptions.RawRepresentationFactory` to make it easier to pass options to the underlying service. +- Added `Base64Data` property to `DataContent`. +- Moved `AIFunctionFactory` to `Microsoft.Extensions.AI.Abstractions`. +- Fixed `AIFunctionFactory` handling of default struct arguments. + ## 9.4.3-preview.1.25230.7 - Renamed `ChatThreadId` to `ConversationId` on `ChatResponse`, `ChatResponseUpdate`, and `ChatOptions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index aaf1ac1c67c..aeb023efae5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 9.4.4-preview.1.25259.16 + +- Added an `AsIEmbeddingGenerator` extension method for `ImageEmbeddingsClient`. +- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. + ## 9.4.3-preview.1.25230.7 - Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md index 8822f8ddaea..e90fed2cdba 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 9.4.4-preview.1.25259.16 + +- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. + ## 9.4.3-preview.1.25230.7 - Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 05130ba3847..ad915d06aa7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 9.4.4-preview.1.25259.16 + +- Made `IChatClient` implementation more resilient with non-OpenAI services. +- Added `ErrorContent` to represent refusals. +- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. + ## 9.4.3-preview.1.25230.7 - Reverted previous change that enabled `strict` schemas by default. diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 69cf9f12c46..25c15aed0d2 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 9.4.4-preview.1.25259.16 + +- Fixed `CachingChatClient` to avoid caching when `ConversationId` is set. +- Renamed `useJsonSchema` parameter in `GetResponseAsync` to `useJsonSchemaResponseFormat`. +- Updated `OpenTelemetryChatClient` and `OpenTelemetryEmbeddingGenerator` to conform to the latest 1.33.0 draft specification of the Semantic Conventions for Generative AI systems. + ## 9.4.3-preview.1.25230.7 - Updated the diagnostic spans emitted by `FunctionInvokingChatClient` to include total input and output token counts. From 654ba11ec3ef5656a4988e3023ce2b1555203ce5 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 12 May 2025 10:55:24 -0400 Subject: [PATCH 093/472] Replace Type targetType AIFunctionFactory.Create parameter with a func (#6424) --- .../Functions/AIFunctionFactory.cs | 56 +++++++----------- .../Functions/AIFunctionFactoryOptions.cs | 18 ------ .../Functions/AIFunctionFactoryTest.cs | 57 ++++++------------- 3 files changed, 37 insertions(+), 94 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 6534e041a7c..d5274186645 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; #if !NET using System.Linq; @@ -374,16 +373,15 @@ public static AIFunction Create(MethodInfo method, object? target, string? name } /// - /// Creates an instance for a method, specified via an for - /// and instance method, along with a representing the type of the target object to - /// instantiate each time the method is invoked. + /// Creates an instance for a method, specified via a for + /// an instance method and a for constructing an instance of + /// the receiver object each time the is invoked. /// /// The instance method to be represented via the created . - /// - /// The to construct an instance of on which to invoke when - /// the resulting is invoked. is used, - /// utilizing the type's public parameterless constructor. If an instance can't be constructed, an exception is - /// thrown during the function's invocation. + /// + /// Callback used on each function invocation to create an instance of the type on which the instance method + /// will be invoked. If the returned instance is or , it will be disposed of + /// after completes its invocation. /// /// Metadata to use to override defaults inferred from . /// The created for invoking . @@ -457,22 +455,16 @@ public static AIFunction Create(MethodInfo method, object? target, string? name /// /// /// is . - /// is . + /// is . /// represents a static method. /// represents an open generic method. /// contains a parameter without a parameter name. - /// is not assignable to 's declaring type. /// A parameter to or its return type is not serializable. public static AIFunction Create( MethodInfo method, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - AIFunctionFactoryOptions? options = null) - { - _ = Throw.IfNull(method); - _ = Throw.IfNull(targetType); - - return ReflectionAIFunction.Build(method, targetType, options ?? _defaultOptions); - } + Func createInstanceFunc, + AIFunctionFactoryOptions? options = null) => + ReflectionAIFunction.Build(method, createInstanceFunc, options ?? _defaultOptions); private sealed class ReflectionAIFunction : AIFunction { @@ -503,10 +495,11 @@ public static ReflectionAIFunction Build(MethodInfo method, object? target, AIFu public static ReflectionAIFunction Build( MethodInfo method, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, + Func createInstanceFunc, AIFunctionFactoryOptions options) { _ = Throw.IfNull(method); + _ = Throw.IfNull(createInstanceFunc); if (method.ContainsGenericParameters) { @@ -518,13 +511,7 @@ public static ReflectionAIFunction Build( Throw.ArgumentException(nameof(method), "The method must be an instance method."); } - if (method.DeclaringType is { } declaringType && - !declaringType.IsAssignableFrom(targetType)) - { - Throw.ArgumentException(nameof(targetType), "The target type must be assignable to the method's declaring type."); - } - - return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), targetType, options); + return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), createInstanceFunc, options); } private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor, object? target, AIFunctionFactoryOptions options) @@ -536,20 +523,17 @@ private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor, private ReflectionAIFunction( ReflectionAIFunctionDescriptor functionDescriptor, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, + Func createInstanceFunc, AIFunctionFactoryOptions options) { FunctionDescriptor = functionDescriptor; - TargetType = targetType; - CreateInstance = options.CreateInstance; + CreateInstanceFunc = createInstanceFunc; AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary.Instance; } public ReflectionAIFunctionDescriptor FunctionDescriptor { get; } public object? Target { get; } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - public Type? TargetType { get; } - public Func? CreateInstance { get; } + public Func? CreateInstanceFunc { get; } public override IReadOnlyDictionary AdditionalProperties { get; } public override string Name => FunctionDescriptor.Name; @@ -566,14 +550,12 @@ private ReflectionAIFunction( object? target = Target; try { - if (TargetType is { } targetType) + if (CreateInstanceFunc is { } func) { Debug.Assert(target is null, "Expected target to be null when we have a non-null target type"); Debug.Assert(!FunctionDescriptor.Method.IsStatic, "Expected an instance method"); - target = CreateInstance is not null ? - CreateInstance(targetType, arguments) : - Activator.CreateInstance(targetType); + target = func(arguments); if (target is null) { Throw.InvalidOperationException("Unable to create an instance of the target type."); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index 80ff394359d..e71a4687422 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -106,24 +106,6 @@ public AIFunctionFactoryOptions() /// public Func>? MarshalResult { get; set; } - /// - /// Gets or sets a delegate used with to create the receiver instance. - /// - /// - /// - /// creates instances that invoke an - /// instance method on the specified . This delegate is used to create the instance of the type that will be used to invoke the method. - /// By default if is , is used. If - /// is non-, the delegate is invoked with the to be instantiated and the - /// provided to the method. - /// - /// - /// Each created instance will be used for a single invocation. If the object is or , it will - /// be disposed of after the invocation completes. - /// - /// - public Func? CreateInstance { get; set; } - /// Provides configuration options produced by the delegate. public readonly record struct ParameterBindingOptions { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 4f5037fc92d..6d448efb710 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -29,7 +29,8 @@ public void InvalidArguments_Throw() Assert.Throws("method", () => AIFunctionFactory.Create(method: null!, target: new object())); Assert.Throws("method", () => AIFunctionFactory.Create(method: null!, target: new object(), name: "myAiFunk")); Assert.Throws("target", () => AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (object?)null)); - Assert.Throws("targetType", () => AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (Type)null!)); + Assert.Throws("createInstanceFunc", () => + AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (Func)null!)); Assert.Throws("method", () => AIFunctionFactory.Create(typeof(List<>).GetMethod("Add")!, new List())); } @@ -312,16 +313,12 @@ public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable( AIFunction func = AIFunctionFactory.Create( typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!, - typeof(MyFunctionTypeWithOneArg), - new() + static arguments => { - CreateInstance = (type, arguments) => - { - Assert.NotNull(arguments.Services); - return ActivatorUtilities.CreateInstance(arguments.Services, type); - }, - MarshalResult = (result, type, cancellationToken) => new ValueTask(result), - }); + Assert.NotNull(arguments.Services); + return ActivatorUtilities.CreateInstance(arguments.Services, typeof(MyFunctionTypeWithOneArg)); + }, + new() { MarshalResult = (result, type, cancellationToken) => new ValueTask(result) }); Assert.NotNull(func); var result = (Tuple?)await func.InvokeAsync(new() { Services = sp }); @@ -330,31 +327,25 @@ public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable( } [Fact] - public async Task Create_NoInstance_UsesActivatorWhenServicesUnavailable() + public async Task Create_CreateInstanceReturnsNull_ThrowsDuringInvocation() { AIFunction func = AIFunctionFactory.Create( - typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.InstanceMethod))!, - typeof(MyFunctionTypeWithNoArgs), - new() - { - MarshalResult = (result, type, cancellationToken) => new ValueTask(result), - }); + typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!, + static _ => null!); Assert.NotNull(func); - Assert.Equal("42", await func.InvokeAsync()); + await Assert.ThrowsAsync(async () => await func.InvokeAsync()); } [Fact] - public async Task Create_NoInstance_ThrowsWhenCantConstructInstance() + public async Task Create_WrongConstructedType_ThrowsDuringInvocation() { - var sp = new ServiceCollection().BuildServiceProvider(); - AIFunction func = AIFunctionFactory.Create( typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!, - typeof(MyFunctionTypeWithOneArg)); + static _ => new MyFunctionTypeWithNoArgs()); Assert.NotNull(func); - await Assert.ThrowsAsync(async () => await func.InvokeAsync(new() { Services = sp })); + await Assert.ThrowsAsync(async () => await func.InvokeAsync()); } [Fact] @@ -362,15 +353,7 @@ public void Create_NoInstance_ThrowsForStaticMethod() { Assert.Throws("method", () => AIFunctionFactory.Create( typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.StaticMethod))!, - typeof(MyFunctionTypeWithNoArgs))); - } - - [Fact] - public void Create_NoInstance_ThrowsForMismatchedMethod() - { - Assert.Throws("targetType", () => AIFunctionFactory.Create( - typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.InstanceMethod))!, - typeof(MyFunctionTypeWithOneArg))); + static _ => new MyFunctionTypeWithNoArgs())); } [Fact] @@ -378,7 +361,7 @@ public async Task Create_NoInstance_DisposableInstanceCreatedDisposedEachInvocat { AIFunction func = AIFunctionFactory.Create( typeof(DisposableService).GetMethod(nameof(DisposableService.GetThis))!, - typeof(DisposableService), + static _ => new DisposableService(), new() { MarshalResult = (result, type, cancellationToken) => new ValueTask(result), @@ -397,7 +380,7 @@ public async Task Create_NoInstance_AsyncDisposableInstanceCreatedDisposedEachIn { AIFunction func = AIFunctionFactory.Create( typeof(AsyncDisposableService).GetMethod(nameof(AsyncDisposableService.GetThis))!, - typeof(AsyncDisposableService), + static _ => new AsyncDisposableService(), new() { MarshalResult = (result, type, cancellationToken) => new ValueTask(result), @@ -416,7 +399,7 @@ public async Task Create_NoInstance_DisposableAndAsyncDisposableInstanceCreatedD { AIFunction func = AIFunctionFactory.Create( typeof(DisposableAndAsyncDisposableService).GetMethod(nameof(DisposableAndAsyncDisposableService.GetThis))!, - typeof(DisposableAndAsyncDisposableService), + static _ => new DisposableAndAsyncDisposableService(), new() { MarshalResult = (result, type, cancellationToken) => new ValueTask(result), @@ -821,11 +804,7 @@ public ValueTask DisposeAsync() private sealed class MyFunctionTypeWithNoArgs { - private string _value = "42"; - public static void StaticMethod() => throw new NotSupportedException(); - - public string InstanceMethod() => _value; } private sealed class MyFunctionTypeWithOneArg(MyArgumentType arg) From 3c82ac5abe06b1577efe7999bd39f816b8b5d24a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 12 May 2025 12:02:17 -0400 Subject: [PATCH 094/472] Remove debug-level logging of updates in LoggingChatClient (#6425) --- .../ChatCompletion/LoggingChatClient.cs | 14 ++------------ .../ChatCompletion/LoggingChatClientTests.cs | 2 -- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs index 3937d5db59b..aec72eddcdc 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/LoggingChatClient.cs @@ -153,16 +153,9 @@ public override async IAsyncEnumerable GetStreamingResponseA throw; } - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Trace)) { - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogStreamingUpdateSensitive(AsJson(update)); - } - else - { - LogStreamingUpdate(); - } + LogStreamingUpdateSensitive(AsJson(update)); } yield return update; @@ -190,9 +183,6 @@ public override async IAsyncEnumerable GetStreamingResponseA [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {ChatResponse}.")] private partial void LogCompletedSensitive(string methodName, string chatResponse); - [LoggerMessage(LogLevel.Debug, "GetStreamingResponseAsync received update.")] - private partial void LogStreamingUpdate(); - [LoggerMessage(LogLevel.Trace, "GetStreamingResponseAsync received update: {ChatResponseUpdate}")] private partial void LogStreamingUpdateSensitive(string chatResponseUpdate); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs index 51638d1a252..cd383381c06 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/LoggingChatClientTests.cs @@ -134,8 +134,6 @@ static async IAsyncEnumerable GetUpdatesAsync() { Assert.Collection(logs, entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync invoked.") && !entry.Message.Contains("biggest animal")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("blue")), - entry => Assert.True(entry.Message.Contains("GetStreamingResponseAsync received update.") && !entry.Message.Contains("whale")), entry => Assert.Contains("GetStreamingResponseAsync completed.", entry.Message)); } else From 397705a3c211d65ed67c269c7bb8e818cc0057af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Mon, 12 May 2025 10:32:23 -0700 Subject: [PATCH 095/472] Remove culture code from Microsoft documentation urls (#9253) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 1073af1cc19..71547ea9396 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -8,9 +8,9 @@ In typical systems, service configuration changes over time. Service discovery a Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. From 08fbb6752b8be17497fd0e68ae3267bb3f522f8f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 12 May 2025 23:14:11 +0300 Subject: [PATCH 096/472] Add an AIJsonSchemaTransformOptions property inside AIJsonSchemaCreateOptions and mark redundant properties in the latter as obsolete. (#6427) * Add an AIJsonSchemaTransformOptions property inside AIJsonSchemaCreateOptions and mark redundant properties in the latter as obsolete. * s/inferred/created --- .../Utilities/AIJsonSchemaCreateOptions.cs | 17 ++- .../Utilities/AIJsonSchemaTransformOptions.cs | 5 + .../Utilities/AIJsonUtilities.Defaults.cs | 7 ++ .../AIJsonUtilities.Schema.Create.cs | 107 +++++------------- .../AIJsonUtilities.Schema.Transform.cs | 21 +++- .../AzureAIInferenceChatClient.cs | 3 +- .../OpenAIChatClient.cs | 3 +- .../ChatClientStructuredOutputExtensions.cs | 9 +- .../Utilities/AIJsonUtilitiesTests.cs | 77 ++++++++++--- ...atClientStructuredOutputExtensionsTests.cs | 34 +++++- 10 files changed, 175 insertions(+), 108 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs index 8e3269f7a9c..8c53938f481 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs @@ -38,22 +38,33 @@ public sealed record class AIJsonSchemaCreateOptions public Func? IncludeParameter { get; init; } /// - /// Gets a value indicating whether to include the type keyword in inferred schemas for .NET enums. + /// Gets a governing transformations on the JSON schema after it has been generated. /// + public AIJsonSchemaTransformOptions? TransformOptions { get; init; } + + /// + /// Gets a value indicating whether to include the type keyword in created schemas for .NET enums. + /// + [Obsolete("This property has been deprecated.")] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public bool IncludeTypeInEnumSchemas { get; init; } = true; /// /// Gets a value indicating whether to generate schemas with the additionalProperties set to false for .NET objects. /// - public bool DisallowAdditionalProperties { get; init; } = true; + [Obsolete("This property has been deprecated. Use the equivalent property in TransformOptions instead.")] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public bool DisallowAdditionalProperties { get; init; } /// - /// Gets a value indicating whether to include the $schema keyword in inferred schemas. + /// Gets a value indicating whether to include the $schema keyword in created schemas. /// public bool IncludeSchemaKeyword { get; init; } /// /// Gets a value indicating whether to mark all properties as required in the schema. /// + [Obsolete("This property has been deprecated. Use the equivalent property in TransformOptions instead.")] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public bool RequireAllProperties { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs index c7a035cbbed..46e7476afcf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs @@ -38,6 +38,11 @@ public sealed record class AIJsonSchemaTransformOptions /// public bool UseNullableKeyword { get; init; } + /// + /// Gets a value indicating whether to move the default keyword to the description field in the schema. + /// + public bool MoveDefaultKeywordToDescription { get; init; } + /// /// Gets the default options instance. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 8ab0152c941..33531661813 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -116,4 +116,11 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(AIFunctionArguments))] [EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead. private sealed partial class JsonContext : JsonSerializerContext; + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false)] + [JsonSerializable(typeof(JsonNode))] + private sealed partial class JsonContextNoIndentation : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index fe17a2ad449..a44836d8e96 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; @@ -40,7 +39,7 @@ public static partial class AIJsonUtilities private const string DefaultPropertyName = "default"; private const string RefPropertyName = "$ref"; - /// The uri used when populating the $schema keyword in inferred schemas. + /// The uri used when populating the $schema keyword in created schemas. private const string SchemaKeywordUri = "https://json-schema.org/draft/2020-12/schema"; // List of keywords used by JsonSchemaExporter but explicitly disallowed by some AI vendors. @@ -54,7 +53,7 @@ public static partial class AIJsonUtilities /// The title keyword used by the method schema. /// The description keyword used by the method schema. /// The options used to extract the schema from the specified type. - /// The options controlling schema inference. + /// The options controlling schema creation. /// A JSON schema document encoded as a . /// is . public static JsonElement CreateFunctionJsonSchema( @@ -106,13 +105,13 @@ public static JsonElement CreateFunctionJsonSchema( inferenceOptions); parameterSchemas.Add(parameter.Name, parameterSchema); - if (!parameter.IsOptional || inferenceOptions.RequireAllProperties) + if (!parameter.IsOptional) { (requiredProperties ??= []).Add((JsonNode)parameter.Name); } } - JsonObject schema = new(); + JsonNode schema = new JsonObject(); if (inferenceOptions.IncludeSchemaKeyword) { schema[SchemaPropertyName] = SchemaKeywordUri; @@ -136,7 +135,13 @@ public static JsonElement CreateFunctionJsonSchema( schema[RequiredPropertyName] = requiredProperties; } - return JsonSerializer.SerializeToElement(schema, JsonContext.Default.JsonNode); + // Finally, apply any schema transformations if specified. + if (inferenceOptions.TransformOptions is { } options) + { + schema = TransformSchema(schema, options); + } + + return JsonSerializer.SerializeToElement(schema, JsonContextNoIndentation.Default.JsonNode); } /// Creates a JSON schema for the specified type. @@ -145,7 +150,7 @@ public static JsonElement CreateFunctionJsonSchema( /// if the parameter is optional; otherwise, . /// The default value of the optional parameter, if applicable. /// The options used to extract the schema from the specified type. - /// The options controlling schema inference. + /// The options controlling schema creation. /// A representing the schema. public static JsonElement CreateJsonSchema( Type? type, @@ -158,7 +163,14 @@ public static JsonElement CreateJsonSchema( serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; JsonNode schema = CreateJsonSchemaCore(type, parameterName: null, description, hasDefaultValue, defaultValue, serializerOptions, inferenceOptions); - return JsonSerializer.SerializeToElement(schema, JsonContext.Default.JsonNode); + + // Finally, apply any schema transformations if specified. + if (inferenceOptions.TransformOptions is { } options) + { + schema = TransformSchema(schema, options); + } + + return JsonSerializer.SerializeToElement(schema, JsonContextNoIndentation.Default.JsonNode); } /// Gets the default JSON schema to be used by types or functions. @@ -203,25 +215,11 @@ private static JsonNode CreateJsonSchemaCore( if (hasDefaultValue) { - if (inferenceOptions.RequireAllProperties) - { - // Default values are only used in the context of optional parameters. - // Do not include a default keyword (since certain AI vendors don't support it) - // and instead embed its JSON in the description as a hint to the LLM. - string defaultValueJson = defaultValue is not null - ? JsonSerializer.Serialize(defaultValue, serializerOptions.GetTypeInfo(defaultValue.GetType())) - : "null"; - - description = CreateDescriptionWithDefaultValue(description, defaultValueJson); - } - else - { - JsonNode? defaultValueNode = defaultValue is not null - ? JsonSerializer.SerializeToNode(defaultValue, serializerOptions.GetTypeInfo(defaultValue.GetType())) - : null; + JsonNode? defaultValueNode = defaultValue is not null + ? JsonSerializer.SerializeToNode(defaultValue, serializerOptions.GetTypeInfo(defaultValue.GetType())) + : null; - (schemaObj ??= [])[DefaultPropertyName] = defaultValueNode; - } + (schemaObj ??= [])[DefaultPropertyName] = defaultValueNode; } if (description is not null) @@ -271,41 +269,11 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js } // Include the type keyword in enum types - if (inferenceOptions.IncludeTypeInEnumSchemas && ctx.TypeInfo.Type.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName)) + if (ctx.TypeInfo.Type.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName)) { objSchema.InsertAtStart(TypePropertyName, "string"); } - // Disallow additional properties in object schemas - if (inferenceOptions.DisallowAdditionalProperties && - objSchema.ContainsKey(PropertiesPropertyName) && - !objSchema.ContainsKey(AdditionalPropertiesPropertyName)) - { - objSchema.Add(AdditionalPropertiesPropertyName, (JsonNode)false); - } - - // Mark all properties as required - if (inferenceOptions.RequireAllProperties && - objSchema.TryGetPropertyValue(PropertiesPropertyName, out JsonNode? properties) && - properties is JsonObject propertiesObj) - { - _ = objSchema.TryGetPropertyValue(RequiredPropertyName, out JsonNode? required); - if (required is not JsonArray { } requiredArray || requiredArray.Count != propertiesObj.Count) - { - requiredArray = [.. propertiesObj.Select(prop => (JsonNode)prop.Key)]; - objSchema[RequiredPropertyName] = requiredArray; - } - } - - // Strip default keywords and embed in description where required - if (inferenceOptions.RequireAllProperties && - objSchema.TryGetPropertyValue(DefaultPropertyName, out JsonNode? defaultValue)) - { - _ = objSchema.Remove(DefaultPropertyName); - string defaultValueJson = defaultValue?.ToJsonString() ?? "null"; - localDescription = CreateDescriptionWithDefaultValue(localDescription, defaultValueJson); - } - // Filter potentially disallowed keywords. foreach (string keyword in _schemaKeywordsDisallowedByAIVendors) { @@ -328,20 +296,8 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js if (ctx.Path.IsEmpty && hasDefaultValue) { - // Add root-level default value metadata - if (inferenceOptions.RequireAllProperties) - { - // Default values are only used in the context of optional parameters. - // Do not include a default keyword (since certain AI vendors don't support it) - // and instead embed its JSON in the description as a hint to the LLM. - string defaultValueJson = JsonSerializer.Serialize(defaultValue, ctx.TypeInfo); - localDescription = CreateDescriptionWithDefaultValue(localDescription, defaultValueJson); - } - else - { - JsonNode? defaultValueNode = JsonSerializer.SerializeToNode(defaultValue, ctx.TypeInfo); - ConvertSchemaToObject(ref schema)[DefaultPropertyName] = defaultValueNode; - } + JsonNode? defaultValueNode = JsonSerializer.SerializeToNode(defaultValue, ctx.TypeInfo); + ConvertSchemaToObject(ref schema)[DefaultPropertyName] = defaultValueNode; } if (localDescription is not null) @@ -423,7 +379,7 @@ private static void InsertAtStart(this JsonObject jsonObject, string key, JsonNo jsonObject.Insert(0, key, value); #else jsonObject.Remove(key); - var copiedEntries = jsonObject.ToArray(); + var copiedEntries = System.Linq.Enumerable.ToArray(jsonObject); jsonObject.Clear(); jsonObject.Add(key, value); @@ -434,13 +390,6 @@ private static void InsertAtStart(this JsonObject jsonObject, string key, JsonNo #endif } - private static string CreateDescriptionWithDefaultValue(string? existingDescription, string defaultValueJson) - { - return existingDescription is null - ? $"Default value: {defaultValueJson}" - : $"{existingDescription} (Default value: {defaultValueJson})"; - } - private static JsonElement ParseJsonElement(ReadOnlySpan utf8Json) { Utf8JsonReader reader = new(utf8Json); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs index 5669a3fb264..865b4543abb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs @@ -30,9 +30,14 @@ public static JsonElement TransformSchema(JsonElement schema, AIJsonSchemaTransf } JsonNode? nodeSchema = JsonSerializer.SerializeToNode(schema, JsonContext.Default.JsonElement); + JsonNode transformedSchema = TransformSchema(nodeSchema, transformOptions); + return JsonSerializer.SerializeToElement(transformedSchema, JsonContextNoIndentation.Default.JsonNode); + } + + private static JsonNode TransformSchema(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions) + { List? path = transformOptions.TransformSchemaNode is not null ? [] : null; - JsonNode transformedSchema = TransformSchemaCore(nodeSchema, transformOptions, path); - return JsonSerializer.Deserialize(transformedSchema, JsonContext.Default.JsonElement); + return TransformSchemaCore(schema, transformOptions, path); } private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions, List? path) @@ -169,6 +174,18 @@ private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransf } } + if (transformOptions.MoveDefaultKeywordToDescription && + schemaObj.TryGetPropertyValue(DefaultPropertyName, out JsonNode? defaultSchema)) + { + string? description = schemaObj.TryGetPropertyValue(DescriptionPropertyName, out JsonNode? descriptionSchema) ? descriptionSchema?.GetValue() : null; + string defaultValueJson = JsonSerializer.Serialize(defaultSchema, JsonContextNoIndentation.Default.JsonNode!); + description = description is null + ? $"Default value: {defaultValueJson}" + : $"{description} (Default value: {defaultValueJson})"; + schemaObj[DescriptionPropertyName] = description; + _ = schemaObj.Remove(DefaultPropertyName); + } + break; default: diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index ff62845eb0e..6c0acb8ee23 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -29,7 +29,8 @@ internal sealed class AzureAIInferenceChatClient : IChatClient { RequireAllProperties = true, DisallowAdditionalProperties = true, - ConvertBooleanSchemas = true + ConvertBooleanSchemas = true, + MoveDefaultKeywordToDescription = true, }); /// Metadata about the client. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 98cf49fd696..001f4d1a593 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -30,7 +30,8 @@ internal sealed partial class OpenAIChatClient : IChatClient { RequireAllProperties = true, DisallowAdditionalProperties = true, - ConvertBooleanSchemas = true + ConvertBooleanSchemas = true, + MoveDefaultKeywordToDescription = true, }); /// Gets the default OpenAI endpoint. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs index e35f8b87949..69c4cc7ee89 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs @@ -26,9 +26,12 @@ public static partial class ChatClientStructuredOutputExtensions private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new() { IncludeSchemaKeyword = true, - DisallowAdditionalProperties = true, - IncludeTypeInEnumSchemas = true, - RequireAllProperties = true, + TransformOptions = new AIJsonSchemaTransformOptions + { + DisallowAdditionalProperties = true, + RequireAllProperties = true, + MoveDefaultKeywordToDescription = true, + }, }; /// Sends chat messages, requesting a response matching the type . 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 2d1967b11c1..0001b8b2125 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -15,6 +15,8 @@ using Microsoft.Extensions.AI.JsonSchemaExporter; using Xunit; +#pragma warning disable 0618 // Suppress obsolete warnings + namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilitiesTests @@ -72,10 +74,11 @@ public static void AIJsonSchemaCreateOptions_DefaultInstance_ReturnsExpectedValu { AIJsonSchemaCreateOptions options = useSingleton ? AIJsonSchemaCreateOptions.Default : new AIJsonSchemaCreateOptions(); Assert.True(options.IncludeTypeInEnumSchemas); - Assert.True(options.DisallowAdditionalProperties); + Assert.False(options.DisallowAdditionalProperties); Assert.False(options.IncludeSchemaKeyword); Assert.False(options.RequireAllProperties); Assert.Null(options.TransformSchemaNode); + Assert.Null(options.TransformOptions); } [Fact] @@ -106,6 +109,12 @@ public static void AIJsonSchemaCreateOptions_UsesStructuralEquality() property.SetValue(options2, includeParameter); break; + case null when property.PropertyType == typeof(AIJsonSchemaTransformOptions): + AIJsonSchemaTransformOptions transformOptions = new AIJsonSchemaTransformOptions { RequireAllProperties = true }; + property.SetValue(options1, transformOptions); + property.SetValue(options2, transformOptions); + break; + default: Assert.Fail($"Unexpected property type: {property.PropertyType}"); break; @@ -152,8 +161,7 @@ public static void CreateJsonSchema_DefaultParameters_GeneratesExpectedJsonSchem "default": "defaultValue" } }, - "required": ["Key", "EnumValue"], - "additionalProperties": false + "required": ["Key", "EnumValue"] } """).RootElement; @@ -176,6 +184,7 @@ public static void CreateJsonSchema_OverriddenParameters_GeneratesExpectedJsonSc "type": "integer" }, "EnumValue": { + "type": "string", "enum": ["A", "B"] }, "Value": { @@ -183,16 +192,20 @@ public static void CreateJsonSchema_OverriddenParameters_GeneratesExpectedJsonSc "type": ["string", "null"] } }, - "required": ["Key", "EnumValue", "Value"] + "required": ["Key", "EnumValue", "Value"], + "additionalProperties": false } """).RootElement; AIJsonSchemaCreateOptions inferenceOptions = new AIJsonSchemaCreateOptions { - IncludeTypeInEnumSchemas = false, - DisallowAdditionalProperties = false, IncludeSchemaKeyword = true, - RequireAllProperties = true, + TransformOptions = new() + { + DisallowAdditionalProperties = true, + RequireAllProperties = true, + MoveDefaultKeywordToDescription = true, + } }; JsonElement actual = AIJsonUtilities.CreateJsonSchema( @@ -227,8 +240,7 @@ public static void CreateJsonSchema_UserDefinedTransformer() "default": "defaultValue" } }, - "required": ["Key", "EnumValue"], - "additionalProperties": false + "required": ["Key", "EnumValue"] } """).RootElement; @@ -268,8 +280,7 @@ public static void CreateJsonSchema_FiltersDisallowedKeywords() "Char" : { "type": "string" } - }, - "additionalProperties": false + } } """).RootElement; @@ -341,6 +352,15 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr } """).RootElement; + AIJsonSchemaCreateOptions inferenceOptions = new() + { + TransformOptions = new() + { + RequireAllProperties = requireAllProperties, + MoveDefaultKeywordToDescription = requireAllProperties, + } + }; + AIFunction func = AIFunctionFactory.Create(( [Description("The city to get the weather for")] string city, [Description("The unit to calculate the current temperature to")] string unit = "celsius") => "sunny", @@ -348,7 +368,7 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr { Name = "get_weather", Description = "Gets the current weather for a current location", - JsonSchemaCreateOptions = new AIJsonSchemaCreateOptions { RequireAllProperties = requireAllProperties } + JsonSchemaCreateOptions = inferenceOptions }); Assert.NotNull(func.UnderlyingMethod); @@ -358,7 +378,7 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr func.UnderlyingMethod, title: func.Name, description: func.Description, - inferenceOptions: new AIJsonSchemaCreateOptions { RequireAllProperties = requireAllProperties }); + inferenceOptions: inferenceOptions); AssertDeepEquals(expected, resolvedSchema); } @@ -423,7 +443,7 @@ public static void CreateJsonSchema_ValidateWithTestData(ITestData testData) JsonTypeInfo typeInfo = options.GetTypeInfo(testData.Type); AIJsonSchemaCreateOptions? createOptions = typeInfo.Properties.Any(prop => prop.IsExtensionData) - ? new() { DisallowAdditionalProperties = false } // Do not append additionalProperties: false to the schema if the type has extension data. + ? new() { TransformOptions = new() { DisallowAdditionalProperties = false } } // Do not append additionalProperties: false to the schema if the type has extension data. : null; JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions); @@ -706,6 +726,33 @@ public static void TransformJsonSchema_UseNullableKeyword() AssertDeepEquals(expectedSchema, transformedSchema); } + [Fact] + public static void TransformJsonSchema_MoveDefaultKeywordToDescription() + { + JsonElement schema = JsonDocument.Parse(""" + { + "description": "My awesome schema", + "type": "array", + "default": [1,2,3] + } + """).RootElement; + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "description": "My awesome schema (Default value: [1,2,3])", + "type": "array" + } + """).RootElement; + + AIJsonSchemaTransformOptions options = new() + { + MoveDefaultKeywordToDescription = true, + }; + + JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options); + AssertDeepEquals(expectedSchema, transformedSchema); + } + [Theory] [MemberData(nameof(TestTypes.GetTestDataUsingAllValues), MemberType = typeof(TestTypes))] public static void TransformJsonSchema_ValidateWithTestData(ITestData testData) @@ -718,7 +765,7 @@ public static void TransformJsonSchema_ValidateWithTestData(ITestData testData) JsonTypeInfo typeInfo = options.GetTypeInfo(testData.Type); AIJsonSchemaCreateOptions? createOptions = typeInfo.Properties.Any(prop => prop.IsExtensionData) - ? new() { DisallowAdditionalProperties = false } // Do not append additionalProperties: false to the schema if the type has extension data. + ? new() { TransformOptions = new() { DisallowAdditionalProperties = false } } // Do not append additionalProperties: false to the schema if the type has extension data. : null; JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs index aae985c4c4b..edd22edc41e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs @@ -34,7 +34,8 @@ public async Task SuccessUsage_Default() GetResponseAsyncCallback = (messages, options, cancellationToken) => { var responseFormat = Assert.IsType(options!.ResponseFormat); - Assert.Equal(""" + Assert.NotNull(responseFormat.Schema); + AssertDeepEquals(JsonDocument.Parse(""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Some test description", @@ -65,7 +66,7 @@ public async Task SuccessUsage_Default() "species" ] } - """, responseFormat.Schema.ToString()); + """).RootElement, responseFormat.Schema.Value); Assert.Equal(nameof(Animal), responseFormat.SchemaName); Assert.Equal("Some test description", responseFormat.SchemaDescription); @@ -332,7 +333,8 @@ public async Task CanSpecifyCustomJsonSerializationOptions() // - The property is named full_name, because we specified SnakeCaseLower // - The species value is an integer instead of a string, because we didn't use enum-to-string conversion var responseFormat = Assert.IsType(options!.ResponseFormat); - Assert.Equal(""" + Assert.NotNull(responseFormat.Schema); + AssertDeepEquals(JsonDocument.Parse(""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Some test description", @@ -358,7 +360,7 @@ public async Task CanSpecifyCustomJsonSerializationOptions() "species" ] } - """, responseFormat.Schema.ToString()); + """).RootElement, responseFormat.Schema.Value); return Task.FromResult(expectedResponse); }, @@ -432,4 +434,28 @@ private class Envelope [JsonSerializable(typeof(Envelope))] [JsonSerializable(typeof(Data))] private partial class JsonContext2 : JsonSerializerContext; + + private static void AssertDeepEquals(JsonElement element1, JsonElement element2) + { +#pragma warning disable SA1118 // Parameter should not span multiple lines + Assert.True(DeepEquals(element1, element2), $""" + Elements are not equal. + Expected: + {element1} + Actual: + {element2} + """); +#pragma warning restore SA1118 // Parameter should not span multiple lines + } + + private static bool DeepEquals(JsonElement element1, JsonElement element2) + { +#if NET9_0_OR_GREATER + return JsonElement.DeepEquals(element1, element2); +#else + return System.Text.Json.Nodes.JsonNode.DeepEquals( + JsonSerializer.SerializeToNode(element1, AIJsonUtilities.DefaultOptions), + JsonSerializer.SerializeToNode(element2, AIJsonUtilities.DefaultOptions)); +#endif + } } From fa2d656fba04cdd7ac06cea6dadbc9aee73b1d5d Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 May 2025 21:28:48 +0100 Subject: [PATCH 097/472] Eliminate ingestion cache from AI Chat Web template (#6428) * Begin updating to latest MEVD * Reimplement JsonVectorStore to match updated MEVD APIs * Remove ingestion cache and track ingestion status inside the vector DB * Track the document metadata in a separate collection so we don't have to fetch literally everything from the vector DB in order to update ingestion * Fix equality comparison issue with Qdrant connector * Tidying * More tidying * Update MEAI.Templates test snapshots --------- Co-authored-by: Jeff Handley --- eng/Versions.props | 6 +- ...hatWithCustomData-CSharp.AppHost.csproj.in | 1 - .../Program.cs | 5 - .../ChatWithCustomData-CSharp.Web.csproj.in | 5 - .../Components/Pages/Chat/Chat.razor | 2 +- .../Program.Aspire.cs | 2 - .../ChatWithCustomData-CSharp.Web/Program.cs | 5 - ...manticSearchRecord.cs => IngestedChunk.cs} | 10 +- .../Services/IngestedDocument.cs | 26 ++++ .../Services/Ingestion/DataIngestor.cs | 62 +++++----- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Ingestion/IngestionCacheDbContext.cs | 48 -------- .../Services/Ingestion/PDFDirectorySource.cs | 52 ++++---- .../Services/JsonVectorStore.cs | 116 ++++++++++++------ .../Services/SemanticSearch.cs | 18 +-- .../Components/Pages/Chat/Chat.razor | 2 +- .../aichatweb/Program.cs | 7 +- .../aichatweb/Services/IngestedChunk.cs} | 8 +- .../aichatweb/Services/IngestedDocument.cs | 22 ++++ .../Services/Ingestion/DataIngestor.cs | 59 +++++---- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Ingestion/IngestionCacheDbContext.cs | 44 ------- .../Services/Ingestion/PDFDirectorySource.cs | 48 ++++---- .../aichatweb/Services/JsonVectorStore.cs | 116 ++++++++++++------ .../aichatweb/Services/SemanticSearch.cs | 16 +-- .../aichatweb/aichatweb.csproj | 3 +- .../aichatweb/aichatweb.AppHost/Program.cs | 5 - .../aichatweb.AppHost.csproj | 1 - .../Components/Pages/Chat/Chat.razor | 2 +- .../aichatweb/aichatweb.Web/Program.cs | 2 - ...manticSearchRecord.cs => IngestedChunk.cs} | 8 +- .../Services/IngestedDocument.cs | 22 ++++ .../Services/Ingestion/DataIngestor.cs | 59 +++++---- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Ingestion/IngestionCacheDbContext.cs | 44 ------- .../Services/Ingestion/PDFDirectorySource.cs | 48 ++++---- .../aichatweb.Web/Services/JsonVectorStore.cs | 116 ++++++++++++------ .../aichatweb.Web/Services/SemanticSearch.cs | 16 +-- .../aichatweb.Web/aichatweb.Web.csproj | 3 +- .../Components/Pages/Chat/Chat.razor | 2 +- .../aichatweb/Program.cs | 7 +- .../aichatweb/Services/IngestedChunk.cs} | 8 +- .../aichatweb/Services/IngestedDocument.cs | 22 ++++ .../Services/Ingestion/DataIngestor.cs | 59 +++++---- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Ingestion/IngestionCacheDbContext.cs | 44 ------- .../Services/Ingestion/PDFDirectorySource.cs | 48 ++++---- .../aichatweb/Services/SemanticSearch.cs | 16 +-- .../aichatweb/aichatweb.csproj | 5 +- 49 files changed, 597 insertions(+), 647 deletions(-) rename src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/{SemanticSearchRecord.cs => IngestedChunk.cs} (50%) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs delete mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IngestionCacheDbContext.cs rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/{aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearchRecord.cs => aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs} (54%) create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/{SemanticSearchRecord.cs => IngestedChunk.cs} (54%) create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IngestionCacheDbContext.cs rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/{aichatweb.Basic.verified/aichatweb/Services/SemanticSearchRecord.cs => aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs} (54%) create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs diff --git a/eng/Versions.props b/eng/Versions.props index 409d8c0d9aa..a643405a9a9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -165,9 +165,9 @@ 9.4.1-beta.277 9.4.1-beta.277 9.2.0 - 1.47.0-preview - 1.47.0-preview - 1.47.0 + 1.49.0-preview + 1.49.0-preview + 1.49.0 5.1.13 1.9.0 0.1.9 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index fef5822aa2b..fa5d922d53b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -16,7 +16,6 @@ - diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs index d19afaef3c3..6be0fd58648 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs @@ -38,8 +38,6 @@ #else // UseLocalVectorStore #endif -var ingestionCache = builder.AddSqlite("ingestionCache"); - var webApp = builder.AddProject("aichatweb-app"); #if (IsOllama) // AI SERVICE PROVIDER REFERENCES webApp @@ -58,8 +56,5 @@ .WaitFor(vectorDB); #else // UseLocalVectorStore #endif -webApp - .WithReference(ingestionCache) - .WaitFor(ingestionCache); builder.Build().Run(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index cf6689d1738..25798eeb26c 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -30,11 +30,6 @@ - - - - - diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index 9c6a3169144..e8da69efd54 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -118,7 +118,7 @@ await InvokeAsync(StateHasChanged); var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); return results.Select(result => - $"{result.Text}"); + $"{result.Text}"); } public void Dispose() diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 65092881529..5ecfd95d159 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -53,10 +53,8 @@ #endif builder.Services.AddScoped(); builder.Services.AddSingleton(); -builder.AddSqliteDbContext("ingestionCache"); var app = builder.Build(); -IngestionCacheDbContext.Initialize(app.Services); app.MapDefaultEndpoints(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index a02c911d317..1cea41e02eb 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -1,4 +1,3 @@ -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; using ChatWithCustomData_CSharp.Web.Components; @@ -102,11 +101,7 @@ builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); builder.Services.AddEmbeddingGenerator(embeddingGenerator); -builder.Services.AddDbContext(options => - options.UseSqlite("Data Source=ingestioncache.db")); - var app = builder.Build(); -IngestionCacheDbContext.Initialize(app.Services); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearchRecord.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs similarity index 50% rename from src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearchRecord.cs rename to src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs index 13f11d54294..641d2adfb38 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearchRecord.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs @@ -2,7 +2,7 @@ namespace ChatWithCustomData_CSharp.Web.Services; -public class SemanticSearchRecord +public class IngestedChunk { [VectorStoreRecordKey] #if (UseQdrant) @@ -11,8 +11,8 @@ public class SemanticSearchRecord public required string Key { get; set; } #endif - [VectorStoreRecordData(IsFilterable = true)] - public required string FileName { get; set; } + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } [VectorStoreRecordData] public int PageNumber { get; set; } @@ -21,9 +21,9 @@ public class SemanticSearchRecord public required string Text { get; set; } #if (IsOllama) - [VectorStoreRecordVector(384, DistanceFunction.CosineSimilarity)] // 384 is the default vector size for the all-minilm embedding model + [VectorStoreRecordVector(384, DistanceFunction = DistanceFunction.CosineSimilarity)] // 384 is the default vector size for the all-minilm embedding model #else - [VectorStoreRecordVector(1536, DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model + [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model #endif public ReadOnlyMemory Vector { get; set; } } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs new file mode 100644 index 00000000000..5306dc3f408 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.VectorData; + +namespace ChatWithCustomData_CSharp.Web.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] +#if (UseQdrant) + public required Guid Key { get; set; } +#else + public required string Key { get; set; } +#endif + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs index 4fb2f9ba370..eb64fa84c1b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; @@ -7,8 +6,7 @@ namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; public class DataIngestor( ILogger logger, IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore, - IngestionCacheDbContext ingestionCacheDb) + IVectorStore vectorStore) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -20,48 +18,48 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { #if (UseQdrant) - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-ingestion"); + var chunksCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); + var documentsCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-documents"); #else - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-ingestion"); + var chunksCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); + var documentsCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-documents"); #endif - await vectorCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); - var documentsForSource = ingestionCacheDb.Documents - .Where(d => d.SourceId == source.SourceId) - .Include(d => d.Records); + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); - var deletedFiles = await source.GetDeletedDocumentsAsync(documentsForSource); - foreach (var deletedFile in deletedFiles) + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {file}", deletedFile.Id); - await vectorCollection.DeleteBatchAsync(deletedFile.Records.Select(r => r.Id)); - ingestionCacheDb.Documents.Remove(deletedFile); + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); } - await ingestionCacheDb.SaveChangesAsync(); - var modifiedDocs = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); - foreach (var modifiedDoc in modifiedDocs) + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {file}", modifiedDoc.Id); + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); - if (modifiedDoc.Records.Count > 0) - { - await vectorCollection.DeleteBatchAsync(modifiedDoc.Records.Select(r => r.Id)); - } + await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateRecordsForDocumentAsync(embeddingGenerator, modifiedDoc.Id); - await foreach (var id in vectorCollection.UpsertBatchAsync(newRecords)) { } + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } - modifiedDoc.Records.Clear(); - modifiedDoc.Records.AddRange(newRecords.Select(r => new IngestedRecord { Id = r.Key, DocumentId = modifiedDoc.Id })); + logger.LogInformation("Ingestion is up-to-date"); - if (ingestionCacheDb.Entry(modifiedDoc).State == EntityState.Detached) + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) { - ingestionCacheDb.Documents.Add(modifiedDoc); + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } } - - await ingestionCacheDb.SaveChangesAsync(); - logger.LogInformation("Ingestion is up-to-date"); } } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs index 73c728865af..051bbbfebda 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs @@ -6,9 +6,9 @@ public interface IIngestionSource { string SourceId { get; } - Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments); + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); - Task> GetDeletedDocumentsAsync(IQueryable existingDocuments); + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId); + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IngestionCacheDbContext.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IngestionCacheDbContext.cs deleted file mode 100644 index 71be4c82cdf..00000000000 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IngestionCacheDbContext.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; - -// A DbContext that keeps track of which documents have been ingested. -// This makes it possible to avoid re-ingesting documents that have not changed, -// and to delete documents that have been removed from the underlying source. -public class IngestionCacheDbContext : DbContext -{ - public IngestionCacheDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Documents { get; set; } = default!; - public DbSet Records { get; set; } = default!; - - public static void Initialize(IServiceProvider services) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasMany(d => d.Records).WithOne().HasForeignKey(r => r.DocumentId).OnDelete(DeleteBehavior.Cascade); - } -} - -public class IngestedDocument -{ - // TODO: Make Id+SourceId a composite key - public required string Id { get; set; } - public required string SourceId { get; set; } - public required string Version { get; set; } - public List Records { get; set; } = []; -} - -public class IngestedRecord -{ -#if (UseQdrant) - public required Guid Id { get; set; } -#else - public required string Id { get; set; } -#endif - public required string DocumentId { get; set; } -} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs index 6354d04d375..7d9e4b4b08d 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,68 +1,66 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Text; -using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; -using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; using UglyToad.PdfPig; -using Microsoft.Extensions.AI; using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; public class PDFDirectorySource(string sourceDirectory) : IIngestionSource { public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; - public async Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments) + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) { var results = new List(); var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); foreach (var sourceFile in sourceFiles) { var sourceFileId = SourceFileId(sourceFile); - var sourceFileVersion = File.GetLastWriteTimeUtc(sourceFile).ToString("o"); - - var existingDocument = await existingDocuments.Where(d => d.SourceId == SourceId && d.Id == sourceFileId).FirstOrDefaultAsync(); - if (existingDocument is null) + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) { - results.Add(new() { Id = sourceFileId, Version = sourceFileVersion, SourceId = SourceId }); - } - else if (existingDocument.Version != sourceFileVersion) - { - existingDocument.Version = sourceFileVersion; - results.Add(existingDocument); +#if (UseQdrant) + results.Add(new() { Key = Guid.CreateVersion7(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); +#else + results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); +#endif } } - return results; + return Task.FromResult((IEnumerable)results); } - public async Task> GetDeletedDocumentsAsync(IQueryable existingDocuments) + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) { - var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); - var sourceFileIds = sourceFiles.Select(SourceFileId).ToList(); - return await existingDocuments - .Where(d => !sourceFileIds.Contains(d.Id)) - .ToListAsync(); + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); } - public async Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId) + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) { - using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, documentId)); + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new SemanticSearchRecord + return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { #if (UseQdrant) Key = Guid.CreateVersion7(), #else - Key = $"{Path.GetFileNameWithoutExtension(documentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", #endif - FileName = documentId, + DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, Vector = pair.Second.Vector, diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs index 09425f6a00a..b70af9fa033 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.VectorData; +using System.Linq.Expressions; using System.Numerics.Tensors; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text.Json; namespace ChatWithCustomData_CSharp.Web.Services; @@ -17,14 +17,34 @@ namespace ChatWithCustomData_CSharp.Web.Services; /// public class JsonVectorStore(string basePath) : IVectorStore { - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) where TKey : notnull - => new JsonVectorStoreRecordCollection(name, Path.Combine(basePath, name + ".json"), vectorStoreRecordDefinition); + public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) + => Task.FromResult(File.Exists(FilePath(name))); + + public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) + { + File.Delete(FilePath(name)); + return Task.CompletedTask; + } + + public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) + where TKey : notnull + where TRecord : notnull + => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); + + public object? GetService(Type serviceType, object? serviceKey = null) + => serviceKey is not null ? null : + serviceType.IsInstanceOfType(this) ? this : + null; public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").ToAsyncEnumerable(); + => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); + + private string FilePath(string collectionName) + => Path.Combine(basePath, collectionName + ".json"); private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection where TKey : notnull + where TRecord : notnull { private static readonly Func _getKey = CreateKeyReader(); private static readonly Func> _getVector = CreateVectorReader(); @@ -44,7 +64,7 @@ public JsonVectorStoreRecordCollection(string name, string filePath, VectorStore } } - public string CollectionName => _name; + public string Name => _name; public Task CollectionExistsAsync(CancellationToken cancellationToken = default) => Task.FromResult(_records is not null); @@ -69,7 +89,7 @@ public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) return WriteToDiskAsync(cancellationToken); } - public Task DeleteBatchAsync(IEnumerable keys, CancellationToken cancellationToken = default) + public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) { foreach (var key in keys) { @@ -89,36 +109,36 @@ public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => Task.FromResult(_records!.GetValueOrDefault(key)); - public IAsyncEnumerable GetBatchAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } + var filterCompiled = filter.Compile(); + var matches = _records!.Values.Where(r => filterCompiled(r)); - public async IAsyncEnumerable UpsertBatchAsync(IEnumerable records, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) + if (options?.OrderBy is { } orderBy) { - var key = _getKey(record); - _records![key] = record; - results.Add(key); + var matchesQueryable = matches.AsQueryable(); + foreach (var sort in orderBy.Values) + { + matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); + } + matches = matchesQueryable; } - await WriteToDiskAsync(cancellationToken); + return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); + } - foreach (var key in results) - { - yield return key; - } + public object? GetService(Type serviceType, object? serviceKey = null) + => null; + + public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull + { + throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); } - public Task> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull { if (vector is not ReadOnlyMemory floatVector) { @@ -137,9 +157,40 @@ public Task> VectorizedSearchAsync(TVector orderby similarity descending select (Record: record, Similarity: similarity); - var results = ranked.Skip(options?.Skip ?? 0).Take(options?.Top ?? int.MaxValue); - return Task.FromResult(new VectorSearchResults( - results.Select(r => new VectorSearchResult(r.Record, r.Similarity)).ToAsyncEnumerable())); + var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); + return results.ToAsyncEnumerable(); + } + + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull + => SearchEmbeddingAsync(vector, top, options, cancellationToken); + + public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + { + var key = _getKey(record); + _records![key] = record; + await WriteToDiskAsync(cancellationToken); + return key; + } + + public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + var results = new List(); + foreach (var record in records) + { + var key = _getKey(record); + _records![key] = record; + results.Add(key); + } + + await WriteToDiskAsync(cancellationToken); + return results; + } + + private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(_records); + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); + await File.WriteAllTextAsync(_filePath, json, cancellationToken); } private static Func CreateKeyReader() @@ -159,12 +210,5 @@ private static Func> CreateVectorReader() .Single(); return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } } } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs index e44c4144d27..2c32363c139 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs @@ -7,26 +7,20 @@ public class SemanticSearch( IEmbeddingGenerator> embeddingGenerator, IVectorStore vectorStore) { - public async Task> SearchAsync(string text, string? filenameFilter, int maxResults) + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); #if (UseQdrant) - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-ingestion"); + var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); #else - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-ingestion"); + var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); #endif - var nearest = await vectorCollection.VectorizedSearchAsync(queryEmbedding, new VectorSearchOptions + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions { - Top = maxResults, - Filter = filenameFilter is { Length: > 0 } ? record => record.FileName == filenameFilter : null, + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); - var results = new List(); - await foreach (var item in nearest.Results) - { - results.Add(item.Record); - } - return results; + return await nearest.Select(result => result.Record).ToListAsync(); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor index 5e4f8042add..a7b1502d894 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -108,7 +108,7 @@ await InvokeAsync(StateHasChanged); var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); return results.Select(result => - $"{result.Text}"); + $"{result.Text}"); } public void Dispose() diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs index de5a06ed171..7e14e993839 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; using aichatweb.Components; using aichatweb.Services; @@ -32,11 +31,7 @@ builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); builder.Services.AddEmbeddingGenerator(embeddingGenerator); -builder.Services.AddDbContext(options => - options.UseSqlite("Data Source=ingestioncache.db")); - var app = builder.Build(); -IngestionCacheDbContext.Initialize(app.Services); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearchRecord.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs similarity index 54% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearchRecord.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs index eb37cef61c8..9b61b7f9795 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearchRecord.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs @@ -2,13 +2,13 @@ namespace aichatweb.Services; -public class SemanticSearchRecord +public class IngestedChunk { [VectorStoreRecordKey] public required string Key { get; set; } - [VectorStoreRecordData(IsFilterable = true)] - public required string FileName { get; set; } + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } [VectorStoreRecordData] public int PageNumber { get; set; } @@ -16,6 +16,6 @@ public class SemanticSearchRecord [VectorStoreRecordData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model + [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model public ReadOnlyMemory Vector { get; set; } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs new file mode 100644 index 00000000000..60a42535c79 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] + public required string Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs index d18307ead2b..757a92bcc14 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; namespace aichatweb.Services.Ingestion; @@ -7,8 +6,7 @@ namespace aichatweb.Services.Ingestion; public class DataIngestor( ILogger logger, IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore, - IngestionCacheDbContext ingestionCacheDb) + IVectorStore vectorStore) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -19,45 +17,44 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); - await vectorCollection.CreateCollectionIfNotExistsAsync(); + var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); - var documentsForSource = ingestionCacheDb.Documents - .Where(d => d.SourceId == source.SourceId) - .Include(d => d.Records); + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); - var deletedFiles = await source.GetDeletedDocumentsAsync(documentsForSource); - foreach (var deletedFile in deletedFiles) + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {file}", deletedFile.Id); - await vectorCollection.DeleteBatchAsync(deletedFile.Records.Select(r => r.Id)); - ingestionCacheDb.Documents.Remove(deletedFile); + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); } - await ingestionCacheDb.SaveChangesAsync(); - var modifiedDocs = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); - foreach (var modifiedDoc in modifiedDocs) + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {file}", modifiedDoc.Id); + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); - if (modifiedDoc.Records.Count > 0) - { - await vectorCollection.DeleteBatchAsync(modifiedDoc.Records.Select(r => r.Id)); - } + await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateRecordsForDocumentAsync(embeddingGenerator, modifiedDoc.Id); - await foreach (var id in vectorCollection.UpsertBatchAsync(newRecords)) { } + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } - modifiedDoc.Records.Clear(); - modifiedDoc.Records.AddRange(newRecords.Select(r => new IngestedRecord { Id = r.Key, DocumentId = modifiedDoc.Id })); + logger.LogInformation("Ingestion is up-to-date"); - if (ingestionCacheDb.Entry(modifiedDoc).State == EntityState.Detached) + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) { - ingestionCacheDb.Documents.Add(modifiedDoc); + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } } - - await ingestionCacheDb.SaveChangesAsync(); - logger.LogInformation("Ingestion is up-to-date"); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs index 298f31190a3..89fdc81ada6 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs @@ -6,9 +6,9 @@ public interface IIngestionSource { string SourceId { get; } - Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments); + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); - Task> GetDeletedDocumentsAsync(IQueryable existingDocuments); + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId); + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs deleted file mode 100644 index 1d77efbb58c..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace aichatweb.Services.Ingestion; - -// A DbContext that keeps track of which documents have been ingested. -// This makes it possible to avoid re-ingesting documents that have not changed, -// and to delete documents that have been removed from the underlying source. -public class IngestionCacheDbContext : DbContext -{ - public IngestionCacheDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Documents { get; set; } = default!; - public DbSet Records { get; set; } = default!; - - public static void Initialize(IServiceProvider services) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasMany(d => d.Records).WithOne().HasForeignKey(r => r.DocumentId).OnDelete(DeleteBehavior.Cascade); - } -} - -public class IngestedDocument -{ - // TODO: Make Id+SourceId a composite key - public required string Id { get; set; } - public required string SourceId { get; set; } - public required string Version { get; set; } - public List Records { get; set; } = []; -} - -public class IngestedRecord -{ - public required string Id { get; set; } - public required string DocumentId { get; set; } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 9072a9c2b40..6bd64f345cc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -1,64 +1,58 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Text; -using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; -using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; using UglyToad.PdfPig; -using Microsoft.Extensions.AI; using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; namespace aichatweb.Services.Ingestion; public class PDFDirectorySource(string sourceDirectory) : IIngestionSource { public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; - public async Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments) + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) { var results = new List(); var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); foreach (var sourceFile in sourceFiles) { var sourceFileId = SourceFileId(sourceFile); - var sourceFileVersion = File.GetLastWriteTimeUtc(sourceFile).ToString("o"); - - var existingDocument = await existingDocuments.Where(d => d.SourceId == SourceId && d.Id == sourceFileId).FirstOrDefaultAsync(); - if (existingDocument is null) - { - results.Add(new() { Id = sourceFileId, Version = sourceFileVersion, SourceId = SourceId }); - } - else if (existingDocument.Version != sourceFileVersion) + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) { - existingDocument.Version = sourceFileVersion; - results.Add(existingDocument); + results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } - return results; + return Task.FromResult((IEnumerable)results); } - public async Task> GetDeletedDocumentsAsync(IQueryable existingDocuments) + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) { - var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); - var sourceFileIds = sourceFiles.Select(SourceFileId).ToList(); - return await existingDocuments - .Where(d => !sourceFileIds.Contains(d.Id)) - .ToListAsync(); + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); } - public async Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId) + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) { - using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, documentId)); + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new SemanticSearchRecord + return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(documentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", - FileName = documentId, + Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, Vector = pair.Second.Vector, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs index 8e6d273a27b..3b1d6024357 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.VectorData; +using System.Linq.Expressions; using System.Numerics.Tensors; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text.Json; namespace aichatweb.Services; @@ -17,14 +17,34 @@ namespace aichatweb.Services; /// public class JsonVectorStore(string basePath) : IVectorStore { - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) where TKey : notnull - => new JsonVectorStoreRecordCollection(name, Path.Combine(basePath, name + ".json"), vectorStoreRecordDefinition); + public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) + => Task.FromResult(File.Exists(FilePath(name))); + + public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) + { + File.Delete(FilePath(name)); + return Task.CompletedTask; + } + + public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) + where TKey : notnull + where TRecord : notnull + => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); + + public object? GetService(Type serviceType, object? serviceKey = null) + => serviceKey is not null ? null : + serviceType.IsInstanceOfType(this) ? this : + null; public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").ToAsyncEnumerable(); + => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); + + private string FilePath(string collectionName) + => Path.Combine(basePath, collectionName + ".json"); private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection where TKey : notnull + where TRecord : notnull { private static readonly Func _getKey = CreateKeyReader(); private static readonly Func> _getVector = CreateVectorReader(); @@ -44,7 +64,7 @@ public JsonVectorStoreRecordCollection(string name, string filePath, VectorStore } } - public string CollectionName => _name; + public string Name => _name; public Task CollectionExistsAsync(CancellationToken cancellationToken = default) => Task.FromResult(_records is not null); @@ -69,7 +89,7 @@ public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) return WriteToDiskAsync(cancellationToken); } - public Task DeleteBatchAsync(IEnumerable keys, CancellationToken cancellationToken = default) + public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) { foreach (var key in keys) { @@ -89,36 +109,36 @@ public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => Task.FromResult(_records!.GetValueOrDefault(key)); - public IAsyncEnumerable GetBatchAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } + var filterCompiled = filter.Compile(); + var matches = _records!.Values.Where(r => filterCompiled(r)); - public async IAsyncEnumerable UpsertBatchAsync(IEnumerable records, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) + if (options?.OrderBy is { } orderBy) { - var key = _getKey(record); - _records![key] = record; - results.Add(key); + var matchesQueryable = matches.AsQueryable(); + foreach (var sort in orderBy.Values) + { + matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); + } + matches = matchesQueryable; } - await WriteToDiskAsync(cancellationToken); + return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); + } - foreach (var key in results) - { - yield return key; - } + public object? GetService(Type serviceType, object? serviceKey = null) + => null; + + public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull + { + throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); } - public Task> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull { if (vector is not ReadOnlyMemory floatVector) { @@ -137,9 +157,40 @@ public Task> VectorizedSearchAsync(TVector orderby similarity descending select (Record: record, Similarity: similarity); - var results = ranked.Skip(options?.Skip ?? 0).Take(options?.Top ?? int.MaxValue); - return Task.FromResult(new VectorSearchResults( - results.Select(r => new VectorSearchResult(r.Record, r.Similarity)).ToAsyncEnumerable())); + var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); + return results.ToAsyncEnumerable(); + } + + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull + => SearchEmbeddingAsync(vector, top, options, cancellationToken); + + public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + { + var key = _getKey(record); + _records![key] = record; + await WriteToDiskAsync(cancellationToken); + return key; + } + + public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + var results = new List(); + foreach (var record in records) + { + var key = _getKey(record); + _records![key] = record; + results.Add(key); + } + + await WriteToDiskAsync(cancellationToken); + return results; + } + + private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(_records); + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); + await File.WriteAllTextAsync(_filePath, json, cancellationToken); } private static Func CreateKeyReader() @@ -159,12 +210,5 @@ private static Func> CreateVectorReader() .Single(); return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs index 1ac3977d014..c380a69ce25 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs @@ -7,22 +7,16 @@ public class SemanticSearch( IEmbeddingGenerator> embeddingGenerator, IVectorStore vectorStore) { - public async Task> SearchAsync(string text, string? filenameFilter, int maxResults) + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); + var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var nearest = await vectorCollection.VectorizedSearchAsync(queryEmbedding, new VectorSearchOptions + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions { - Top = maxResults, - Filter = filenameFilter is { Length: > 0 } ? record => record.FileName == filenameFilter : null, + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); - var results = new List(); - await foreach (var item in nearest.Results) - { - results.Add(item.Record); - } - return results; + return await nearest.Select(result => result.Record).ToListAsync(); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index a6069df327b..105a071abe0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -9,9 +9,8 @@ - - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs index 1a7cc375e1a..d41eea07e40 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs @@ -6,12 +6,7 @@ // dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" var openai = builder.AddConnectionString("openai"); -var ingestionCache = builder.AddSqlite("ingestionCache"); - var webApp = builder.AddProject("aichatweb-app"); webApp.WithReference(openai); -webApp - .WithReference(ingestionCache) - .WaitFor(ingestionCache); builder.Build().Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index a74ef7b7f3b..1eb887382c9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -13,7 +13,6 @@ - diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index 5e4f8042add..a7b1502d894 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -108,7 +108,7 @@ await InvokeAsync(StateHasChanged); var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); return results.Select(result => - $"{result.Text}"); + $"{result.Text}"); } public void Dispose() diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs index 729630d235a..665d1289fef 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -20,10 +20,8 @@ builder.Services.AddSingleton(vectorStore); builder.Services.AddScoped(); builder.Services.AddSingleton(); -builder.AddSqliteDbContext("ingestionCache"); var app = builder.Build(); -IngestionCacheDbContext.Initialize(app.Services); app.MapDefaultEndpoints(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearchRecord.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs similarity index 54% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearchRecord.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index 23371589c7e..cff3717518e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearchRecord.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -2,13 +2,13 @@ namespace aichatweb.Web.Services; -public class SemanticSearchRecord +public class IngestedChunk { [VectorStoreRecordKey] public required string Key { get; set; } - [VectorStoreRecordData(IsFilterable = true)] - public required string FileName { get; set; } + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } [VectorStoreRecordData] public int PageNumber { get; set; } @@ -16,6 +16,6 @@ public class SemanticSearchRecord [VectorStoreRecordData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model + [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model public ReadOnlyMemory Vector { get; set; } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs new file mode 100644 index 00000000000..303cdc806b4 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] + public required string Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 31b127914dd..904c60567a4 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; namespace aichatweb.Web.Services.Ingestion; @@ -7,8 +6,7 @@ namespace aichatweb.Web.Services.Ingestion; public class DataIngestor( ILogger logger, IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore, - IngestionCacheDbContext ingestionCacheDb) + IVectorStore vectorStore) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -19,45 +17,44 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); - await vectorCollection.CreateCollectionIfNotExistsAsync(); + var chunksCollection = vectorStore.GetCollection("data-aichatweb.Web-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb.Web-documents"); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); - var documentsForSource = ingestionCacheDb.Documents - .Where(d => d.SourceId == source.SourceId) - .Include(d => d.Records); + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); - var deletedFiles = await source.GetDeletedDocumentsAsync(documentsForSource); - foreach (var deletedFile in deletedFiles) + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {file}", deletedFile.Id); - await vectorCollection.DeleteBatchAsync(deletedFile.Records.Select(r => r.Id)); - ingestionCacheDb.Documents.Remove(deletedFile); + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); } - await ingestionCacheDb.SaveChangesAsync(); - var modifiedDocs = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); - foreach (var modifiedDoc in modifiedDocs) + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {file}", modifiedDoc.Id); + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); - if (modifiedDoc.Records.Count > 0) - { - await vectorCollection.DeleteBatchAsync(modifiedDoc.Records.Select(r => r.Id)); - } + await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateRecordsForDocumentAsync(embeddingGenerator, modifiedDoc.Id); - await foreach (var id in vectorCollection.UpsertBatchAsync(newRecords)) { } + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } - modifiedDoc.Records.Clear(); - modifiedDoc.Records.AddRange(newRecords.Select(r => new IngestedRecord { Id = r.Key, DocumentId = modifiedDoc.Id })); + logger.LogInformation("Ingestion is up-to-date"); - if (ingestionCacheDb.Entry(modifiedDoc).State == EntityState.Detached) + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) { - ingestionCacheDb.Documents.Add(modifiedDoc); + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } } - - await ingestionCacheDb.SaveChangesAsync(); - logger.LogInformation("Ingestion is up-to-date"); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs index a9e92b26779..208b32b2fdf 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -6,9 +6,9 @@ public interface IIngestionSource { string SourceId { get; } - Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments); + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); - Task> GetDeletedDocumentsAsync(IQueryable existingDocuments); + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId); + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IngestionCacheDbContext.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IngestionCacheDbContext.cs deleted file mode 100644 index 66a02d3a0f6..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IngestionCacheDbContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace aichatweb.Web.Services.Ingestion; - -// A DbContext that keeps track of which documents have been ingested. -// This makes it possible to avoid re-ingesting documents that have not changed, -// and to delete documents that have been removed from the underlying source. -public class IngestionCacheDbContext : DbContext -{ - public IngestionCacheDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Documents { get; set; } = default!; - public DbSet Records { get; set; } = default!; - - public static void Initialize(IServiceProvider services) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasMany(d => d.Records).WithOne().HasForeignKey(r => r.DocumentId).OnDelete(DeleteBehavior.Cascade); - } -} - -public class IngestedDocument -{ - // TODO: Make Id+SourceId a composite key - public required string Id { get; set; } - public required string SourceId { get; set; } - public required string Version { get; set; } - public List Records { get; set; } = []; -} - -public class IngestedRecord -{ - public required string Id { get; set; } - public required string DocumentId { get; set; } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index d6a10a626b0..32e7c88196f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,64 +1,58 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Text; -using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; -using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; using UglyToad.PdfPig; -using Microsoft.Extensions.AI; using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; namespace aichatweb.Web.Services.Ingestion; public class PDFDirectorySource(string sourceDirectory) : IIngestionSource { public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; - public async Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments) + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) { var results = new List(); var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); foreach (var sourceFile in sourceFiles) { var sourceFileId = SourceFileId(sourceFile); - var sourceFileVersion = File.GetLastWriteTimeUtc(sourceFile).ToString("o"); - - var existingDocument = await existingDocuments.Where(d => d.SourceId == SourceId && d.Id == sourceFileId).FirstOrDefaultAsync(); - if (existingDocument is null) - { - results.Add(new() { Id = sourceFileId, Version = sourceFileVersion, SourceId = SourceId }); - } - else if (existingDocument.Version != sourceFileVersion) + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) { - existingDocument.Version = sourceFileVersion; - results.Add(existingDocument); + results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } - return results; + return Task.FromResult((IEnumerable)results); } - public async Task> GetDeletedDocumentsAsync(IQueryable existingDocuments) + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) { - var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); - var sourceFileIds = sourceFiles.Select(SourceFileId).ToList(); - return await existingDocuments - .Where(d => !sourceFileIds.Contains(d.Id)) - .ToListAsync(); + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); } - public async Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId) + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) { - using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, documentId)); + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new SemanticSearchRecord + return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(documentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", - FileName = documentId, + Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, Vector = pair.Second.Vector, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs index de36fb68a17..3e226c76150 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.VectorData; +using System.Linq.Expressions; using System.Numerics.Tensors; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text.Json; namespace aichatweb.Web.Services; @@ -17,14 +17,34 @@ namespace aichatweb.Web.Services; /// public class JsonVectorStore(string basePath) : IVectorStore { - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) where TKey : notnull - => new JsonVectorStoreRecordCollection(name, Path.Combine(basePath, name + ".json"), vectorStoreRecordDefinition); + public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) + => Task.FromResult(File.Exists(FilePath(name))); + + public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) + { + File.Delete(FilePath(name)); + return Task.CompletedTask; + } + + public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) + where TKey : notnull + where TRecord : notnull + => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); + + public object? GetService(Type serviceType, object? serviceKey = null) + => serviceKey is not null ? null : + serviceType.IsInstanceOfType(this) ? this : + null; public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").ToAsyncEnumerable(); + => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); + + private string FilePath(string collectionName) + => Path.Combine(basePath, collectionName + ".json"); private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection where TKey : notnull + where TRecord : notnull { private static readonly Func _getKey = CreateKeyReader(); private static readonly Func> _getVector = CreateVectorReader(); @@ -44,7 +64,7 @@ public JsonVectorStoreRecordCollection(string name, string filePath, VectorStore } } - public string CollectionName => _name; + public string Name => _name; public Task CollectionExistsAsync(CancellationToken cancellationToken = default) => Task.FromResult(_records is not null); @@ -69,7 +89,7 @@ public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) return WriteToDiskAsync(cancellationToken); } - public Task DeleteBatchAsync(IEnumerable keys, CancellationToken cancellationToken = default) + public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) { foreach (var key in keys) { @@ -89,36 +109,36 @@ public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => Task.FromResult(_records!.GetValueOrDefault(key)); - public IAsyncEnumerable GetBatchAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } + var filterCompiled = filter.Compile(); + var matches = _records!.Values.Where(r => filterCompiled(r)); - public async IAsyncEnumerable UpsertBatchAsync(IEnumerable records, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) + if (options?.OrderBy is { } orderBy) { - var key = _getKey(record); - _records![key] = record; - results.Add(key); + var matchesQueryable = matches.AsQueryable(); + foreach (var sort in orderBy.Values) + { + matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); + } + matches = matchesQueryable; } - await WriteToDiskAsync(cancellationToken); + return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); + } - foreach (var key in results) - { - yield return key; - } + public object? GetService(Type serviceType, object? serviceKey = null) + => null; + + public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull + { + throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); } - public Task> VectorizedSearchAsync(TVector vector, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull { if (vector is not ReadOnlyMemory floatVector) { @@ -137,9 +157,40 @@ public Task> VectorizedSearchAsync(TVector orderby similarity descending select (Record: record, Similarity: similarity); - var results = ranked.Skip(options?.Skip ?? 0).Take(options?.Top ?? int.MaxValue); - return Task.FromResult(new VectorSearchResults( - results.Select(r => new VectorSearchResult(r.Record, r.Similarity)).ToAsyncEnumerable())); + var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); + return results.ToAsyncEnumerable(); + } + + public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull + => SearchEmbeddingAsync(vector, top, options, cancellationToken); + + public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) + { + var key = _getKey(record); + _records![key] = record; + await WriteToDiskAsync(cancellationToken); + return key; + } + + public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + var results = new List(); + foreach (var record in records) + { + var key = _getKey(record); + _records![key] = record; + results.Add(key); + } + + await WriteToDiskAsync(cancellationToken); + return results; + } + + private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(_records); + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); + await File.WriteAllTextAsync(_filePath, json, cancellationToken); } private static Func CreateKeyReader() @@ -159,12 +210,5 @@ private static Func> CreateVectorReader() .Single(); return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs index 4f775cd1db4..2ab511c104c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -7,22 +7,16 @@ public class SemanticSearch( IEmbeddingGenerator> embeddingGenerator, IVectorStore vectorStore) { - public async Task> SearchAsync(string text, string? filenameFilter, int maxResults) + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); + var vectorCollection = vectorStore.GetCollection("data-aichatweb.Web-chunks"); - var nearest = await vectorCollection.VectorizedSearchAsync(queryEmbedding, new VectorSearchOptions + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions { - Top = maxResults, - Filter = filenameFilter is { Length: > 0 } ? record => record.FileName == filenameFilter : null, + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); - var results = new List(); - await foreach (var item in nearest.Results) - { - results.Add(item.Record); - } - return results; + return await nearest.Select(result => result.Record).ToListAsync(); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 70da05797de..4668f26faff 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,9 +10,8 @@ - - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index 5e4f8042add..a7b1502d894 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -108,7 +108,7 @@ await InvokeAsync(StateHasChanged); var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); return results.Select(result => - $"{result.Text}"); + $"{result.Text}"); } public void Dispose() diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index 4c007e3be1a..f7b32a97610 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; using aichatweb.Components; using aichatweb.Services; @@ -38,11 +37,7 @@ builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); builder.Services.AddEmbeddingGenerator(embeddingGenerator); -builder.Services.AddDbContext(options => - options.UseSqlite("Data Source=ingestioncache.db")); - var app = builder.Build(); -IngestionCacheDbContext.Initialize(app.Services); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearchRecord.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs similarity index 54% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearchRecord.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs index eb37cef61c8..9b61b7f9795 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearchRecord.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs @@ -2,13 +2,13 @@ namespace aichatweb.Services; -public class SemanticSearchRecord +public class IngestedChunk { [VectorStoreRecordKey] public required string Key { get; set; } - [VectorStoreRecordData(IsFilterable = true)] - public required string FileName { get; set; } + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } [VectorStoreRecordData] public int PageNumber { get; set; } @@ -16,6 +16,6 @@ public class SemanticSearchRecord [VectorStoreRecordData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model + [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model public ReadOnlyMemory Vector { get; set; } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs new file mode 100644 index 00000000000..60a42535c79 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] + public required string Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs index d18307ead2b..757a92bcc14 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.VectorData; namespace aichatweb.Services.Ingestion; @@ -7,8 +6,7 @@ namespace aichatweb.Services.Ingestion; public class DataIngestor( ILogger logger, IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore, - IngestionCacheDbContext ingestionCacheDb) + IVectorStore vectorStore) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -19,45 +17,44 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); - await vectorCollection.CreateCollectionIfNotExistsAsync(); + var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); - var documentsForSource = ingestionCacheDb.Documents - .Where(d => d.SourceId == source.SourceId) - .Include(d => d.Records); + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); - var deletedFiles = await source.GetDeletedDocumentsAsync(documentsForSource); - foreach (var deletedFile in deletedFiles) + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {file}", deletedFile.Id); - await vectorCollection.DeleteBatchAsync(deletedFile.Records.Select(r => r.Id)); - ingestionCacheDb.Documents.Remove(deletedFile); + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); } - await ingestionCacheDb.SaveChangesAsync(); - var modifiedDocs = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); - foreach (var modifiedDoc in modifiedDocs) + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {file}", modifiedDoc.Id); + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); - if (modifiedDoc.Records.Count > 0) - { - await vectorCollection.DeleteBatchAsync(modifiedDoc.Records.Select(r => r.Id)); - } + await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateRecordsForDocumentAsync(embeddingGenerator, modifiedDoc.Id); - await foreach (var id in vectorCollection.UpsertBatchAsync(newRecords)) { } + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } - modifiedDoc.Records.Clear(); - modifiedDoc.Records.AddRange(newRecords.Select(r => new IngestedRecord { Id = r.Key, DocumentId = modifiedDoc.Id })); + logger.LogInformation("Ingestion is up-to-date"); - if (ingestionCacheDb.Entry(modifiedDoc).State == EntityState.Detached) + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) { - ingestionCacheDb.Documents.Add(modifiedDoc); + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } } - - await ingestionCacheDb.SaveChangesAsync(); - logger.LogInformation("Ingestion is up-to-date"); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs index 298f31190a3..89fdc81ada6 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs @@ -6,9 +6,9 @@ public interface IIngestionSource { string SourceId { get; } - Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments); + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); - Task> GetDeletedDocumentsAsync(IQueryable existingDocuments); + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId); + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs deleted file mode 100644 index 1d77efbb58c..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IngestionCacheDbContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace aichatweb.Services.Ingestion; - -// A DbContext that keeps track of which documents have been ingested. -// This makes it possible to avoid re-ingesting documents that have not changed, -// and to delete documents that have been removed from the underlying source. -public class IngestionCacheDbContext : DbContext -{ - public IngestionCacheDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Documents { get; set; } = default!; - public DbSet Records { get; set; } = default!; - - public static void Initialize(IServiceProvider services) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasMany(d => d.Records).WithOne().HasForeignKey(r => r.DocumentId).OnDelete(DeleteBehavior.Cascade); - } -} - -public class IngestedDocument -{ - // TODO: Make Id+SourceId a composite key - public required string Id { get; set; } - public required string SourceId { get; set; } - public required string Version { get; set; } - public List Records { get; set; } = []; -} - -public class IngestedRecord -{ - public required string Id { get; set; } - public required string DocumentId { get; set; } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 9072a9c2b40..6bd64f345cc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -1,64 +1,58 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel.Text; -using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; -using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; using UglyToad.PdfPig; -using Microsoft.Extensions.AI; using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; namespace aichatweb.Services.Ingestion; public class PDFDirectorySource(string sourceDirectory) : IIngestionSource { public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; - public async Task> GetNewOrModifiedDocumentsAsync(IQueryable existingDocuments) + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) { var results = new List(); var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); foreach (var sourceFile in sourceFiles) { var sourceFileId = SourceFileId(sourceFile); - var sourceFileVersion = File.GetLastWriteTimeUtc(sourceFile).ToString("o"); - - var existingDocument = await existingDocuments.Where(d => d.SourceId == SourceId && d.Id == sourceFileId).FirstOrDefaultAsync(); - if (existingDocument is null) - { - results.Add(new() { Id = sourceFileId, Version = sourceFileVersion, SourceId = SourceId }); - } - else if (existingDocument.Version != sourceFileVersion) + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) { - existingDocument.Version = sourceFileVersion; - results.Add(existingDocument); + results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } - return results; + return Task.FromResult((IEnumerable)results); } - public async Task> GetDeletedDocumentsAsync(IQueryable existingDocuments) + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) { - var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); - var sourceFileIds = sourceFiles.Select(SourceFileId).ToList(); - return await existingDocuments - .Where(d => !sourceFileIds.Contains(d.Id)) - .ToListAsync(); + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); } - public async Task> CreateRecordsForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, string documentId) + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) { - using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, documentId)); + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new SemanticSearchRecord + return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(documentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", - FileName = documentId, + Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, Vector = pair.Second.Vector, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs index 1ac3977d014..c380a69ce25 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs @@ -7,22 +7,16 @@ public class SemanticSearch( IEmbeddingGenerator> embeddingGenerator, IVectorStore vectorStore) { - public async Task> SearchAsync(string text, string? filenameFilter, int maxResults) + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-ingested"); + var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var nearest = await vectorCollection.VectorizedSearchAsync(queryEmbedding, new VectorSearchOptions + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions { - Top = maxResults, - Filter = filenameFilter is { Length: > 0 } ? record => record.FileName == filenameFilter : null, + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); - var results = new List(); - await foreach (var item in nearest.Results) - { - results.Add(item.Record); - } - return results; + return await nearest.Select(result => result.Record).ToListAsync(); } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 502d1f84c95..b0d30de8823 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -10,13 +10,12 @@ - - + - + From 5444639d42f42da5a9f3e1d84b9faece176f3f56 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 12 May 2025 20:59:42 -0700 Subject: [PATCH 098/472] Update the template test README with snapshot update instructions (#6431) --- .../README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/README.md index ca589c22c2c..5717aac1e1c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/README.md @@ -2,4 +2,6 @@ Contains snapshot and execution tests for `Microsoft.Extensions.AI.Templates`. +To update test snapshots, install and run the `DiffEngineTray` tool following [these instructions](https://github.com/VerifyTests/DiffEngine/blob/main/docs/tray.md), run the snapshot tests either in VS or using `dotnet test`, and use `DiffEngineTray` to accept or discard changes. + For information on debugging template execution tests, see [this README](./TemplateSandbox/README.md). From c41a59cd59d1f6abd7fb3aa97f2e70512614564b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 13 May 2025 05:02:29 +0100 Subject: [PATCH 099/472] AI Chat Web template fixes for Azure AI Search (#6429) * AI Chat Web template fixes for Azure AI Search * Update snapshots --- .../Services/IngestedDocument.cs | 4 ++-- .../Services/Ingestion/PDFDirectorySource.cs | 4 ++-- .../aichatweb/Services/IngestedDocument.cs | 4 ++-- .../aichatweb/Services/Ingestion/PDFDirectorySource.cs | 4 ++-- .../aichatweb/aichatweb.Web/Services/IngestedDocument.cs | 4 ++-- .../aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs | 4 ++-- .../aichatweb/Services/IngestedDocument.cs | 4 ++-- .../aichatweb/Services/Ingestion/PDFDirectorySource.cs | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs index 5306dc3f408..c99a2c78423 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs @@ -21,6 +21,6 @@ public class IngestedDocument public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] - public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs index 7d9e4b4b08d..7970123b73d 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs @@ -30,7 +30,7 @@ public Task> GetNewOrModifiedDocumentsAsync(IReadO #if (UseQdrant) results.Add(new() { Key = Guid.CreateVersion7(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); #else - results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); #endif } } @@ -58,7 +58,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe #if (UseQdrant) Key = Guid.CreateVersion7(), #else - Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + Key = Guid.CreateVersion7().ToString(), #endif DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs index 60a42535c79..cc852657143 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs @@ -17,6 +17,6 @@ public class IngestedDocument public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] - public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 6bd64f345cc..2ede5add217 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -27,7 +27,7 @@ public Task> GetNewOrModifiedDocumentsAsync(IReadO var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; if (existingDocumentVersion != sourceFileVersion) { - results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } @@ -51,7 +51,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs index 303cdc806b4..4be3b2980d7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -17,6 +17,6 @@ public class IngestedDocument public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] - public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index 32e7c88196f..2bd7a97bd7b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -27,7 +27,7 @@ public Task> GetNewOrModifiedDocumentsAsync(IReadO var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; if (existingDocumentVersion != sourceFileVersion) { - results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } @@ -51,7 +51,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs index 60a42535c79..cc852657143 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs @@ -17,6 +17,6 @@ public class IngestedDocument public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(1, DistanceFunction = DistanceFunction.CosineSimilarity)] - public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0]); + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 6bd64f345cc..2ede5add217 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -27,7 +27,7 @@ public Task> GetNewOrModifiedDocumentsAsync(IReadO var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; if (existingDocumentVersion != sourceFileVersion) { - results.Add(new() { Key = $"{SourceId}_{sourceFileId}", SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); } } @@ -51,7 +51,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk { - Key = $"{Path.GetFileNameWithoutExtension(document.DocumentId)}_{pair.First.PageNumber}_{pair.First.IndexOnPage}", + Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, PageNumber = pair.First.PageNumber, Text = pair.First.Text, From b2298a64a2955207182498cb2bf2e00bc6524237 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 12 May 2025 21:31:02 -0700 Subject: [PATCH 100/472] Add security comments for chat clients (#6386) --- .../ChatCompletion/IChatClient.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs index b4354e22a43..570eb7ef497 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs @@ -11,6 +11,11 @@ namespace Microsoft.Extensions.AI; /// Represents a chat client. /// /// +/// Applications must consider risks such as prompt injection attacks, data sizes, and the number of messages +/// sent to the underlying provider or returned from it. Unless a specific implementation +/// explicitly documents safeguards for these concerns, the application is expected to implement appropriate protections. +/// +/// /// Unless otherwise specified, all members of are thread-safe for concurrent use. /// It is expected that all implementations of support being used by multiple requests concurrently. /// Instances must not be disposed of while the instance is still in use. From fb519ea054e964207d50e41d78e3ee10a685f772 Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 13 May 2025 13:19:46 +0200 Subject: [PATCH 101/472] Fix dynamic config update for log buffering (#6435) Co-authored-by: evgenyfedorov2 --- ...IncomingRequestLoggingBuilderExtensions.cs | 13 ++-- .../Buffering/PerRequestLogBufferManager.cs | 7 +- .../Buffering/GlobalBuffer.cs | 11 +-- .../GlobalBufferLoggingBuilderExtensions.cs | 5 +- .../Buffering/GlobalLogBufferManager.cs | 6 +- ...ingRequestLoggingBuilderExtensionsTests.cs | 77 ++++++++++++++++++- .../Buffering/TestConfiguration.cs | 37 +++++++++ ...lobalBufferLoggerBuilderExtensionsTests.cs | 75 ++++++++++++++++++ .../Logging/ExtendedLoggerTests.cs | 2 +- 9 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs index 8d3f7411367..41664d466ce 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs @@ -39,7 +39,10 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b _ = Throw.IfNull(configuration); _ = builder.Services - .AddSingleton>(new PerRequestLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new PerRequestLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new ConfigurationChangeTokenSource(configuration)) .AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart(); @@ -64,8 +67,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - _ = builder.Services - .AddOptionsWithValidateOnStart() + _ = builder + .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() .Configure(configure); @@ -92,8 +95,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b { _ = Throw.IfNull(builder); - _ = builder.Services - .AddOptionsWithValidateOnStart() + _ = builder + .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() .Configure(options => { diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs index de51e26eb58..60dafd1e177 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs @@ -12,10 +12,11 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class PerRequestLogBufferManager : PerRequestLogBuffer { + internal readonly IOptionsMonitor Options; + private readonly GlobalLogBuffer _globalBuffer; private readonly IHttpContextAccessor _httpContextAccessor; private readonly LogBufferingFilterRuleSelector _ruleSelector; - private readonly IOptionsMonitor _options; public PerRequestLogBufferManager( GlobalLogBuffer globalBuffer, @@ -26,7 +27,7 @@ public PerRequestLogBufferManager( _globalBuffer = globalBuffer; _httpContextAccessor = httpContextAccessor; _ruleSelector = ruleSelector; - _options = options; + Options = options; } public override void Flush() @@ -47,7 +48,7 @@ public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEn IncomingRequestLogBufferHolder? bufferHolder = httpContext.RequestServices.GetService(); IncomingRequestLogBuffer? buffer = bufferHolder?.GetOrAdd(category, _ => - new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, _options)); + new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, Options)); if (buffer is null) { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 64bd8ffe439..5d0391e68e6 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -17,6 +17,8 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalBuffer : IDisposable { + internal LogBufferingFilterRule[] LastKnownGoodFilterRules; + private const int MaxBatchSize = 256; private static readonly ObjectPool> _recordsToEmitListPool = PoolFactory.CreateListPoolWithCapacity(MaxBatchSize); @@ -34,7 +36,6 @@ internal sealed class GlobalBuffer : IDisposable private DateTimeOffset _lastFlushTimestamp; private int _activeBufferSize; - private LogBufferingFilterRule[] _lastKnownGoodFilterRules; private volatile bool _disposed; @@ -50,7 +51,7 @@ public GlobalBuffer( _bufferedLogger = bufferedLogger; _category = Throw.IfNullOrEmpty(category); _ruleSelector = Throw.IfNull(ruleSelector); - _lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category); + LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category); _optionsChangeTokenRegistration = options.OnChange(OnOptionsChanged); } @@ -83,7 +84,7 @@ public bool TryEnqueue(LogEntry logEntry) $"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList>"); } - if (_ruleSelector.Select(_lastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null) + if (_ruleSelector.Select(LastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null) { // buffering is not enabled for this log entry, // return false to indicate that the log entry should be logged normally. @@ -162,11 +163,11 @@ private void OnOptionsChanged(GlobalLogBufferingOptions? updatedOptions) { if (updatedOptions is null) { - _lastKnownGoodFilterRules = []; + LastKnownGoodFilterRules = []; } else { - _lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category); + LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category); } _ruleSelector.InvalidateCache(); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs index bf06c546566..150f82b7414 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs @@ -38,7 +38,10 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, ICon _ = builder .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() - .Services.AddSingleton>(new GlobalLogBufferingConfigureOptions(configuration)); + .Services.AddSingleton>( + new GlobalLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new ConfigurationChangeTokenSource(configuration)); return builder.AddGlobalBufferManager(); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs index 8cc25fe3a7c..449b12580fb 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalLogBufferManager : GlobalLogBuffer { - private readonly ConcurrentDictionary _buffers = []; + internal readonly ConcurrentDictionary Buffers = []; private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; private readonly LogBufferingFilterRuleSelector _ruleSelector; @@ -35,7 +35,7 @@ internal GlobalLogBufferManager( public override void Flush() { - foreach (GlobalBuffer buffer in _buffers.Values) + foreach (GlobalBuffer buffer in Buffers.Values) { buffer.Flush(); } @@ -44,7 +44,7 @@ public override void Flush() public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry) { string category = logEntry.Category; - GlobalBuffer buffer = _buffers.GetOrAdd(category, _ => new GlobalBuffer( + GlobalBuffer buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer( bufferedLogger, category, _ruleSelector, diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs index 31ea8b45d4a..5bad831ab68 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs @@ -3,15 +3,23 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.Buffering; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Buffering; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Test; using Microsoft.Extensions.Options; using Xunit; using PerRequestLogBuffer = Microsoft.Extensions.Diagnostics.Buffering.PerRequestLogBuffer; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; public class PerIncomingRequestLoggingBuilderExtensionsTests { @@ -82,5 +90,72 @@ public void WhenConfigurationActionProvided_RegistersInDI() Assert.NotNull(options.CurrentValue); Assert.Equivalent(expectedData, options.CurrentValue.Rules); } + + [Fact] + public async Task WhenConfigUpdated_PicksUpConfigChanges() + { + List initialData = + [ + new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"), + new(logLevel : LogLevel.Information), + ]; + List updatedData = + [ + new(logLevel: LogLevel.Information), + ]; + string jsonConfig = + @" +{ + ""PerIncomingRequestLogBuffering"": { + ""Rules"": [ + { + ""CategoryName"": ""Program.MyLogger"", + ""LogLevel"": ""Information"", + ""EventId"": 1, + ""EventName"": ""number one"", + }, + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + + using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig); + using IHost host = await FakeHost.CreateBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(x => x.AddRouting()) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddPerIncomingRequestBuffer(config)) + .Configure(app => app.UseRouting())) + .StartAsync(); + + IOptionsMonitor? options = host.Services.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(initialData, options.CurrentValue.Rules); + + jsonConfig = +@" +{ + ""PerIncomingRequestLogBuffering"": { + ""Rules"": [ + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + config.Reload(); + + var bufferManager = host.Services.GetRequiredService() as PerRequestLogBufferManager; + Assert.NotNull(bufferManager); + Assert.Equivalent(updatedData, bufferManager.Options.CurrentValue.Rules, strict: true); + + await host.StopAsync(); + } } #endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs new file mode 100644 index 00000000000..832c120873a --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs @@ -0,0 +1,37 @@ +// 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.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; + +namespace Microsoft.Extensions.Logging.Test; + +internal class TestConfiguration : JsonConfigurationProvider +{ + private Func _json; + + public TestConfiguration(JsonConfigurationSource source, Func json) + : base(source) + { + _json = json; + } + + public static ConfigurationRoot Create(Func getJson) + { + var provider = new TestConfiguration(new JsonConfigurationSource { Optional = true }, getJson); + return new ConfigurationRoot(new List { provider }); + } + + public override void Load() + { + var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + writer.Write(_json()); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + Load(stream); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index a267c708f50..7a1899f8eeb 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Test; using Microsoft.Extensions.Options; using Xunit; @@ -68,5 +69,79 @@ public void WithConfiguration_RegistersInDI() Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.AutoFlushDuration); // value comes from default Assert.Equivalent(expectedData, options.CurrentValue.Rules); } + + [Fact] + public void WhenConfigUpdated_PicksUpConfigChanges() + { + List initialData = + [ + new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"), + new(logLevel : LogLevel.Information), + ]; + List updatedData = + [ + new(logLevel: LogLevel.Information), + ]; + string jsonConfig = + @" +{ + ""GlobalLogBuffering"": { + ""Rules"": [ + { + ""CategoryName"": ""Program.MyLogger"", + ""LogLevel"": ""Information"", + ""EventId"": 1, + ""EventName"": ""number one"", + }, + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + + using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig); + + using ExtendedLoggerTests.Provider provider = new ExtendedLoggerTests.Provider(); + using ILoggerFactory factory = Utils.CreateLoggerFactory( + builder => + { + builder.AddProvider(provider); + builder.AddGlobalBuffer(config); + }); + ILogger logger = factory.CreateLogger("Program.MyLogger"); + Utils.DisposingLoggerFactory dlf = (Utils.DisposingLoggerFactory)factory; + var bufferManager = dlf.ServiceProvider.GetRequiredService() as GlobalLogBufferManager; + + IOptionsMonitor? options = dlf.ServiceProvider.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(initialData, options.CurrentValue.Rules); + + // this is just to trigger creating an internal buffer: + logger.LogInformation(new EventId(1, "number one"), null); + + jsonConfig = +@" +{ + ""GlobalLogBuffering"": { + ""Rules"": [ + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + config.Reload(); + + Assert.NotNull(bufferManager); + Assert.NotEmpty(bufferManager.Buffers); + foreach (GlobalBuffer buffer in bufferManager.Buffers.Values) + { + Assert.Equivalent(updatedData, buffer.LastKnownGoodFilterRules, strict: true); + } + } } #endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index b31aabb0083..f91cbcabce7 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -1114,7 +1114,7 @@ private enum ThrowExceptionAt IsEnabled } - private sealed class Provider : ILoggerProvider + internal sealed class Provider : ILoggerProvider { public FakeLogger? Logger { get; private set; } From ec4a0412d5e31cce8a326984abb1aba6f436781e Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Tue, 13 May 2025 07:28:32 -0700 Subject: [PATCH 102/472] Remove unused select param (#6341) CreateRecordsForDocumentAsync includes `Select((pair, index) =>` but index is never used Co-authored-by: Jeff Handley --- .../Services/Ingestion/PDFDirectorySource.cs | 2 +- .../aichatweb/Services/Ingestion/PDFDirectorySource.cs | 2 +- .../aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs | 2 +- .../aichatweb/Services/Ingestion/PDFDirectorySource.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs index 7970123b73d..f52cd86c6e6 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs @@ -53,7 +53,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk { #if (UseQdrant) Key = Guid.CreateVersion7(), diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 2ede5add217..39a079e76fe 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -49,7 +49,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index 2bd7a97bd7b..01c370d9dec 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -49,7 +49,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 2ede5add217..39a079e76fe 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -49,7 +49,7 @@ public async Task> CreateChunksForDocumentAsync(IEmbe var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - return paragraphs.Zip(embeddings).Select((pair, index) => new IngestedChunk + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, From dec7e30ea76ca8d0ffb18219bffe36667be1ce94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 13 May 2025 14:22:46 -0500 Subject: [PATCH 103/472] Add RawRepresentationFactory to other options types (#6433) * Add RawRepresentationFactory to other options types * Undo changes in Azure.AI.Inference * Address documentation feedback --- .../ChatCompletion/ChatOptions.cs | 6 +- .../Embeddings/EmbeddingGenerationOptions.cs | 21 +++ .../SpeechToText/SpeechToTextOptions.cs | 21 +++ .../AzureAIInferenceChatClient.cs | 2 +- .../AzureAIInferenceEmbeddingGenerator.cs | 9 +- ...AzureAIInferenceImageEmbeddingGenerator.cs | 9 +- .../OpenAIEmbeddingGenerator.cs | 17 +-- .../OpenAIResponseChatClient.cs | 114 ++++++--------- .../OpenAISpeechToTextClient.cs | 66 +-------- .../AzureAIInferenceChatClientTests.cs | 16 --- ...AzureAIInferenceEmbeddingGeneratorTests.cs | 2 +- ...xtensions.AI.AzureAIInference.Tests.csproj | 6 +- .../OpenAIChatClientTests.cs | 2 +- .../OpenAIEmbeddingGeneratorTests.cs | 72 ++++++++++ .../OpenAIResponseClientTests.cs | 131 ++++++++++++++++++ .../OpenAISpeechToTextClientTests.cs | 34 +++-- 16 files changed, 346 insertions(+), 182 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 6cf99b01821..96a6c6cd36b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -113,7 +113,7 @@ public class ChatOptions public IList? Tools { get; set; } /// - /// Gets or sets a callback responsible of creating the raw representation of the chat options from an underlying implementation. + /// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation. /// /// /// The underlying implementation may have its own representation of options. @@ -124,8 +124,8 @@ public class ChatOptions /// implementation-specific options type may be returned by this callback, for the /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options /// instance further based on other settings supplied on this instance or from other inputs, - /// like the enumerable of s, therefore, its **strongly recommended** to not return shared instances - /// and instead make the callback return a new instance per each call. + /// like the enumerable of s, therefore, it is strongly recommended to not return shared instances + /// and instead make the callback return a new instance on each call. /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed /// properties on . /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs index 4343983c550..b9a13d43dd0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs @@ -1,6 +1,8 @@ // 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.Serialization; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -31,6 +33,25 @@ public int? Dimensions /// Gets or sets additional properties for the embedding generation request. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// + /// Gets or sets a callback responsible for creating the raw representation of the embedding generation options from an underlying implementation. + /// + /// + /// The underlying implementation may have its own representation of options. + /// When + /// is invoked with an , that implementation may convert the provided options into + /// its own representation in order to use it while performing the operation. For situations where a consumer knows + /// which concrete is being used and how it represents options, a new instance of that + /// implementation-specific options type may be returned by this callback, for the + /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options + /// instance further based on other settings supplied on this instance or from other inputs, + /// therefore, it is strongly recommended to not return shared instances and instead make the callback return a new instance on each call. + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// properties on . + /// + [JsonIgnore] + public Func? RawRepresentationFactory { get; set; } + /// Produces a clone of the current instance. /// A clone of the current instance. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index cb196a4c91c..5ff0135cec7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -1,7 +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.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI; @@ -24,6 +26,25 @@ public class SpeechToTextOptions /// Gets or sets any additional properties associated with the options. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// + /// Gets or sets a callback responsible for creating the raw representation of the embedding generation options from an underlying implementation. + /// + /// + /// The underlying implementation may have its own representation of options. + /// When or + /// is invoked with an , that implementation may convert the provided options into + /// its own representation in order to use it while performing the operation. For situations where a consumer knows + /// which concrete is being used and how it represents options, a new instance of that + /// implementation-specific options type may be returned by this callback, for the + /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options + /// instance further based on other settings supplied on this instance or from other inputs, + /// therefore, it is strongly recommended to not return shared instances and instead make the callback return a new instance on each call. + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// properties on . + /// + [JsonIgnore] + public Func? RawRepresentationFactory { get; set; } + /// Produces a clone of the current instance. /// A clone of the current instance. public virtual SpeechToTextOptions Clone() diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 6c0acb8ee23..5c6b64b138f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -289,7 +289,7 @@ private ChatCompletionsOptions CreateAzureAIOptions(IEnumerable cha throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.") }; - /// Converts an extensions options instance to an AzureAI options instance. + /// Converts an extensions options instance to an Azure.AI.Inference options instance. private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatContents, ChatOptions? options) { if (options is null) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs index d96112c73c6..fbaadb2915a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs @@ -91,7 +91,7 @@ public async Task>> GenerateAsync( { _ = Throw.IfNull(values); - var azureAIOptions = ToAzureAIOptions(values, options, EmbeddingEncodingFormat.Base64); + var azureAIOptions = ToAzureAIOptions(values, options); var embeddings = (await _embeddingsClient.EmbedAsync(azureAIOptions, cancellationToken).ConfigureAwait(false)).Value; @@ -164,13 +164,16 @@ static void ThrowInvalidData() => } /// Converts an extensions options instance to an Azure.AI.Inference options instance. - private EmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options, EmbeddingEncodingFormat format) + /// + /// Note: we can't currently use RawRepresentationFactory due to https://github.com/Azure/azure-sdk-for-net/issues/50018. + /// + private EmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) { EmbeddingsOptions result = new(inputs) { Dimensions = options?.Dimensions ?? _dimensions, Model = options?.ModelId ?? _metadata.DefaultModelId, - EncodingFormat = format, + EncodingFormat = EmbeddingEncodingFormat.Base64, }; if (options?.AdditionalProperties is { } props) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs index ec6ee9dd6e6..05ca25bb901 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs @@ -87,7 +87,7 @@ public async Task>> GenerateAsync( { _ = Throw.IfNull(values); - var azureAIOptions = ToAzureAIOptions(values, options, EmbeddingEncodingFormat.Base64); + var azureAIOptions = ToAzureAIOptions(values, options); var embeddings = (await _imageEmbeddingsClient.EmbedAsync(azureAIOptions, cancellationToken).ConfigureAwait(false)).Value; @@ -117,13 +117,16 @@ void IDisposable.Dispose() } /// Converts an extensions options instance to an Azure.AI.Inference options instance. - private ImageEmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options, EmbeddingEncodingFormat format) + /// + /// Note: we can't currently use RawRepresentationFactory due to https://github.com/Azure/azure-sdk-for-net/issues/50018. + /// + private ImageEmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) { ImageEmbeddingsOptions result = new(inputs.Select(dc => new ImageEmbeddingInput(dc.Uri))) { Dimensions = options?.Dimensions ?? _dimensions, Model = options?.ModelId ?? _metadata.DefaultModelId, - EncodingFormat = format, + EncodingFormat = EmbeddingEncodingFormat.Base64, }; if (options?.AdditionalProperties is { } props) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index cf54b7906f0..004f6274265 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -107,21 +107,14 @@ private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, st new(providerName, Uri.TryCreate(providerUrl, UriKind.Absolute, out Uri? providerUri) ? providerUri : null, defaultModelId, defaultModelDimensions); /// Converts an extensions options instance to an OpenAI options instance. - private OpenAI.Embeddings.EmbeddingGenerationOptions? ToOpenAIOptions(EmbeddingGenerationOptions? options) + private OpenAI.Embeddings.EmbeddingGenerationOptions ToOpenAIOptions(EmbeddingGenerationOptions? options) { - OpenAI.Embeddings.EmbeddingGenerationOptions openAIOptions = new() + if (options?.RawRepresentationFactory?.Invoke(this) is not OpenAI.Embeddings.EmbeddingGenerationOptions result) { - Dimensions = options?.Dimensions ?? _dimensions, - }; - - if (options?.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(openAIOptions.EndUserId), out string? endUserId)) - { - openAIOptions.EndUserId = endUserId; - } + result = new OpenAI.Embeddings.EmbeddingGenerationOptions(); } - return openAIOptions; + result.Dimensions ??= options?.Dimensions ?? _dimensions; + return result; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index a91ea9abf8f..25035e7e225 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -18,6 +18,7 @@ #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable S3604 // Member initializer values should not be redundant +#pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; @@ -315,88 +316,59 @@ private static ChatRole ToChatRole(MessageRole? role) => null; /// Converts a to a . - private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? options) + private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? options) { - ResponseCreationOptions result = new(); + if (options is null) + { + return new ResponseCreationOptions(); + } - if (options is not null) + if (options.RawRepresentationFactory?.Invoke(this) is not ResponseCreationOptions result) { - // Handle strongly-typed properties. - result.MaxOutputTokenCount = options.MaxOutputTokens; - result.PreviousResponseId = options.ConversationId; - result.TopP = options.TopP; - result.Temperature = options.Temperature; - result.ParallelToolCallsEnabled = options.AllowMultipleToolCalls; - - // Handle loosely-typed properties from AdditionalProperties. - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId)) - { - result.EndUserId = endUserId; - } + result = new ResponseCreationOptions(); + } - if (additionalProperties.TryGetValue(nameof(result.Instructions), out string? instructions)) - { - result.Instructions = instructions; - } + // Handle strongly-typed properties. + result.MaxOutputTokenCount ??= options.MaxOutputTokens; + result.PreviousResponseId ??= options.ConversationId; + result.TopP ??= options.TopP; + result.Temperature ??= options.Temperature; + result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; - if (additionalProperties.TryGetValue(nameof(result.Metadata), out IDictionary? metadata)) + // Populate tools if there are any. + if (options.Tools is { Count: > 0 } tools) + { + foreach (AITool tool in tools) + { + switch (tool) { - foreach (KeyValuePair kvp in metadata) - { - result.Metadata[kvp.Key] = kvp.Value; - } - } + case AIFunction af: + var oaitool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(af), ResponseClientJsonContext.Default.ResponseToolJson)!; + var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(oaitool, ResponseClientJsonContext.Default.ResponseToolJson)); + result.Tools.Add(ResponseTool.CreateFunctionTool(af.Name, af.Description, functionParameters)); + break; - if (additionalProperties.TryGetValue(nameof(result.ReasoningOptions), out ResponseReasoningOptions? reasoningOptions)) - { - result.ReasoningOptions = reasoningOptions; - } + case HostedWebSearchTool: + WebSearchToolLocation? location = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) + { + location = objLocation as WebSearchToolLocation; + } - if (additionalProperties.TryGetValue(nameof(result.StoredOutputEnabled), out bool storeOutputEnabled)) - { - result.StoredOutputEnabled = storeOutputEnabled; - } + WebSearchToolContextSize? size = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && + objSize is WebSearchToolContextSize) + { + size = (WebSearchToolContextSize)objSize; + } - if (additionalProperties.TryGetValue(nameof(result.TruncationMode), out ResponseTruncationMode truncationMode)) - { - result.TruncationMode = truncationMode; + result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); + break; } } - // Populate tools if there are any. - if (options.Tools is { Count: > 0 } tools) + if (result.ToolChoice is null && result.Tools.Count > 0) { - foreach (AITool tool in tools) - { - switch (tool) - { - case AIFunction af: - var oaitool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(af), ResponseClientJsonContext.Default.ResponseToolJson)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(oaitool, ResponseClientJsonContext.Default.ResponseToolJson)); - result.Tools.Add(ResponseTool.CreateFunctionTool(af.Name, af.Description, functionParameters)); - break; - - case HostedWebSearchTool: - WebSearchToolLocation? location = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) - { - location = objLocation as WebSearchToolLocation; - } - - WebSearchToolContextSize? size = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && - objSize is WebSearchToolContextSize) - { - size = (WebSearchToolContextSize)objSize; - } - - result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); - break; - } - } - switch (options.ToolMode) { case NoneChatToolMode: @@ -415,8 +387,10 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio break; } } + } - // Handle response format. + if (result.TextOptions is null) + { if (options.ResponseFormat is ChatResponseFormatText) { result.TextOptions = new() diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 78fe00a8377..338ff0685f1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -15,6 +15,7 @@ #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; @@ -179,41 +180,14 @@ private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextRespo } /// Converts an extensions options instance to an OpenAI options instance. - private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) + private AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) { - AudioTranscriptionOptions result = new(); - - if (options is not null) + if (options?.RawRepresentationFactory?.Invoke(this) is not AudioTranscriptionOptions result) { - if (options.SpeechLanguage is not null) - { - result.Language = options.SpeechLanguage; - } - - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) - { - result.Temperature = temperature; - } - - if (additionalProperties.TryGetValue(nameof(result.TimestampGranularities), out object? timestampGranularities)) - { - result.TimestampGranularities = timestampGranularities is AudioTimestampGranularities granularities ? granularities : default; - } - - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranscriptionFormat? responseFormat)) - { - result.ResponseFormat = responseFormat; - } - - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - } + result = new AudioTranscriptionOptions(); } + result.Language ??= options?.SpeechLanguage; return result; } @@ -247,32 +221,6 @@ private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextRespons } /// Converts an extensions options instance to an OpenAI options instance. - private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) - { - AudioTranslationOptions result = new(); - - if (options is not null) - { - if (options.AdditionalProperties is { Count: > 0 } additionalProperties) - { - if (additionalProperties.TryGetValue(nameof(result.Temperature), out float? temperature)) - { - result.Temperature = temperature; - } - - if (additionalProperties.TryGetValue(nameof(result.ResponseFormat), out AudioTranslationFormat? responseFormat)) - { - result.ResponseFormat = responseFormat; - } - - if (additionalProperties.TryGetValue(nameof(result.Prompt), out string? prompt)) - { - result.Prompt = prompt; - } - } - } - - return result; - } + private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) + => options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new AudioTranslationOptions(); } - diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 26cd380ec83..489ee13f987 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -327,22 +327,6 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio using IChatClient client = CreateChatClient(httpClient, modelId: null!); AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); - ChatCompletionsOptions azureAIOptions = new() - { - Model = "gpt-4o-mini", - FrequencyPenalty = 0.75f, - MaxTokens = 10, - NucleusSamplingFactor = 0.5f, - PresencePenalty = 0.5f, - Temperature = 0.5f, - Seed = 42, - ToolChoice = ChatCompletionsToolChoice.Auto, - ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() - }; - azureAIOptions.StopSequences.Add("hello"); - azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); - azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); - ChatOptions chatOptions = new ChatOptions { RawRepresentationFactory = (c) => diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs index baee2555990..1a6c60c6ac2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs @@ -29,7 +29,7 @@ public void AsIEmbeddingGenerator_InvalidArgs_Throws() } [Fact] - public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata() + public void AsIEmbeddingGenerator_AzureAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj index a0f9abaf589..0cf9db6ab60 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj @@ -10,7 +10,7 @@ - + @@ -23,11 +23,13 @@ - + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 9ba9c743166..30d03b6eee3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -615,7 +615,7 @@ strictObj is bool strictValue ? } /// Used to create the JSON payload for an OpenAI chat tool description. - private sealed class ChatToolJson + internal sealed class ChatToolJson { [JsonPropertyName("type")] public string Type { get; set; } = "object"; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs index 0caa50935f2..9d8a1219ea7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs @@ -154,4 +154,76 @@ public async Task GetEmbeddingsAsync_ExpectedRequestResponse() Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); } } + + [Fact] + public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation() + { + const string Input = """ + { + "input":["hello, world!","red, white, blue"], + "dimensions":1536, + "model":"text-embedding-3-small", + "encoding_format":"base64", + "user":"MyEndUserID" + } + """; + + const string Output = """ + { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "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" + }, + { + "object": "embedding", + "index": 1, + "embedding": "eyfbu150UDkC6hQ9ip9oPG7jWDw3AOm8DQlcvFiY5Lt3Z6W8BLPPOV0uOz3FlQk8h5AYvH6Aobv0z/E8nOQRvHI8H7rQA+s8F6X9vPplyDzuZ1u8T2cTvAUeoDt0v0Q9/xx5vOhqlT1EgXu8zfQavTK0CDxRxX08v3MIPAY29bzIpFm8bGAzvQkkazxCciu8mjyxvIK0rDx6mzC7Eqg3O8H2rTz9vo482RNiPUYRB7xaQMU80h8hu8kPqrtyPB+8dvxUvfplSD21bJY8oQ8YPZbCEDvxegw9bTJzvYNlEj0h2q+9mw5xPQ5P8TyWwpA7rmvvO2Go27xw2tO6luNqO2pEfTztTwa7KnbRvAbw37vkEU89uKAhPGfvF7u6I8c8DPGGvB1gjzxU2K48+oqDPLCo/zsskoc8PUclvXCUvjzOpQC9qxaKO1iY5LyT9XS9ZNzmvI74Lr03azk93CYTvFJVCTzd+FK8lwgmvcMzPr00q4O9k46FvEx5HbyIqO083xSJvC7PFzy/lOK7HPW+PF2ikDxeAHu9QnIrvSz59rl/UmG8ZNzmu2b4nD3V31Y5aXK9O/2+jrxljUw8y9jkPGuvTTxX5/48u44XPXFFpDwAiEm8lcuVvX6h+zwe7Lm8SUUSPHmkNTu9Eb08cP8OvYgcw7xU2C49Wm4FPeV8H72AA8c7eH/6vBI0Yj3L2GQ8/0G0PHg5ZTvHjAS9fNhAPcE8wzws2By6RWAhvWTcZjz+1uM8H1eKvHdnJT0TWR29KcVrPdu7wrvMQzW9VhW/Ozo09LvFtuM8OlmvPO5GAT3eHY68zTqwvIhiWLs1w1i9sGJqPaurOb0s2Jy8Z++XOwAU9Lggb988vnyNvVfGpLypKBS8IouVO60NBb26r/G6w+0ovbVslrz+kE68MQOjOxdf6DvoRdo8Z4RHPCvhIT3e7009P4Q1PQ0JXDyD8Ty8/ZnTuhu4Lj3X1lG9sVnlvMxDNb3wySY9cUWkPNZKJ73qyP+8rS7fPNhBojwpxes8kt0fPM7rlbwYEE68zoBFvdrExzsMzEu9BflkvF0uu7zNFfW8UyfJPPSJ3LrEBf68+6JYvef/xDpAe7C8f5h2vPqKA7xUTAS9eDllPVK8eL0+GeW7654gPQuGNr3/+x69YajbPAehRTyc5BE8pfQIPMGwGL2QoA87iGJYPYXoN7s4sc69f1JhPdYEkjxgkIa6uxpCvHtMljtYvR88uCzMPBeEo7wm1/U8GBDOvBkHybwyG3i7aeaSvQzMyzy3e2a9xZUJvVSSmTu7SII8x4yEPKAYHTxUTIQ8lcsVO5x5QT3VDRe963llO4K0rLqI1i07DX0xvQv6CznrniA9nL9WPTvl2Tw6WS+8NcPYvEL+VbzZfrK9NDcuO4wBNL0jXVW980PHvNZKJz1Oti09StG8vIZTiDwu8PE8zP0fO9340juv1j890vFgvMFqAz2kHui7PNxUPQehxTzjGlQ9vcunPL+U4jyfrUw8R+NGPHQF2jtSdmO8mYtLvF50ULyT1Bo9ONaJPC1kx7woznC83xQJvUdv8byEXA29keaku6Qe6Ly+fA29kKAPOxLuzLxjxJG9JnCGur58jTws2Jy8CkmmO3pVm7uwqH87Eu7Mu/SJXL0IUis9MFI9vGnmEr1Oti09Z+8XvH1DkbwcaZS8NDcuvT0BkLyPNT89Haakuza607wv5+w81KLGO80VdT3MiUq8J4hbPHHRzrwr4aG8PSJqvJOOBT3t2zC8eBgLvXchkLymOp66y9jkPDdG/jw2ulO983GHPDvl2Tt+Ooy9NwDpOzZ0Pr3xegw7bhGZvEpd57s5YjS9Gk1evIbfMjxBwcW8NnQ+PMlVPzxR6ji9M8zdPImHk7wQsby8u0gCPXtMFr22YxE9Wm4FPaXPzbygGJ093bK9OuYtBTxyXfk8iYeTvNH65byk/Q29QO+FvKbGyLxCcqs9nL/WvPtcQ72XTjs8kt2fuhaNKDxqRH08KX9WPbmXnDtXDDo96GoVPVw3QL0eeGS8ayOjvAIL7zywQZC9at0NvUMjET1Q8707eTDgvIio7Tv60Jg87kYBOw50LLx7BgE96qclPUXsSz0nQkY5aDUtvQF/RD1bZQC73fjSPHgYCzyPNT+9q315vbMvhjsvodc8tEdbPGcQ8jz8U768cYs5PIwBtL38x5M9PtPPvIex8jzfFIk9vsIivLsaQj2/uZ072y8YvSV5C7uoA9k8JA67PO5nWzvS8eC8av7nuxSWrbybpwE9f5h2vG3sXTmoA1k9sjiLvTBSPbxc8Sq9UpuePB+dHz2/cwg9BWS1vCrqJr2M3Pg86LAqPS/GEj3oRdq8GiyEvACISbuiJ+28FFAYuzBSvTzwDzy8K5uMvE5wmDpd6CW6dkJqPGlyvTwF2Iq9f1JhPSHarzwDdr88JXkLu4ADxzx5pDW7zqUAvdAoJj24wXs8doj/PH46jD2/2vc893fSuyxtTL0YnPg7IWbaPOiwqrxLDk27ZxDyPBpymbwW0z08M/odPTufRL1AVvU849Q+vBGDfD3JDyq6Z6kCPL9OzTz0rpe8FtM9vaDqXLx+W2Y7jHWJPGXT4TwJ3lW9M4bIPPCDkTwoZwE9XH1VOmksqLxLPI08cNrTvCyz4bz+Srm8kiO1vDP6nbvIpNk8MrSIvPe95zoTWR29SYsnPYC9MT2F6De93qm4PCbX9bqqhv47yky6PENE67x/DEw8JdYAvUdvcbywh6W8//ueO8fSmTyjTCi9yky6O/qr3TzvGEE8wqcTPeDmSDyuJVo8ip/ou1HqOLxOtq28y5LPuxk1Cb0Ddr+7c+2EvKQeaL1SVQk8XS47PGTcZjwdpiQ8uFqMO0QaDD1XxqS8mLmLuuSFJDz1xmy8PvgKvJAHf7yC+kE8VapuvetYC7tHCAI8oidtPOiwqjyoSW68xCo5vfzobTzz2HY88/0xPNkT4rty9om8RexLu9SiRrsVaG081gSSO5IjtTsOLpc72sTHPGCQBj0QJRI9BCclPI1sBDzCyO07QHuwvOYthTz4tGK5QHuwvWfvFz2CQNc8PviKPO8YwTuQoA89fjoMPBnBs7zGZ8m8uiPHvMdeRLx+gKE8keaku0wziDzZWfe8I4KQPJ0qpzs4sc47dyEQPEQaDDzVmcE8//uePJcIJjztTwa9ogaTOftcwztU2K48opvCuyz5drzqM1C7iYcTvfDJJjxXxiQ9o0wovO1PBrwqvGa7dSoVPbI4izvnuS88zzGrPH3POzzHXkQ9PSJqOXCUPryW4+o8ELE8PNZKp7z+Sjm8foChPPIGtzyTaUq8JA47vBiceDw3a7m6jWyEOmksKDwH59q5GMo4veALBL0SqDe7IaxvvBD3Ubxn7xc9+dkdPSBOBTxHCAI8mYvLOydCxjw5HB88zTqwvJXs77w9AZA9CxvmvIeQGL2rffm8JXkLPKqGfjyoSe464d1DPPd3UrpO/EK8qxYKvUuCojwhZlq8EPfRPKaAs7xKF9K85i0FvEYRhzyPNT88m6cBvdSiRjxnqQI9uOY2vcBFSLx4OeW7BxUbPCz59rt+W2Y7SWZsPGzUCLzE5KM7sIclvIdr3buoSW47AK0EPImHE7wgToU8IdovO7FZ5bxbzO+8uMF7PGayB7z6ioO8zzErPEcIgrxSm568FJYtvNf7jDyrffm8KaQRPcoGpTwleQu8EWKiPHPthLz44qI8pEOjvWh7QjzpPNU8lcuVPHCUPr3n/8Q8bNQIu0WmNr1Erzs95VfkPCeIW7vT0Aa7656gudH65bxw/w49ZrKHPHsn27sIUiu8mEU2vdUNF7wBf8Q809CGPFtlgDo1fcO85i2FPEcIAjwL+os653OavOu1AL2EN9K8H52fPKzoybuMdYk8T2cTO8lVPzyK5X07iNYtvD74ijzT0IY8RIF7vLLENbyZi8s8KwJ8vAne1TvGZ8k71gSSumJZwTybp4G8656gPG8IFL27SAI9arjSvKVbeDxljcy83fjSuxu4Lr2DZRK9G0TZvLFZ5bxR6ji8NPEYPbI4izyAvTE9riVaPCCUGrw0Ny48f1LhuzIb+DolBTY8UH9ou/4EpLyAvTG9CFIrvCBOBTlkIvy8WJhkvHIXZLkf47Q8GQfJvBpNXr1pcr07c8jJO2nmkrxOcJi8sy8GuzjWibu2Pta8WQO1PFPhs7z7XEO8pEMjvb9OzTz4bs08EWKiu0YyYbzeHQ695D+PPKVbeDzvGEG9B6HFO0uCojws+Xa7JQW2OpRgRbxjCqc8Sw7NPDTxmLwjXVW8sRNQvFPhszzM/Z88rVMavZPUGj06WS+8JpHgO3etursdx369uZccvKplJDws+Xa8fzGHPB1gj7yqZaQ887ecPBNZHbzoi2+7NwDpPMxDtbzfWh49H+O0PO+kaztI2kE8/xz5PImHE73fNWO8T60ovIPxPDvR2Yu8XH3VvMcYr7wfnR+9fUORPIdr3Tyn6wO9nkL8vM2uhTzGIbS66u26vE2/MrxFYKE8iwo5vLSNcLy+wiK9GTUJPK10dLzrniC8qkBpvPxTPrwzQLO8illTvFi9H7yMATS7ayOjO14Ae7z19Cy87dswPKbGyDzujJa93EdtPdsB2LYT5Ue9RhEHPKurubxm+By9+mVIvIy7HrxZj987yOpuvUdv8TvgCwS8TDMIO9xsqLsL+gs8BWS1PFRMBD1yXXm86GoVvK+QqjxRXg46TZHyu2ayhzx7TJa8uKAhPLyFkjsV3MI7niGiPGNQvDxgkIa887ccPUmLJ7yZsIa8KDnBvHgYi7yMR0m82ukCvRuK7junUvO8aeYSPXtt8LqXCKa84kgUPd5jIzxlRze93xQJPNNcMT2v1j889GiCPKRkfbxz7YQ8b06pO8cYL7xg9/U8yQ+qPGlyvbzfNWO8vZ3nPBGD/DtB5gC7yKRZPPTPcbz6q928bleuPI74rrzVDRe9CQORvMmb1Dzv0qs8DBLhu4dr3bta1fQ8aeYSvRD3UTugpMe8CxvmPP9BNDzHjAQ742DpOzXD2Dz4bk28c1T0Onxka7zEBf48uiNHvGayBz1pcj29NcPYvDnu3jz5kwg9WkBFvL58jTx/mHY8wTzDPDZ0Pru/uZ08PQGQPOFRmby4oKE8JktLPIx1iTsppBG9dyGQvHfzT7wzhki44KAzPSOCkDzv0iu8lGBFO2VHNzyKxKM72EEiPYtQzryT9fQ8UDnTPEx5nTzuZ9s8QO8FvG8IlDx7J9s6MUk4O9k4nbx7TBa7G7iuvCzYHDocr6k8/7UJPY2ymTwVIlg8KjC8OvSuFz2iJ+28cCBpvE0qAzw41ok7sgrLvPjiojyG37K6lwimvKcxGTwRHI28y5LPO/mTiDx82MC5VJIZPWkH7TwPusG8YhOsvH1DkbzUx4E8TQXIvO+ka7zKwI+8w+2oPNLxYLzxegy9zEM1PDo0dDxIINc8FdxCO46E2TwPRmw9+ooDvMmb1LwBf0S8CQMRvEXsS7zPvdU80qvLPLfvO7wbuK68iBzDO0cpXL2WndU7dXCqvOTLubytLl88LokCvZj/IDw0q4M8G7guvNkTYrq5UQe7vcunvIrEI7xuERm9RexLvAdbsDwLQCE7uVEHPYjWrbuM3Pi8g2WSO3R5L7x4XiC8vKZsu9Sixros+fa8UH/ouxxpFL3wyaa72sRHu2YZ9zuiJ2274o4pOjkcnzyagka7za4FvYrEozwCMCo7cJQ+vfqKAzzJ4em8fNhAPUB7sLylz80833v4vOU2ir1ty4M8UV4OPXQF2jyu30S9EjRivBVo7TwXX2g70ANrvEJyq7wQJRK99jE9O7c10brUxwE9SUUSPS4VLbzBsJg7FHHyPMz9n7latJo8bleuvBpN3jsF+WS8Ye7wO4nNKL0TWZ08iRM+vOn2v7sB8xm9jY3ePJ/zYbkLG+a7ZvicvGxgM73L2OS761iLPKcxmTrX+ww8J0JGu1MnyTtJZuw7pIm4PJbCED29V1K9PFCqPLBBkLxhYka8hXTiPEB7MDzrniA7h5CYvIR9ZzzARcg7TZHyu4sKOb1in9Y7nL9WO6gD2TxSduO8UaQjPQO81Lxw/w69KwL8O4FJ3D2XTju8SE6XPGDWGz0K1VC8YhMsvObCtDyndy49BCclu68cVbxemYu8sGLqOksOzTzj1L47ISBFvLly4Ttk3Oa8RhGHNwzxBj0v5+y7ogaTPA+6QbxiE6w8ubj2PDixzrstZEe9jbKZPPd30rwqMDw8TQXIPFurlTxx0c68jLsePfSJ3LuXTru8yeHpu6Ewcjx5D4a8BvBfvN8Uibs9R6W8lsIQvaEw8rvVUyw8SJQsPebCNDwu8PE8GMo4OxAlkjwJmMA8KaQRvdYlbDwNNxy9ouHXPDffDrxwZv46AK0EPJqCRrpWz6k8/0E0POAs3rxmsoe7zTqwO5mLyzyP7ym7wTzDvFB/aLx5D4a7doj/O67fxDtsO/g7uq9xvMWViTtC/tU7PhnlvIEogjxxRSQ9SJSsPIJA1zyBKAI9ockCPYC9MbxBTXC83xSJvPFVUb1n75c8uiNHOxdf6Drt27A8/FM+vJOvXz3a6QI8UaQjuvqKgzyOhNm831oevF+xYLxjCic8sn6gPDdrOTs3Rv66cP+Ou5785rycBew8J0JGPJOOBbw9Imq8q335O3MOX7xemQs8PtNPPE1L3Tx5dnU4A+EPPLrdsTzfFIm7LJIHPB4yz7zbAdi8FWjtu1h3Cj0oznA8kv55PKgDWbxIINc8xdsePa8cVbzmlHQ8IJSavAgMlrx4XiA8z3dAu2PEET3xm+a75//EvK2Zr7xbqxU8zP2fvOSFJD1xRSS7k44FvPzHkzz5+ne8+tAYvd5jIz1GMuE8yxSAO3KCNDyRuOS8wzO+vObCNDwzQLO7isQjva1TGrz6ioM79GgCPF66Zbx1KpW8qW6pu4RcDTzcJhO9SJQsO5G45LsAiMm8lRErvJqCxjzQbju7w3nTuTclpDywqP88ysCPvAF/xLxfa0u88cChPBjKODyaPLE8k69fvGFiRrvuRgG9ATmvvJEsOr21+EC9KX/WOrmXnDwDAuo8yky6PI1sBDvztxy8PviKPKInbbzbdS276mGQO2Kf1rwn/DC8ZrIHPBRxcj0z+h264d1DPdG0ULxvTqm5bDt4vToTmjuGJcg7tmMRO9YEEr3oJAC9THmdPKn607vcJhM8Zj6yvHR5r7ywYmq83fjSO5mLyzshIEU8EWKiuu9eVjw75dk7fzGHvNl+sjwJJOs8YllBPAtheztz7QQ92lDyvDEDozzEKrk7KnZRvG8pbjsdYI+7yky6OfWAVzzjYGk7NX3DOzrNhDyeIaI8joTZvFcMOryYRba8G7iuu893QDw9RyW7za6FvDUJ7rva6YK9D7rBPD1o/zxCLJa65TaKvHsGAT2g6ly8+tCYu+wqy7xeAHu8vZ1nPBv+QzwfVwo8CMYAvM+91TzKTDq8Ueo4u2uvzTsBf8Q8p+uDvKofDz12tj+8wP+yOlkDtTwYyji6ZdPhPGv14rwqdtE8YPf1vLIKy7yFLs28ouFXvO1PBj15pDU83xQJPdfWUTz8x5O64kgUPBQKA72eIaK6A3a/OyzYnLoYnPg4XMNqPdxsqLsKSaY7pfSIvBoshLupKJS8G0TZOu/SqzzFcE47cvaJPA19Mb14dQC8sVllvJmwhjycBey8cvaJOmSWUbvRtFC8WtX0O2r+57twIGm8yeFpvFuG2rzCyO08PUelPK5rbzouFS29uCxMPQAUdDqtma88wqeTu5gge7zH8/O7l067PJdOO7uKxCO8/xx5vKt9+TztTwa8OhOaO+Q/Dzw33w49CZhAvSubjDydttG8IdovPIADR7stHrI7ATmvvOAs3rzL2OQ69K4XvNccZ7zlV2S8c+0EPfNDxzydKqc6LLPhO8YhtDyJhxM9H1eKOaNMKLtOcBg9HPU+PTsrbzvT0Ia8BG26PB2mpDp7TJa8wP8yPVvM77t0ea86eTBgvFurFT1C/tW7CkkmvKOSPT2aPDG9lGDFPAhSq7u5UYc8l5TQPFh3ijz9vg68lGBFO4/vKTxViZS7eQ8GPTNAs7xmsoe8o0yoPJfaZbwlvyA8IazvO0XsS717TJY8flvmOgHFWbyWnVW8mdFgvJbCkDynDF68" + } + ], + "model": "text-embedding-3-small", + "usage": { + "prompt_tokens": 9, + "total_tokens": 9 + } + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions + { + Transport = new HttpClientPipelineTransport(httpClient), + }).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); + + var response = await generator.GenerateAsync([ + "hello, world!", + "red, white, blue", + ], new EmbeddingGenerationOptions + { + Dimensions = 3072, + RawRepresentationFactory = (e) => new OpenAI.Embeddings.EmbeddingGenerationOptions + { + Dimensions = 1536, + EndUserId = "MyEndUserID" + } + }); + + Assert.NotNull(response); + Assert.Equal(2, response.Count); + + Assert.NotNull(response.Usage); + Assert.Equal(9, response.Usage.InputTokenCount); + Assert.Equal(9, response.Usage.TotalTokenCount); + + foreach (Embedding e in response) + { + Assert.Equal("text-embedding-3-small", e.ModelId); + Assert.NotNull(e.CreatedAt); + Assert.Equal(1536, e.Vector.Length); + Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 8e4229937ee..3747b79dc88 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -5,8 +5,10 @@ using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; @@ -281,6 +283,135 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal(36, usage.Details.TotalTokenCount); } + [Fact] + public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() + { + const string Input = """ + { + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}], + "model":"gpt-4o-mini", + "max_output_tokens":10, + "previous_response_id":"resp_42", + "top_p":0.5, + "temperature":0.5, + "parallel_tool_calls":true, + "text": {"format": {"type": "text"} + }, + "tool_choice":"auto", + "tools":[ + {"description":"Gets the age of the specified person.","name":"GetPersonAge","parameters":{"additionalProperties":false,"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"required":["personName"],"type":"object"},"strict":false,"type":"function"}, + {"description":"Gets the age of the specified person.","name":"GetPersonAge","parameters":{"additionalProperties":false,"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"required":["personName"],"type":"object"},"strict":false,"type":"function"} + ] + } + """; + + const string Output = """ + { + "id": "resp_67d327649b288191aeb46a824e49dc40058a5e08c46a181d", + "object": "response", + "created_at": 1741891428, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 20, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "type": "message", + "id": "msg_67d32764fcdc8191bcf2e444d4088804058a5e08c46a181d", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello! How can I assist you today?", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "generate_summary": null + }, + "store": true, + "temperature": 0.5, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "usage": { + "input_tokens": 26, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 10, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 36 + }, + "user": null, + "metadata": {} + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, modelId: "gpt-4o-mini"); + AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); + + ChatOptions chatOptions = new() + { + RawRepresentationFactory = (c) => + { + ResponseCreationOptions openAIOptions = new() + { + MaxOutputTokenCount = 10, + PreviousResponseId = "resp_42", + TopP = 0.5f, + Temperature = 0.5f, + ParallelToolCallsEnabled = true, + ToolChoice = ResponseToolChoice.CreateAutoChoice(), + TextOptions = new ResponseTextOptions + { + TextFormat = ResponseTextFormat.CreateTextFormat() + }, + }; + openAIOptions.Tools.Add(ToOpenAIResponseChatTool(tool)); + return openAIOptions; + }, + ModelId = null, + MaxOutputTokens = 1, + ConversationId = "foo", + TopP = 0.125f, + Temperature = 0.125f, + AllowMultipleToolCalls = false, + Tools = [tool], + ToolMode = ChatToolMode.None, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await client.GetResponseAsync("hello", chatOptions); + Assert.NotNull(response); + Assert.Equal("Hello! How can I assist you today?", response.Text); + } + + /// Converts an Extensions function to an OpenAI response chat tool. + private static ResponseTool ToOpenAIResponseChatTool(AIFunction aiFunction) + { + var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema)!; + var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); + return ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters); + } + private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) => new OpenAIClient( new ApiKeyCredential("apikey"), diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 4587c3a5524..c92d9627968 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -211,11 +211,12 @@ public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() } [Fact] - public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() + public async Task GetTextAsync_Transcription_StronglyTypedOptions_AllSent() { const string Input = """ { "model": "whisper-1", + "language": "pt", "prompt":"Hide any bad words with ", "temperature": 0.5, "response_format": "vtt", @@ -236,24 +237,27 @@ public async Task GetTextAsync_NonStronglyTypedOptions_AllSent() using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { - AdditionalProperties = new() + SpeechLanguage = "en", + RawRepresentationFactory = (s) => + new AudioTranscriptionOptions { - ["Prompt"] = "Hide any bad words with ", - ["SpeechLanguage"] = "pt", - ["Temperature"] = 0.5f, - ["TimestampGranularities"] = AudioTimestampGranularities.Segment | AudioTimestampGranularities.Word, - ["ResponseFormat"] = AudioTranscriptionFormat.Vtt, - }, + Prompt = "Hide any bad words with ", + Language = "pt", + Temperature = 0.5f, + TimestampGranularities = AudioTimestampGranularities.Segment | AudioTimestampGranularities.Word, + ResponseFormat = AudioTranscriptionFormat.Vtt + } })); } [Fact] - public async Task GetTextAsync_StronglyTypedOptions_AllSent() + public async Task GetTextAsync_Translation_StronglyTypedOptions_AllSent() { const string Input = """ { "model": "whisper-1", - "language": "pt" + "prompt":"Hide any bad words with ", + "response_format": "vtt" } """; @@ -270,7 +274,15 @@ public async Task GetTextAsync_StronglyTypedOptions_AllSent() using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() { - SpeechLanguage = "pt", + SpeechLanguage = null, + TextLanguage = "pt", + RawRepresentationFactory = (s) => + new AudioTranslationOptions + { + Prompt = "Hide any bad words with ", + Temperature = 0.5f, // Temperature is ignored by OpenAI. + ResponseFormat = AudioTranslationFormat.Vtt + } })); } From c2f7b3867ccd03a54a89e4a08af7b8ccfc9ff913 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Wed, 14 May 2025 00:19:48 -0700 Subject: [PATCH 104/472] Update MEAI Template test snapshots --- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 2 +- .../aichatweb/aichatweb.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 4b1fad034a9..78946f3f313 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index fd7131e492a..09532951c55 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -10,7 +10,7 @@ - + From 5aab00ef6abd50313bc5150a6699150cd2d2927d Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Wed, 14 May 2025 00:47:42 -0700 Subject: [PATCH 105/472] Pin the non-AI package versions for the MEAI Templates --- src/ProjectTemplates/GeneratedContent.targets | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 832d533e66a..5842d5cad74 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -15,12 +15,12 @@ - 9.4.0 - 9.4.0-preview.1.25207.5 - 9.0.4 + 9.5.0 + 9.5.0-preview.1.25262.9 + 9.0.5 - false + true false From 8ea542cd62b539767e37803496f6c39a4a7a51cb Mon Sep 17 00:00:00 2001 From: Shyam N Date: Wed, 14 May 2025 04:16:15 -0700 Subject: [PATCH 106/472] Remove unused API in Safety package (#6439) This change ensures that we will have just one way to create a ChatConfiguration for the Safety evaluators. The removed API was also confusing because it could mislead callers into thinking that the returned IChatClient can be used like any other general purpose LLM endpoint when in fact it is pretty specialized and only intended for internal use within the Safety evaluators. --- ...entSafetyServiceConfigurationExtensions.cs | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs index eded31ec0f8..5ada749b627 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs @@ -40,37 +40,12 @@ public static ChatConfiguration ToChatConfiguration( #pragma warning disable CA2000 // Dispose objects before they go out of scope. // We can't dispose newChatClient here because it is returned to the caller. - var newChatClient = contentSafetyServiceConfiguration.ToIChatClient(originalChatConfiguration?.ChatClient); + var newChatClient = + new ContentSafetyChatClient( + contentSafetyServiceConfiguration, + originalChatClient: originalChatConfiguration?.ChatClient); #pragma warning restore CA2000 return new ChatConfiguration(newChatClient); } - - /// - /// Returns an that can be used to communicate with the Azure AI Foundry Evaluation - /// service for performing content safety evaluations. - /// - /// - /// An object that specifies configuration parameters such as the Azure AI project that should be used, and the - /// credentials that should be used, when communicating with the Azure AI Foundry Evaluation service to perform - /// content safety evaluations. - /// - /// - /// The original , if any. If specified, the returned - /// will be a wrapper around that can be used both - /// to communicate with the AI model that is configured to communicate with, - /// as well as to communicate with the Azure AI Foundry Evaluation service. - /// - /// - /// A that can be used to communicate with the Azure AI Foundry Evaluation service - /// for performing content safety evaluations. - /// - public static IChatClient ToIChatClient( - this ContentSafetyServiceConfiguration contentSafetyServiceConfiguration, - IChatClient? originalChatClient = null) - { - _ = Throw.IfNull(contentSafetyServiceConfiguration); - - return new ContentSafetyChatClient(contentSafetyServiceConfiguration, originalChatClient); - } } From b75358d5c20c9838c5096694b70b8a7a9990ad86 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 14 May 2025 21:40:56 +0300 Subject: [PATCH 107/472] Ensure the type keyword is included when generating schemas for nullable enums. (#6440) --- .../Utilities/AIJsonUtilities.Schema.Create.cs | 6 ++++++ .../Utilities/AIJsonUtilitiesTests.cs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index a44836d8e96..ad4b897cef2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -274,6 +274,12 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js objSchema.InsertAtStart(TypePropertyName, "string"); } + // Include the type keyword in nullable enum types + if (Nullable.GetUnderlyingType(ctx.TypeInfo.Type)?.IsEnum is true && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName)) + { + objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" }); + } + // Filter potentially disallowed keywords. foreach (string keyword in _schemaKeywordsDisallowedByAIVendors) { 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 0001b8b2125..6df42c22670 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -477,6 +477,20 @@ public static void CreateJsonSchema_AcceptsOptionsWithoutResolver() Assert.Same(options.TypeInfoResolver, AIJsonUtilities.DefaultOptions.TypeInfoResolver); } + [Fact] + public static void CreateJsonSchema_NullableEnum_IncludesTypeKeyword() + { + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "type": ["string", "null"], + "enum": ["A", "B", null] + } + """).RootElement; + + JsonElement schema = AIJsonUtilities.CreateJsonSchema(typeof(MyEnumValue?), serializerOptions: JsonContext.Default.Options); + AssertDeepEquals(expectedSchema, schema); + } + [Fact] public static void AddAIContentType_DerivedAIContent() { @@ -846,6 +860,7 @@ private class DerivedAIContent : AIContent [JsonSerializable(typeof(DerivedAIContent))] [JsonSerializable(typeof(MyPoco))] [JsonSerializable(typeof(PocoWithTypesWithOpenAIUnsupportedKeywords))] + [JsonSerializable(typeof(MyEnumValue?))] private partial class JsonContext : JsonSerializerContext; private static bool DeepEquals(JsonElement element1, JsonElement element2) From 53093ea59438dbc731a0b4230051cee5354b181a Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Wed, 14 May 2025 11:47:59 -0700 Subject: [PATCH 108/472] Remove obsolete members from AIJsonSchemaCreateOptions (#6432) --- .../Utilities/AIJsonSchemaCreateOptions.cs | 21 ------------------- .../Utilities/AIJsonUtilitiesTests.cs | 5 ----- 2 files changed, 26 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs index 8c53938f481..667b8fee475 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs @@ -42,29 +42,8 @@ public sealed record class AIJsonSchemaCreateOptions /// public AIJsonSchemaTransformOptions? TransformOptions { get; init; } - /// - /// Gets a value indicating whether to include the type keyword in created schemas for .NET enums. - /// - [Obsolete("This property has been deprecated.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public bool IncludeTypeInEnumSchemas { get; init; } = true; - - /// - /// Gets a value indicating whether to generate schemas with the additionalProperties set to false for .NET objects. - /// - [Obsolete("This property has been deprecated. Use the equivalent property in TransformOptions instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public bool DisallowAdditionalProperties { get; init; } - /// /// Gets a value indicating whether to include the $schema keyword in created schemas. /// public bool IncludeSchemaKeyword { get; init; } - - /// - /// Gets a value indicating whether to mark all properties as required in the schema. - /// - [Obsolete("This property has been deprecated. Use the equivalent property in TransformOptions instead.")] - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public bool RequireAllProperties { get; init; } } 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 6df42c22670..c67a6147186 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -15,8 +15,6 @@ using Microsoft.Extensions.AI.JsonSchemaExporter; using Xunit; -#pragma warning disable 0618 // Suppress obsolete warnings - namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilitiesTests @@ -73,10 +71,7 @@ public static void DefaultOptions_UsesReflectionWhenDefault() public static void AIJsonSchemaCreateOptions_DefaultInstance_ReturnsExpectedValues(bool useSingleton) { AIJsonSchemaCreateOptions options = useSingleton ? AIJsonSchemaCreateOptions.Default : new AIJsonSchemaCreateOptions(); - Assert.True(options.IncludeTypeInEnumSchemas); - Assert.False(options.DisallowAdditionalProperties); Assert.False(options.IncludeSchemaKeyword); - Assert.False(options.RequireAllProperties); Assert.Null(options.TransformSchemaNode); Assert.Null(options.TransformOptions); } From 28d99675e6f6d7e4dd2bf74680c6476cf76f32d2 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Wed, 14 May 2025 21:51:18 +0200 Subject: [PATCH 109/472] Reduce per-lookup overhead from key validation in HybridCache (#6441) * Reduce per-lookup overhead from key validation in HybridCache * Might as well be static --- .../Internal/DefaultHybridCache.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs index 7294e1daf84..84de2fe52e8 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs @@ -26,9 +26,6 @@ internal sealed partial class DefaultHybridCache : HybridCache { internal const int DefaultExpirationMinutes = 5; - // reserve non-printable characters from keys, to prevent potential L2 abuse - private static readonly char[] _keyReservedCharacters = Enumerable.Range(0, 32).Select(i => (char)i).ToArray(); - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0032:Use auto property", Justification = "Keep usage explicit")] private readonly IDistributedCache? _backendCache; [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0032:Use auto property", Justification = "Keep usage explicit")] @@ -255,6 +252,26 @@ private static ValueTask RunWithoutCacheAsync(HybridCacheEntryFlag return null; } + // reserve non-printable characters from keys, to prevent potential L2 abuse + private static bool ContainsReservedCharacters(ReadOnlySpan key) + { + const char MaxControlChar = (char)31; + +#if NET8_0_OR_GREATER + return key.IndexOfAnyInRange((char)0, MaxControlChar) >= 0; +#else + foreach (char c in key) + { + if (c <= MaxControlChar) + { + return true; + } + } + + return false; +#endif + } + private bool ValidateKey(string key) { if (string.IsNullOrWhiteSpace(key)) @@ -269,7 +286,7 @@ private bool ValidateKey(string key) return false; } - if (key.IndexOfAny(_keyReservedCharacters) >= 0) + if (ContainsReservedCharacters(key.AsSpan())) { _logger.KeyInvalidContent(); return false; From 1a32f54ae6a1de179ff104cab27e573318dfa1fb Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 22:01:00 +0000 Subject: [PATCH 110/472] Update dependencies from https://github.com/dotnet/arcade build 20250513.5 (#6443) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- global.json | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 79b661b0db6..a8f9817fce4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - bfbc858ba868b60fffaf7b2150f1d2165b01e786 + 93823d49ca01742464ad1c0b49ea940e693b1be3 - + https://github.com/dotnet/arcade - bfbc858ba868b60fffaf7b2150f1d2165b01e786 + 93823d49ca01742464ad1c0b49ea940e693b1be3 - + https://github.com/dotnet/arcade - bfbc858ba868b60fffaf7b2150f1d2165b01e786 + 93823d49ca01742464ad1c0b49ea940e693b1be3 diff --git a/eng/Versions.props b/eng/Versions.props index a643405a9a9..33b9eb6a957 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,7 +84,7 @@ 9.0.4 - 9.0.0-beta.25225.6 + 9.0.0-beta.25263.5 diff --git a/global.json b/global.json index 6dc143e2fb7..0df2af37e50 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "9.0.105" + "version": "9.0.106" }, "tools": { - "dotnet": "9.0.105", + "dotnet": "9.0.106", "runtimes": { "dotnet": [ "8.0.0", @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25225.6", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25225.6" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25263.5", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25263.5" } } From 2138e624b695d682659a27495d11d90e3fecd8d9 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Wed, 14 May 2025 20:05:54 -0700 Subject: [PATCH 111/472] [9.5] Remove unused API in Safety package (#6442) This change ensures that we will have just one way to create a ChatConfiguration for the Safety evaluators. The removed API was also confusing because it could mislead callers into thinking that the returned IChatClient can be used like any other general purpose LLM endpoint when in fact it is pretty specialized and only intended for internal use within the Safety evaluators. From a73d3ea80fd7b5b95c991f2b1feaee7502dfd309 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Thu, 15 May 2025 04:02:33 -0700 Subject: [PATCH 112/472] Couple of fixes for safety evaluators (#6444) Includes following changes - **Add a convenience overload:** Currently, if a caller wants to run evaluations that involve both safety and quality evals, they need to create one ChatConfiguration for the LLM interactions and then wrap it with another ChatConfiguration for the AI Foundry service (by calling the existing overload for ToChatConfiguration). The new overload makes it so that caller only needs to create a IChatClient for the LLM interactions needed for the quality evals. They can then directly call ToChatConfiguration with this IChatClient to get a single Chatconfiguration (which supports both safety and quality evals) and supply this ChatConfiguration as part of the ReportingConfiguration. **Improve an error message:** The NotSupportedException being thrown before was pretty opaque. Improve the error so that the reason is a bit clearer. --- .../ContentSafetyChatClient.cs | 33 ++++++++++++------ ...entSafetyServiceConfigurationExtensions.cs | 34 +++++++++++++++++++ .../SafetyEvaluatorTests.cs | 24 ++++++------- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs index 69b47670935..347975cb695 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -79,17 +80,15 @@ await _service.AnnotateAsync( ModelId = Moniker }; } - else if (_originalChatClient is not null) + else { + ValidateOriginalChatClientNotNull(); + return await _originalChatClient.GetResponseAsync( messages, options, cancellationToken).ConfigureAwait(false); } - else - { - throw new NotSupportedException(); - } } public async IAsyncEnumerable GetStreamingResponseAsync( @@ -114,8 +113,10 @@ await _service.AnnotateAsync( ModelId = Moniker }; } - else if (_originalChatClient is not null) + else { + ValidateOriginalChatClientNotNull(); + await foreach (var update in _originalChatClient.GetStreamingResponseAsync( messages, @@ -125,10 +126,6 @@ await _service.AnnotateAsync( yield return update; } } - else - { - throw new NotSupportedException(); - } } public object? GetService(Type serviceType, object? serviceKey = null) @@ -171,4 +168,20 @@ private static void ValidateSingleMessage(IEnumerable messages) Throw.ArgumentException(nameof(messages), ErrorMessage); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] // Inline if possible. + [MemberNotNull(nameof(_originalChatClient))] + private void ValidateOriginalChatClientNotNull([CallerMemberName] string? callerMemberName = null) + { + if (_originalChatClient is null) + { + string errorMessage = + $""" + Failed to invoke '{nameof(IChatClient)}.{callerMemberName}()'. + Did you forget to specify the argument value for 'originalChatClient' or 'originalChatConfiguration' when calling '{nameof(ContentSafetyServiceConfiguration)}.ToChatConfiguration()'? + """; + + Throw.ArgumentNullException(nameof(_originalChatClient), errorMessage); + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs index 5ada749b627..1d7f15ee724 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfigurationExtensions.cs @@ -48,4 +48,38 @@ public static ChatConfiguration ToChatConfiguration( return new ChatConfiguration(newChatClient); } + + /// + /// Returns a that can be used to communicate with the Azure AI Foundry Evaluation + /// service for performing content safety evaluations. + /// + /// + /// An object that specifies configuration parameters such as the Azure AI project that should be used, and the + /// credentials that should be used, when communicating with the Azure AI Foundry Evaluation service to perform + /// content safety evaluations. + /// + /// + /// The original . The returned will be a + /// wrapper around that can be used both to communicate with the AI model + /// that is configured to communicate with, as well as to communicate with + /// the Azure AI Foundry Evaluation service. + /// + /// + /// A that can be used to communicate with the Azure AI Foundry Evaluation service + /// for performing content safety evaluations. + /// + public static ChatConfiguration ToChatConfiguration( + this ContentSafetyServiceConfiguration contentSafetyServiceConfiguration, + IChatClient originalChatClient) + { + _ = Throw.IfNull(contentSafetyServiceConfiguration); + +#pragma warning disable CA2000 // Dispose objects before they go out of scope. + // We can't dispose newChatClient here because it is returned to the caller. + + var newChatClient = new ContentSafetyChatClient(contentSafetyServiceConfiguration, originalChatClient); +#pragma warning restore CA2000 + + return new ChatConfiguration(newChatClient); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs index be6e08c1f43..609646c8061 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs @@ -49,7 +49,7 @@ static SafetyEvaluatorTests() string usesContext = $"Feature: Context"; var credential = new ChainedTokenCredential(new AzureCliCredential(), new DefaultAzureCredential()); - ContentSafetyServiceConfiguration contentSafetyServiceConfiguration = + var contentSafetyServiceConfiguration = new ContentSafetyServiceConfiguration( credential, subscriptionId: Settings.Current.AzureSubscriptionId, @@ -153,8 +153,8 @@ At its furthest point (conjunction), Mars is about 250 million miles from Earth. The distance varies due to the elliptical orbits of both planets. """; - GroundednessProEvaluatorContext groundednessProContext = new GroundednessProEvaluatorContext(groundingContext); - UngroundedAttributesEvaluatorContext ungroundedAttributesContext = new UngroundedAttributesEvaluatorContext(groundingContext); + var groundednessProContext = new GroundednessProEvaluatorContext(groundingContext); + var ungroundedAttributesContext = new UngroundedAttributesEvaluatorContext(groundingContext); IEnumerable additionalContext = [groundednessProContext, ungroundedAttributesContext]; EvaluationResult result = await scenarioRun.EvaluateAsync(messages, response, additionalContext); @@ -228,8 +228,8 @@ At its closest (opposition), Jupiter is about 365 million miles away. At its furthest (conjunction), it can be approximately 601 million miles away. """; - GroundednessProEvaluatorContext groundednessProContext = new GroundednessProEvaluatorContext(groundingContext); - UngroundedAttributesEvaluatorContext ungroundedAttributesContext = new UngroundedAttributesEvaluatorContext(groundingContext); + var groundednessProContext = new GroundednessProEvaluatorContext(groundingContext); + var ungroundedAttributesContext = new UngroundedAttributesEvaluatorContext(groundingContext); IEnumerable additionalContext = [groundednessProContext, ungroundedAttributesContext]; EvaluationResult result = await scenarioRun.EvaluateAsync(messages, response2, additionalContext); @@ -266,7 +266,7 @@ public async Task EvaluateConversationWithImageInQuestion() await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithImageInQuestion)}"); - ChatMessage question = + var question = new ChatMessage { Role = ChatRole.User, @@ -304,7 +304,7 @@ await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( ChatMessage question = "Can you show me an image pertaining to DotNet?".ToUserMessage(); - ChatMessage answer = + var answer = new ChatMessage { Role = ChatRole.Assistant, @@ -338,7 +338,7 @@ public async Task EvaluateConversationWithImagesInMultipleTurns() await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithImagesInMultipleTurns)}"); - ChatMessage question1 = + var question1 = new ChatMessage { Role = ChatRole.User, @@ -351,7 +351,7 @@ await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( ChatMessage question2 = "Can you show me an image pertaining to Microsoft Copilot?".ToUserMessage(); - ChatMessage answer2 = + var answer2 = new ChatMessage { Role = ChatRole.Assistant, @@ -387,7 +387,7 @@ public async Task EvaluateConversationWithImagesAndTextInMultipleTurns() await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithImagesAndTextInMultipleTurns)}"); - ChatMessage question1 = + var question1 = new ChatMessage { Role = ChatRole.User, @@ -400,7 +400,7 @@ await _imageContentSafetyReportingConfiguration.CreateScenarioRunAsync( ChatMessage question2 = "Can you show me an image pertaining to Microsoft Copilot?".ToUserMessage(); - ChatMessage answer2 = + var answer2 = new ChatMessage { Role = ChatRole.Assistant, @@ -499,7 +499,7 @@ await _codeVulnerabilityReportingConfiguration.CreateScenarioRunAsync( """.ToAssistantMessage(); ChatMessage[] messages = [context1, completion1, context2]; - ChatResponse response = new ChatResponse(completion2); + var response = new ChatResponse(completion2); EvaluationResult result = await scenarioRun.EvaluateAsync(messages, response); Assert.False( From c8d2c9ba0580eca7998c13cacc0de3d05dacea6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 15 May 2025 09:08:31 -0500 Subject: [PATCH 113/472] Use RawRepresentationFactory on AzureAIInference embedding generators (#6445) --- .../AzureAIInferenceEmbeddingGenerator.cs | 22 +++--- ...AzureAIInferenceImageEmbeddingGenerator.cs | 23 ++++--- ...AzureAIInferenceEmbeddingGeneratorTests.cs | 63 +++++++++++++++++ ...AIInferenceImageEmbeddingGeneratorTests.cs | 67 +++++++++++++++++++ 4 files changed, 159 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs index fbaadb2915a..46a8a204e80 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs @@ -164,17 +164,23 @@ static void ThrowInvalidData() => } /// Converts an extensions options instance to an Azure.AI.Inference options instance. - /// - /// Note: we can't currently use RawRepresentationFactory due to https://github.com/Azure/azure-sdk-for-net/issues/50018. - /// private EmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) { - EmbeddingsOptions result = new(inputs) + if (options?.RawRepresentationFactory?.Invoke(this) is not EmbeddingsOptions result) { - Dimensions = options?.Dimensions ?? _dimensions, - Model = options?.ModelId ?? _metadata.DefaultModelId, - EncodingFormat = EmbeddingEncodingFormat.Base64, - }; + result = new EmbeddingsOptions(inputs); + } + else + { + foreach (var input in inputs) + { + result.Input.Add(input); + } + } + + result.Dimensions ??= options?.Dimensions ?? _dimensions; + result.Model ??= options?.ModelId ?? _metadata.DefaultModelId; + result.EncodingFormat = EmbeddingEncodingFormat.Base64; if (options?.AdditionalProperties is { } props) { diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs index 05ca25bb901..1604509a410 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs @@ -117,17 +117,24 @@ void IDisposable.Dispose() } /// Converts an extensions options instance to an Azure.AI.Inference options instance. - /// - /// Note: we can't currently use RawRepresentationFactory due to https://github.com/Azure/azure-sdk-for-net/issues/50018. - /// private ImageEmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) { - ImageEmbeddingsOptions result = new(inputs.Select(dc => new ImageEmbeddingInput(dc.Uri))) + IEnumerable imageEmbeddingInputs = inputs.Select(dc => new ImageEmbeddingInput(dc.Uri)); + if (options?.RawRepresentationFactory?.Invoke(this) is not ImageEmbeddingsOptions result) { - Dimensions = options?.Dimensions ?? _dimensions, - Model = options?.ModelId ?? _metadata.DefaultModelId, - EncodingFormat = EmbeddingEncodingFormat.Base64, - }; + result = new ImageEmbeddingsOptions(imageEmbeddingInputs); + } + else + { + foreach (var input in imageEmbeddingInputs) + { + result.Input.Add(input); + } + } + + result.Dimensions ??= options?.Dimensions ?? _dimensions; + result.Model ??= options?.ModelId ?? _metadata.DefaultModelId; + result.EncodingFormat = EmbeddingEncodingFormat.Base64; if (options?.AdditionalProperties is { } props) { diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs index 1a6c60c6ac2..3b1a7dda9d1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs @@ -122,4 +122,67 @@ public async Task GenerateAsync_ExpectedRequestResponse() Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); } } + + [Fact] + public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation() + { + const string Input = """ + { + "input":["hello, world!","red, white, blue"], + "dimensions":1536, + "encoding_format":"base64", + "model":"text-embedding-3-small" + } + """; + + const string Output = """ + { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "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" + }, + { + "object": "embedding", + "index": 1, + "embedding": "eyfbu150UDkC6hQ9ip9oPG7jWDw3AOm8DQlcvFiY5Lt3Z6W8BLPPOV0uOz3FlQk8h5AYvH6Aobv0z/E8nOQRvHI8H7rQA+s8F6X9vPplyDzuZ1u8T2cTvAUeoDt0v0Q9/xx5vOhqlT1EgXu8zfQavTK0CDxRxX08v3MIPAY29bzIpFm8bGAzvQkkazxCciu8mjyxvIK0rDx6mzC7Eqg3O8H2rTz9vo482RNiPUYRB7xaQMU80h8hu8kPqrtyPB+8dvxUvfplSD21bJY8oQ8YPZbCEDvxegw9bTJzvYNlEj0h2q+9mw5xPQ5P8TyWwpA7rmvvO2Go27xw2tO6luNqO2pEfTztTwa7KnbRvAbw37vkEU89uKAhPGfvF7u6I8c8DPGGvB1gjzxU2K48+oqDPLCo/zsskoc8PUclvXCUvjzOpQC9qxaKO1iY5LyT9XS9ZNzmvI74Lr03azk93CYTvFJVCTzd+FK8lwgmvcMzPr00q4O9k46FvEx5HbyIqO083xSJvC7PFzy/lOK7HPW+PF2ikDxeAHu9QnIrvSz59rl/UmG8ZNzmu2b4nD3V31Y5aXK9O/2+jrxljUw8y9jkPGuvTTxX5/48u44XPXFFpDwAiEm8lcuVvX6h+zwe7Lm8SUUSPHmkNTu9Eb08cP8OvYgcw7xU2C49Wm4FPeV8H72AA8c7eH/6vBI0Yj3L2GQ8/0G0PHg5ZTvHjAS9fNhAPcE8wzws2By6RWAhvWTcZjz+1uM8H1eKvHdnJT0TWR29KcVrPdu7wrvMQzW9VhW/Ozo09LvFtuM8OlmvPO5GAT3eHY68zTqwvIhiWLs1w1i9sGJqPaurOb0s2Jy8Z++XOwAU9Lggb988vnyNvVfGpLypKBS8IouVO60NBb26r/G6w+0ovbVslrz+kE68MQOjOxdf6DvoRdo8Z4RHPCvhIT3e7009P4Q1PQ0JXDyD8Ty8/ZnTuhu4Lj3X1lG9sVnlvMxDNb3wySY9cUWkPNZKJ73qyP+8rS7fPNhBojwpxes8kt0fPM7rlbwYEE68zoBFvdrExzsMzEu9BflkvF0uu7zNFfW8UyfJPPSJ3LrEBf68+6JYvef/xDpAe7C8f5h2vPqKA7xUTAS9eDllPVK8eL0+GeW7654gPQuGNr3/+x69YajbPAehRTyc5BE8pfQIPMGwGL2QoA87iGJYPYXoN7s4sc69f1JhPdYEkjxgkIa6uxpCvHtMljtYvR88uCzMPBeEo7wm1/U8GBDOvBkHybwyG3i7aeaSvQzMyzy3e2a9xZUJvVSSmTu7SII8x4yEPKAYHTxUTIQ8lcsVO5x5QT3VDRe963llO4K0rLqI1i07DX0xvQv6CznrniA9nL9WPTvl2Tw6WS+8NcPYvEL+VbzZfrK9NDcuO4wBNL0jXVW980PHvNZKJz1Oti09StG8vIZTiDwu8PE8zP0fO9340juv1j890vFgvMFqAz2kHui7PNxUPQehxTzjGlQ9vcunPL+U4jyfrUw8R+NGPHQF2jtSdmO8mYtLvF50ULyT1Bo9ONaJPC1kx7woznC83xQJvUdv8byEXA29keaku6Qe6Ly+fA29kKAPOxLuzLxjxJG9JnCGur58jTws2Jy8CkmmO3pVm7uwqH87Eu7Mu/SJXL0IUis9MFI9vGnmEr1Oti09Z+8XvH1DkbwcaZS8NDcuvT0BkLyPNT89Haakuza607wv5+w81KLGO80VdT3MiUq8J4hbPHHRzrwr4aG8PSJqvJOOBT3t2zC8eBgLvXchkLymOp66y9jkPDdG/jw2ulO983GHPDvl2Tt+Ooy9NwDpOzZ0Pr3xegw7bhGZvEpd57s5YjS9Gk1evIbfMjxBwcW8NnQ+PMlVPzxR6ji9M8zdPImHk7wQsby8u0gCPXtMFr22YxE9Wm4FPaXPzbygGJ093bK9OuYtBTxyXfk8iYeTvNH65byk/Q29QO+FvKbGyLxCcqs9nL/WvPtcQ72XTjs8kt2fuhaNKDxqRH08KX9WPbmXnDtXDDo96GoVPVw3QL0eeGS8ayOjvAIL7zywQZC9at0NvUMjET1Q8707eTDgvIio7Tv60Jg87kYBOw50LLx7BgE96qclPUXsSz0nQkY5aDUtvQF/RD1bZQC73fjSPHgYCzyPNT+9q315vbMvhjsvodc8tEdbPGcQ8jz8U768cYs5PIwBtL38x5M9PtPPvIex8jzfFIk9vsIivLsaQj2/uZ072y8YvSV5C7uoA9k8JA67PO5nWzvS8eC8av7nuxSWrbybpwE9f5h2vG3sXTmoA1k9sjiLvTBSPbxc8Sq9UpuePB+dHz2/cwg9BWS1vCrqJr2M3Pg86LAqPS/GEj3oRdq8GiyEvACISbuiJ+28FFAYuzBSvTzwDzy8K5uMvE5wmDpd6CW6dkJqPGlyvTwF2Iq9f1JhPSHarzwDdr88JXkLu4ADxzx5pDW7zqUAvdAoJj24wXs8doj/PH46jD2/2vc893fSuyxtTL0YnPg7IWbaPOiwqrxLDk27ZxDyPBpymbwW0z08M/odPTufRL1AVvU849Q+vBGDfD3JDyq6Z6kCPL9OzTz0rpe8FtM9vaDqXLx+W2Y7jHWJPGXT4TwJ3lW9M4bIPPCDkTwoZwE9XH1VOmksqLxLPI08cNrTvCyz4bz+Srm8kiO1vDP6nbvIpNk8MrSIvPe95zoTWR29SYsnPYC9MT2F6De93qm4PCbX9bqqhv47yky6PENE67x/DEw8JdYAvUdvcbywh6W8//ueO8fSmTyjTCi9yky6O/qr3TzvGEE8wqcTPeDmSDyuJVo8ip/ou1HqOLxOtq28y5LPuxk1Cb0Ddr+7c+2EvKQeaL1SVQk8XS47PGTcZjwdpiQ8uFqMO0QaDD1XxqS8mLmLuuSFJDz1xmy8PvgKvJAHf7yC+kE8VapuvetYC7tHCAI8oidtPOiwqjyoSW68xCo5vfzobTzz2HY88/0xPNkT4rty9om8RexLu9SiRrsVaG081gSSO5IjtTsOLpc72sTHPGCQBj0QJRI9BCclPI1sBDzCyO07QHuwvOYthTz4tGK5QHuwvWfvFz2CQNc8PviKPO8YwTuQoA89fjoMPBnBs7zGZ8m8uiPHvMdeRLx+gKE8keaku0wziDzZWfe8I4KQPJ0qpzs4sc47dyEQPEQaDDzVmcE8//uePJcIJjztTwa9ogaTOftcwztU2K48opvCuyz5drzqM1C7iYcTvfDJJjxXxiQ9o0wovO1PBrwqvGa7dSoVPbI4izvnuS88zzGrPH3POzzHXkQ9PSJqOXCUPryW4+o8ELE8PNZKp7z+Sjm8foChPPIGtzyTaUq8JA47vBiceDw3a7m6jWyEOmksKDwH59q5GMo4veALBL0SqDe7IaxvvBD3Ubxn7xc9+dkdPSBOBTxHCAI8mYvLOydCxjw5HB88zTqwvJXs77w9AZA9CxvmvIeQGL2rffm8JXkLPKqGfjyoSe464d1DPPd3UrpO/EK8qxYKvUuCojwhZlq8EPfRPKaAs7xKF9K85i0FvEYRhzyPNT88m6cBvdSiRjxnqQI9uOY2vcBFSLx4OeW7BxUbPCz59rt+W2Y7SWZsPGzUCLzE5KM7sIclvIdr3buoSW47AK0EPImHE7wgToU8IdovO7FZ5bxbzO+8uMF7PGayB7z6ioO8zzErPEcIgrxSm568FJYtvNf7jDyrffm8KaQRPcoGpTwleQu8EWKiPHPthLz44qI8pEOjvWh7QjzpPNU8lcuVPHCUPr3n/8Q8bNQIu0WmNr1Erzs95VfkPCeIW7vT0Aa7656gudH65bxw/w49ZrKHPHsn27sIUiu8mEU2vdUNF7wBf8Q809CGPFtlgDo1fcO85i2FPEcIAjwL+os653OavOu1AL2EN9K8H52fPKzoybuMdYk8T2cTO8lVPzyK5X07iNYtvD74ijzT0IY8RIF7vLLENbyZi8s8KwJ8vAne1TvGZ8k71gSSumJZwTybp4G8656gPG8IFL27SAI9arjSvKVbeDxljcy83fjSuxu4Lr2DZRK9G0TZvLFZ5bxR6ji8NPEYPbI4izyAvTE9riVaPCCUGrw0Ny48f1LhuzIb+DolBTY8UH9ou/4EpLyAvTG9CFIrvCBOBTlkIvy8WJhkvHIXZLkf47Q8GQfJvBpNXr1pcr07c8jJO2nmkrxOcJi8sy8GuzjWibu2Pta8WQO1PFPhs7z7XEO8pEMjvb9OzTz4bs08EWKiu0YyYbzeHQ695D+PPKVbeDzvGEG9B6HFO0uCojws+Xa7JQW2OpRgRbxjCqc8Sw7NPDTxmLwjXVW8sRNQvFPhszzM/Z88rVMavZPUGj06WS+8JpHgO3etursdx369uZccvKplJDws+Xa8fzGHPB1gj7yqZaQ887ecPBNZHbzoi2+7NwDpPMxDtbzfWh49H+O0PO+kaztI2kE8/xz5PImHE73fNWO8T60ovIPxPDvR2Yu8XH3VvMcYr7wfnR+9fUORPIdr3Tyn6wO9nkL8vM2uhTzGIbS66u26vE2/MrxFYKE8iwo5vLSNcLy+wiK9GTUJPK10dLzrniC8qkBpvPxTPrwzQLO8illTvFi9H7yMATS7ayOjO14Ae7z19Cy87dswPKbGyDzujJa93EdtPdsB2LYT5Ue9RhEHPKurubxm+By9+mVIvIy7HrxZj987yOpuvUdv8TvgCwS8TDMIO9xsqLsL+gs8BWS1PFRMBD1yXXm86GoVvK+QqjxRXg46TZHyu2ayhzx7TJa8uKAhPLyFkjsV3MI7niGiPGNQvDxgkIa887ccPUmLJ7yZsIa8KDnBvHgYi7yMR0m82ukCvRuK7junUvO8aeYSPXtt8LqXCKa84kgUPd5jIzxlRze93xQJPNNcMT2v1j889GiCPKRkfbxz7YQ8b06pO8cYL7xg9/U8yQ+qPGlyvbzfNWO8vZ3nPBGD/DtB5gC7yKRZPPTPcbz6q928bleuPI74rrzVDRe9CQORvMmb1Dzv0qs8DBLhu4dr3bta1fQ8aeYSvRD3UTugpMe8CxvmPP9BNDzHjAQ742DpOzXD2Dz4bk28c1T0Onxka7zEBf48uiNHvGayBz1pcj29NcPYvDnu3jz5kwg9WkBFvL58jTx/mHY8wTzDPDZ0Pru/uZ08PQGQPOFRmby4oKE8JktLPIx1iTsppBG9dyGQvHfzT7wzhki44KAzPSOCkDzv0iu8lGBFO2VHNzyKxKM72EEiPYtQzryT9fQ8UDnTPEx5nTzuZ9s8QO8FvG8IlDx7J9s6MUk4O9k4nbx7TBa7G7iuvCzYHDocr6k8/7UJPY2ymTwVIlg8KjC8OvSuFz2iJ+28cCBpvE0qAzw41ok7sgrLvPjiojyG37K6lwimvKcxGTwRHI28y5LPO/mTiDx82MC5VJIZPWkH7TwPusG8YhOsvH1DkbzUx4E8TQXIvO+ka7zKwI+8w+2oPNLxYLzxegy9zEM1PDo0dDxIINc8FdxCO46E2TwPRmw9+ooDvMmb1LwBf0S8CQMRvEXsS7zPvdU80qvLPLfvO7wbuK68iBzDO0cpXL2WndU7dXCqvOTLubytLl88LokCvZj/IDw0q4M8G7guvNkTYrq5UQe7vcunvIrEI7xuERm9RexLvAdbsDwLQCE7uVEHPYjWrbuM3Pi8g2WSO3R5L7x4XiC8vKZsu9Sixros+fa8UH/ouxxpFL3wyaa72sRHu2YZ9zuiJ2274o4pOjkcnzyagka7za4FvYrEozwCMCo7cJQ+vfqKAzzJ4em8fNhAPUB7sLylz80833v4vOU2ir1ty4M8UV4OPXQF2jyu30S9EjRivBVo7TwXX2g70ANrvEJyq7wQJRK99jE9O7c10brUxwE9SUUSPS4VLbzBsJg7FHHyPMz9n7latJo8bleuvBpN3jsF+WS8Ye7wO4nNKL0TWZ08iRM+vOn2v7sB8xm9jY3ePJ/zYbkLG+a7ZvicvGxgM73L2OS761iLPKcxmTrX+ww8J0JGu1MnyTtJZuw7pIm4PJbCED29V1K9PFCqPLBBkLxhYka8hXTiPEB7MDzrniA7h5CYvIR9ZzzARcg7TZHyu4sKOb1in9Y7nL9WO6gD2TxSduO8UaQjPQO81Lxw/w69KwL8O4FJ3D2XTju8SE6XPGDWGz0K1VC8YhMsvObCtDyndy49BCclu68cVbxemYu8sGLqOksOzTzj1L47ISBFvLly4Ttk3Oa8RhGHNwzxBj0v5+y7ogaTPA+6QbxiE6w8ubj2PDixzrstZEe9jbKZPPd30rwqMDw8TQXIPFurlTxx0c68jLsePfSJ3LuXTru8yeHpu6Ewcjx5D4a8BvBfvN8Uibs9R6W8lsIQvaEw8rvVUyw8SJQsPebCNDwu8PE8GMo4OxAlkjwJmMA8KaQRvdYlbDwNNxy9ouHXPDffDrxwZv46AK0EPJqCRrpWz6k8/0E0POAs3rxmsoe7zTqwO5mLyzyP7ym7wTzDvFB/aLx5D4a7doj/O67fxDtsO/g7uq9xvMWViTtC/tU7PhnlvIEogjxxRSQ9SJSsPIJA1zyBKAI9ockCPYC9MbxBTXC83xSJvPFVUb1n75c8uiNHOxdf6Drt27A8/FM+vJOvXz3a6QI8UaQjuvqKgzyOhNm831oevF+xYLxjCic8sn6gPDdrOTs3Rv66cP+Ou5785rycBew8J0JGPJOOBbw9Imq8q335O3MOX7xemQs8PtNPPE1L3Tx5dnU4A+EPPLrdsTzfFIm7LJIHPB4yz7zbAdi8FWjtu1h3Cj0oznA8kv55PKgDWbxIINc8xdsePa8cVbzmlHQ8IJSavAgMlrx4XiA8z3dAu2PEET3xm+a75//EvK2Zr7xbqxU8zP2fvOSFJD1xRSS7k44FvPzHkzz5+ne8+tAYvd5jIz1GMuE8yxSAO3KCNDyRuOS8wzO+vObCNDwzQLO7isQjva1TGrz6ioM79GgCPF66Zbx1KpW8qW6pu4RcDTzcJhO9SJQsO5G45LsAiMm8lRErvJqCxjzQbju7w3nTuTclpDywqP88ysCPvAF/xLxfa0u88cChPBjKODyaPLE8k69fvGFiRrvuRgG9ATmvvJEsOr21+EC9KX/WOrmXnDwDAuo8yky6PI1sBDvztxy8PviKPKInbbzbdS276mGQO2Kf1rwn/DC8ZrIHPBRxcj0z+h264d1DPdG0ULxvTqm5bDt4vToTmjuGJcg7tmMRO9YEEr3oJAC9THmdPKn607vcJhM8Zj6yvHR5r7ywYmq83fjSO5mLyzshIEU8EWKiuu9eVjw75dk7fzGHvNl+sjwJJOs8YllBPAtheztz7QQ92lDyvDEDozzEKrk7KnZRvG8pbjsdYI+7yky6OfWAVzzjYGk7NX3DOzrNhDyeIaI8joTZvFcMOryYRba8G7iuu893QDw9RyW7za6FvDUJ7rva6YK9D7rBPD1o/zxCLJa65TaKvHsGAT2g6ly8+tCYu+wqy7xeAHu8vZ1nPBv+QzwfVwo8CMYAvM+91TzKTDq8Ueo4u2uvzTsBf8Q8p+uDvKofDz12tj+8wP+yOlkDtTwYyji6ZdPhPGv14rwqdtE8YPf1vLIKy7yFLs28ouFXvO1PBj15pDU83xQJPdfWUTz8x5O64kgUPBQKA72eIaK6A3a/OyzYnLoYnPg4XMNqPdxsqLsKSaY7pfSIvBoshLupKJS8G0TZOu/SqzzFcE47cvaJPA19Mb14dQC8sVllvJmwhjycBey8cvaJOmSWUbvRtFC8WtX0O2r+57twIGm8yeFpvFuG2rzCyO08PUelPK5rbzouFS29uCxMPQAUdDqtma88wqeTu5gge7zH8/O7l067PJdOO7uKxCO8/xx5vKt9+TztTwa8OhOaO+Q/Dzw33w49CZhAvSubjDydttG8IdovPIADR7stHrI7ATmvvOAs3rzL2OQ69K4XvNccZ7zlV2S8c+0EPfNDxzydKqc6LLPhO8YhtDyJhxM9H1eKOaNMKLtOcBg9HPU+PTsrbzvT0Ia8BG26PB2mpDp7TJa8wP8yPVvM77t0ea86eTBgvFurFT1C/tW7CkkmvKOSPT2aPDG9lGDFPAhSq7u5UYc8l5TQPFh3ijz9vg68lGBFO4/vKTxViZS7eQ8GPTNAs7xmsoe8o0yoPJfaZbwlvyA8IazvO0XsS717TJY8flvmOgHFWbyWnVW8mdFgvJbCkDynDF68" + } + ], + "model": "text-embedding-3-small" + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = new EmbeddingsClient(new("http://somewhere"), new AzureKeyCredential("key"), new() + { + Transport = new HttpClientTransport(httpClient), + }).AsIEmbeddingGenerator("text-embedding-3-large"); + + var response = await generator.GenerateAsync([ + "hello, world!", + "red, white, blue", + ], new EmbeddingGenerationOptions + { + Dimensions = 3072, + RawRepresentationFactory = (e) => new EmbeddingsOptions(input: []) + { + Dimensions = 1536, + Model = "text-embedding-3-small", + EncodingFormat = EmbeddingEncodingFormat.Single, // this will be overwritten, we only support base64. + } + }); + + Assert.NotNull(response); + + foreach (Embedding e in response) + { + Assert.Equal("text-embedding-3-small", e.ModelId); + Assert.NotNull(e.CreatedAt); + Assert.Equal(1536, e.Vector.Length); + Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs index 7ca945eb07b..0e2a3b685af 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs @@ -125,4 +125,71 @@ public async Task GenerateAsync_ExpectedRequestResponse() Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); } } + + [Fact] + public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation() + { + DataContent dotnetPng = new(ImageDataUri.GetImageDataUri()); + + const string Input = """ + { + "input":[{"image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcgAAAHICAMAAAD9f4rYAAAAS1BMVEVRK9RRK9T///\u002BolenTyvTp5fpnRdl8YN\u002B\u002Br\u002B708v1cONedh\u002Be\u002Bru5nRtl9YN6HbeKyouzJvfKSeuSzou2\u002Br\u002B9yU9ze1/dcONbe2PcNfWisAAAAAXRSTlP\u002BGuMHfQAAB79JREFUeNrs0QENAAAMw6Ddv\u002Bn7aMACOwomskFkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESKfnTvMTRyGoiisF5K2SYZhKKX7X\u002BpEeuov7Ngxorp\u002BOmcH9KssLnISJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMki/DzkNqUZr7H146M0ynYZnmgof4cn\u002B2BPpQA6rFQMymxDk/GalgMwmBDlcrRSQ2ZQgh79WCMhsUpDTYvsBmU0Kcvhn\u002BwGZTQuydLgCmU0MsjAmgcwmBlkYk0BmU4PcH5NAZlOD3D9cgcwmBzlcLB\u002BQ2fQg98YkkNn0IPfGJJDZBCF3xiSQ2RQhvy3XKyDnsboP\u002B\u002Bk6FpoT/wZjodWeSBEyPyZfATnaKxqHh072yiQhj4xJID1JyCN/XCA9TcgDYxJITxRyXqwyID1RyPoxCaSnClk9JoH0NCDH9jEJpKcBeR\u002BaPzeQngbk5do8JoH0NCA/35vHJJCeBuRqY0Ly0yoC0tOAPNm5dUwC6alA2q1xTALpaUBuYsvUNiaB9DQgP8w9Gq59AOnpQNq1aUwC6QlBnueWMQmkJwRpa8uYBNJTgrSx4doHkJ4UZMuYBNKTgkzeVvyy3YD0tCAbxiSQnhZkw5gE0hODtNvRMQmkpwa5zEOtiwekpwZpl4NjEkhPDvLomATS04M8z4fGJJCeHqSth95uBqQnCGnjkTEJpKcIeT8yJoH0FCEPjUkgPUnI5C91d0v2a08sf1p9QJp34JprM2S5dgcgf/qqHpNAeqKQS/W1DyA9Ucj6MQmkpwpZPSaB9GQhz3PdmATSk4W0U90zBEB6upD2XXW4AukJQ9aNSSA9YUi71YxJID1lyGWqGJNAesqQVYcrkJ40pF3LbzcD0tOGXMpjEkhPG9LW4pgE0hOHLP9S9zTkPNW1Wn1APnSeC28344aApw5pp8KYBNKTh7TCmATS04csjEkgPX1Iu\u002B2OSSC9DiCXae8ZAiC9DiDtsjcmgfR6gNwdk0B6XUDujUkgvS4gbc3/ZAak1wekjdkxCaTXCeQ9OyaB9DqBtFPuVdlAer1AZsckkF4vkPaeGZNAet1A2i09JoH0\u002BoHMXvu4A7nVD6RdMmPyDcitjiDTYxJIryfI85xkWIDc6gnS1vS1DyC3uoK0MTkmZyDN\u002BoJMj8kJSLO\u002BINNjcgTSrDPIZUpIfAFp1hlk8nDlaN3qDTL1KiW\u002BtW51B7nMQKbqDtJWIP\u002BzdwerDcNQEEUZWbIqG9XESev8/5d2EQol7wXcZBSwmLv3Zg54oYXkdTxIREE6HRCyFkHa2JDbfEohlHj5xINehsQgSBsXchtK\u002BC2tcHsdEt\u002BCNFEhx7Tj0XICZBakiQk53gvFCTYCJM5EyOv4nzbs6diQowW6wMaAnBIBsuGVEMeG3Hl9NQMSWZAmFmQO\u002Bx7WpUDiJMhbfEh/2hkmCmQtgkQbyOB2gokCiVmQQAvIHNwSTBxIREE2gVyCH0wkyCrIJpBrMLWFxCDIVr/W90JOSZANIMfgdoWJBYksSD6kx\u002BOft/IgcRZkA0h/owoTD3IqgqRD\u002BqteYCJCYhEkHdJdNVWYmJCIguRD2pXKF2xUyFoESYc0MyXXkQqJWZANILH\u002BNYoVfvNw34KnmwenCQ/Kw4vlvUt4n7aKDwms8aZYPjLU2\u002BJDAlte1jxCvbUbpOohQXaSIDtJkJ0kyE4SZCcJspME2UmC/GGPDmQAAAAABvlb36M9hRBHIo5EHIk4EnEk4kjEkYgjEUcijkQciTgScSTiSMSRiCMRRyKORByJOBJxJOJIxJGIIxFHIo5EHIk4EnEk4kjEkYgjEUciYo8OZAAAAAAG\u002BVvf4yuFRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQWXt0QAMAAIAwyP6p7cFOBRBFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokjEgjh2WnxgwCuWdQAAAABJRU5ErkJggg=="}], + "dimensions":1536, + "encoding_format":"base64", + "model":"embed-v-4-0" + } + """; + + const string Output = """ + { + "id":"9da7c0f0-4b9d-46d9-9323-f10f46977493", + "object":"list", + "data": + [ + { + "index":0, + "object":"embedding", + "embedding":"AADkPAAA6bsAAD68AAAnOwAAYbwAAFa8AABfvQAAqzsAAGy8AABwvAAAyDwAALo9AAC6vAAAID0AAGE8AAA2PAAA4TsAABU9AAC0PAAAqzwAADw8AAAaPQAA2LwAANa8AACoOgAA4DsAAA48AAC9PAAAdz0AAIC8AACGvAAA+7oAAEo7AABMuwAAab0AAFc9AAA2OwAAob0AAPO8AABGOwAAoLwAAKQ6AACHvAAAX70AAJQ8AAA/OwAAtbwAAME7AADMvAAA2DwAABQ9AABZPQAAd7wAAD+9AAAquwAAgTwAABE9AABFPQAALbwAAEk9AAC8ugAABj0AAI27AAAWPQAAMrwAAE88AABnvAAAZbwAAMK8AAAhPQAAPr0AAAg8AABcOgAA/jwAANE8AACvvAAAEbwAALy8AAAIvQAAe7wAAGW8AAAPPQAAFTsAALc5AACrOwAAirwAALa8AACLvAAACLsAABa7AAC+uAAAljwAAMS9AAC0vAAAhz0AAJw7AAAgvQAAxjwAAMK8AACMPAAAdz0AALC8AADIPAAArjsAAGG8AAATvAAAkrsAALs8AAAWPAAAxDwAADK9AAAAvQAAmTwAAIK9AADZPAAAmjwAABG9AAARuwAA/zwAAGO8AAC3PAAAGTwAACC8AAAtPAAAArwAAGG7AAC4PAAA/7sAAKG8AACdOwAA8DwAAJo7AAC8PAAAST0AAAI8AABnvAAAXTwAABc9AACSPAAAMjsAAPc7AABSvAAATLsAAKa8AAB1PAAAA70AAC87AAASvAAA/DwAADC7AABfvAAAYbwAAGW9AADlOwAANzwAAFc8AADEPAAAyrsAAMM8AAATPQAA3DwAABu8AAB+uQAAKj0AADS7AACkOwAAhD0AACK8AABIvQAAaboAALu8AADtOwAAoDwAAI88AACQPAAAmjwAAEy8AAC2OwAAtTwAAE68AACGvQAA0LsAAJM7AAAUvQAA17wAAEg9AABhOwAAVjwAALg8AACHvQAA5DwAACI9AACLPAAA4zsAAOk7AADOPAAA/7wAAPe8AAAGPQAAYTwAAEo9AAA/PAAA47wAAEq8AADgvAAAybsAAPk8AAA7vQAA3zsAAP87AAAUvAAAKjwAAKA8AAATvQAAcrwAAGm7AADlvAAAprwAAJM7AACivAAABr0AAEu7AAAxuwAAjD0AAMu8AAA7vQAATjwAADo8AADfOgAAFboAACA9AAA2OwAAZDwAAOo7AABBvAAAKzsAAJK8AAC7vAAAFL0AAO47AAADvQAARj0AAJS8AADLuwAA5bwAAKa7AAAGPQAA8bwAAKG9AAC/vAAADrwAAKO8AACDOgAAr7wAABM9AAD0uwAAQr0AABs9AAC2PAAAOLwAAM88AADSPAAAqzsAAOm7AABMPAAAGz0AAHU8AAAAvQAAULwAAAa9AADavAAAgzoAAKk8AABVvAAAPboAAHU8AACKvAAAgbwAADI8AAALuwAAIb0AAKi8AABxvAAAUz0AAJk8AAAnvQAA3zwAAMM7AAAVPAAA0bwAAME6AADCuwAAIrwAAMs8AACbPAAArbwAAGG8AAChOwAAEL0AAIQ7AADePAAADr0AADE9AAAbvQAAprwAAK+8AAARvAAAWrwAAL+8AAALPAAAWTwAAJ86AACGOwAAU70AACm8AAAJPQAA+LwAAKC8AABtvAAAtLwAALQ7AACmvAAAAj0AALW8AAA0PQAAhjwAAEa6AAAfPQAAirwAAOa7AABDPAAAqLwAANM8AAAGvQAA4DsAANO7AAAdvAAA7TwAACM7AACfvAAASDsAABs8AACxPAAAVzwAAEy9AAAxPAAAmb0AALw8AAAZvAAAiLwAALY8AAB3vAAA9zwAAJs8AAAkvAAAOz0AAMo7AABLPQAAwbsAAN47AABGuQAAl7oAAG08AACJvAAAZ7oAALw8AACavAAA37wAAKA7AAAgvAAANb0AAGA8AAAhPQAANz0AAMq7AADGvAAAlTwAABI9AABhuwAAkbsAAIY7AADauwAAtDwAABk8AAD7PAAAiDwAAPG8AACwvQAAn70AAFI8AACqugAAn7wAAGA9AAA0vQAAmrwAACo9AACCOwAAoTsAAIE9AABwOwAAAr0AANc8AAAbvAAAjDwAABe9AAAPvQAA07wAACG8AADBOwAAeDwAAAg9AAB0PAAAm7wAAEW6AACaugAADr0AANY8AAD1vAAA5zsAAKK9AAAXOwAAPr0AAAA9AAD3uwAAG7wAABW9AAAOPQAAMrwAAIA7AADdPAAAEb0AAGM8AAAjvQAAUDoAABI9AAD/PAAAHL0AAKM8AACbOQAAlbwAAAO9AACqPAAAAr0AAIy7AACCvAAAZjwAAGO8AAC9PQAA7DsAAJ88AAByOwAAmrsAAD+8AAArvAAA37wAAPo8AAAkvAAAL7sAACO9AAAnvQAA9DwAAJY8AACxPAAAeTwAAFO8AAAFvQAAHzwAADe7AACmPAAAKD0AAHM9AAAgvAAAmrwAALy8AAC/OwAA3LwAAG06AAAfOwAA/7sAALE9AADBPAAAtrsAAKI8AACZuwAAgrwAAES9AADcuwAAsjsAAJE8AABWPAAAK70AAEU8AABEPAAAMbwAAK+7AACcvAAARLsAABK6AAAiPAAAEbwAANG7AAChPAAAzzwAAMs6AAAFPQAA2TsAACG9AAB1PAAAsrwAAC29AABMPAAAzzwAANI8AADfvAAAm7wAAC29AACLuwAAHTwAALq8AAAcuwAA07wAAHm8AACxvAAA7LwAAK06AAA4PQAA7LsAAKC7AAAvuwAAKrwAAC68AABtPAAAtjwAAC+8AAAJvQAATLwAALE7AACCvAAApjwAAKE8AAC4vAAAjDwAACS9AAD3PAAAHz4AACe9AAB7vQAAET0AAII6AAC2OwAAyzwAANY7AAB+PQAAuDwAAME8AADMugAAAjwAANA7AAAgvAAAFT4AAPe7AAAPvQAALrwAAJQ5AAArOwAAFjwAAKe8AAD4uwAAGTwAACQ8AAAJPAAAZTwAAJa8AACgOwAANjsAAJk8AAC7OwAAdzwAAPG7AACfvAAAtjwAAFq8AAAMPQAAMDwAAHu9AAC6vAAAVT0AAKo7AACOPAAAoTsAANc8AAAXPQAAbDwAAKi7AABVOgAA5zwAAHU8AADCvAAAyjwAAAa9AADqOwAAmbwAALq7AAA+vAAAjDwAAB+9AAAqvQAAir0AAFo9AAA+PQAAgrsAANM8AAAhPAAAhbwAAAU8AACavAAAuLwAAKa8AACqPAAAI7wAAHG8AABFPAAAgToAAIy8AAAkuwAAjrwAAA49AACpPAAACz0AABC9AAAbvAAAWjwAAPI7AAAoPAAAJjoAAK26AAAXOwAADzwAAC+9AAC4vAAAIL0AAIk8AABhPAAAPj0AAHI7AAAUvQAALT0AACG8AAByPAAADD0AANk8AAC/vAAA4bwAAGu8AAC1vAAA0jsAALc8AAChvAAAT7wAAMu8AACOvAAA4bwAAHg8AAD1PAAACz0AAB08AAAXPAAAPr0AAIG6AAAFuwAAKTwAAI27AABPPAAAmzsAAOC8AAAbPQAAp7oAAGq8AABdOgAAzDwAANe8AAAdvQAALjoAABU9AAATPQAA0rsAAAc9AAD7PAAATLwAALA6AAAruwAAX7sAABK9AAC7PAAAErwAACG8AAC3OgAAkzwAAMw7AAAEOwAAqjwAAEW7AAAHPQAA6rsAAES8AACCPQAARj0AAGY8AABGPAAAdLwAANE8AAD1vAAAGzsAAEQ6AACuuwAAFb0AAIE8AAA4PAAAlbsAAH68AAACOwAAsjwAAKE8AAAoPAAAhDsAAME8AAD7uwAAkr0AAFq9AAC4OwAAsjwAADA8AACCvAAAbbwAAAs9AACWvAAAEzwAALS8AAAgPQAAd7wAAO42AABWvQAAHLwAAPG6AAAAPAAAFz0AAME7AAAoOwAAULsAANo8AABRuwAAiDwAABw8AADVuwAA+rsAAAo9AAAavAAAMDwAANe8AAD+vAAAibwAAJC8AABfOwAAtTwAAIE8AADmOwAAgLwAAMS8AABwPAAAAb0AALS8AAAqvQAANDwAAOU8AACWvAAAzjwAABG7AACouwAAJr0AAIM7AAAZvQAA0boAAFi8AABPPAAAnzgAAIE8AACbvAAAFb0AABY8AAC+OwAA5DwAAJa6AACkPAAAITwAAGE6AABtPAAAMb0AADg9AAAEPQAAnDwAAJ08AAABPAAAursAAHc8AAAFvAAA5ToAAD28AAAAPAAAazwAADQ7AADqPAAAA70AAFO7AAA6vAAAAj4AAEg6AADhPAAAELwAAFm9AACIvAAAxTwAACQ8AADkOwAAbrwAALq7AACGPAAAIL0AAGE8AADMPAAAOr0AACM9AACMPAAAKrsAAAY8AAAhPAAAKz0AACe9AACOvAAAa7wAACG9AABKuwAASrwAAI+8AAApvQAA+LsAAPe8AADGuwAAgroAADe7AACvuwAATz0AAMQ6AACFvAAAMLwAACg9AAADvQAAtTwAALa8AACuPAAAI70AAJI8AAAauwAAZbwAAA89AADWvAAAqDwAAAm9AAAAPQAAEDwAAOA8AAAxvQAAYzwAAB87AADhOgAAwrsAAOA8AAA3vQAA1jwAAKi8AAB1uwAAGb0AAJo8AABmPAAAPLwAAMI7AAC4PAAAmj0AAFc8AADcOgAAe7wAAH47AABdOwAAlrwAAPO8AAB5PQAAijsAABU8AAAOvQAAkTwAABK8AAC4PAAAZLwAAK68AACRvAAAwzwAAKq8AABWvQAA4DsAAKC8AACUPAAAm7wAAJO8AAAMuQAAwrsAAAk8AABdvQAAkrwAACQ8AAAoNwAApDwAABQ8AAAVPAAAH7wAAFK8AAAGPAAAkrsAAIA8AADGPAAAbrwAALc8AABxPAAApDwAABy8AAAZPQAAk7wAAMW8AABhvAAAPLwAAEI8AAB5PAAAxrwAAFi7AADwvAAAUL0AAAk9AABZOwAAED0AALY8AAB5PAAAmzwAAFM9AAAwPQAAsToAAPA6AADOvAAAMLsAAHO8AADQuAAAqLwAANc7AAA4PAAA3DsAAK48AAAdPAAAH7wAACQ7AAD5OwAAo7sAACY8AACrPAAATzwAAL68AAC9PAAA8DwAABI7AADeOwAAFL0AAAC9AACEOwAAITsAAJI8AADtuwAA8LsAANa8AACvvAAAI70AAAG9AABmOwAAd7wAAIE8AAA6vQAAvzwAAEK9AAD0vAAA/zwAAPU8AACVPAAAET0AAAU7AAAfOwAANroAAKm8AAAUvQAAyLsAAAa9AAAUvAAAErwAAII7AAAFPQAAALsAAC08AAA0uwAAgTwAAIu7AADRvAAADzwAAKA7AABDvAAAirsAALo8AAB3vAAAOLwAACO9AADEPAAA7jwAADg9AAAiPQAAqzcAANA8AAAuPAAAODwAAAW8AACNvAAAIjwAANC8AAAmvQAAoTwAAAc9AACHvAAABjsAAI68AADZPAAAobsAAIi9AADsvAAABrsAAAm8AABkOwAACDwAAIY8AABQvAAAmTwAABE9AAAFvQAABzwAAF08AACoPAAAzjwAAL49AAAfPAAAkbwAALQ6AAByvAAAcD0AAN+6AACTvAAAkDsAAK66AAC0PAAAkzoAAHy8AAAiOwAADDwAAIG9AAAmvAAACrsAADU9AAAjuAAAjbwAAPc8AACNOwAABbwAAMG7AACIvAAAO7wAAL88AAD7vAAAXLwAADw4AAC2PAAAnbsAADs7AAAwvAAA0LwAAPG8AAAmPQAAz7oAAOa8AABhuwAA+jwAAFU8AADLuwAAtzwAAHA8AAA3vAAAdbwAAIG8AAC6PAAAiDkAANi7AADpuQAALrsAAL09AABauwAAMbwAAOG8AAA2OgAAejsAAGY8AAB/uwAACTsAADa7AAAGvAAASrwAAKG8AAC2OgAA3LoAABy8AACiPAAACD0AAPy8AACyvAAAIDsAAIi7AACwvAAA6rwAAMy8AAA0vQAALr0AAKS7AABgPAAASbwAAA69AAAnvQAApLwAAIE8AACUOgAAYbwAABo7AACfPAAADr0AACg9AAAAvAAAFzwAAIM7AAABOwAAujwAABS9AABqvAAAHLwAAHg8AAB3PQAAQ7wAAB08AAAIPQAAhLwAAHq8AAAfPQAAljwAAME7AAChOwAA5jgAAAy7AAALPAAAv7wAAA08AAC+uwAAzDwAAAQ9AACoPAAANTwAANi8AAAPPQAABj0AAM68AAB7uwAAIz0AAB29AAATuwAAjbsAAJ88AACfOwAAAj0AAHi8AAA9vAAAYbwAAMo8AADpPAAAAbwAABU7AAAgPAAA+jsAAAm8AABgPAAAIb0AAIK9AABwPAAAtzwAAFi7AAAmPAAAozwAAFW9AAAwvAAAFT0AAJm8AADjvAAAEjsAAFI8AAACvQAANrwAAEm7AACLuwAAITwAABu8AAD4uwAAyLwAAFw8AAA2PAAAVTwAANW8AADDPAAAMLwAACC7AADMPAAARTsAAA28AABkPAAArjwAADI8AAAEvQAAujsAAFY8AABavAAA9zwAAKI8AABVPAAA+7sAAOC8AACFPQAAjTsAAKg8AACpuwAAsjsAABU7AABRPAAAHL0AAEY8AAAhPAAAerwAAKS7AAAXOwAAkLsAAAA9AAAxPAAA4TwAACi8AADYOwAAu7sAAF68AABLPAAATL0AAEK9AADwuwAAjDsAADW6AACEPAAAv7wAAJa8AABQPQAAfLwAAAe8AAC9PAAAnTsAABM8AADQvAAAcjwAAP86AAA2vQAAKD0AAMQ8AADevAAAobwAAGE8AAB7PAAAjzwAAIY8AACkPAAA2joAAKY8AAAGPQAAc7sAALw8AAABPAAAebwAAAs9AAAoOwAAmjsAAH48AABZPAAAAjwAAIm9AAAGvQAAFTwAACo7AACLvAAArrwAAJS6AADnugAABj0AAAu8AADcvAAAvbwAAKE5AADePAAAqbwAAOw8AAA2vQAA7ToAAIG7AAA2vQAAC70AACk8AACIPAAAFr0AAKe6AAAZvQAArzwAAG48AABsPAAAAbsAAD89AACnPAAAAb0AAOu8AAAQPQAA5TwAALg8AAAbOwAAWbwAAJu7AAAJPAAA4TwAABm9AAD0OgAA07wAAPe7AAB/uwAAED0AADs8AADEOgAAhrsAAJM8AABLvQAAq7wAAL06AACfPAAAlDwAAIY9AABavAAAjDoAAAG9AABlPAAAjLsAALK8AADaPAAA4LsAAPA8AABMvAAAXTsAAOG6AAD6OgAAvjkAANC8AABxuwAAybsAAK+7AABsOwAA5zwAABW8AAC9PAAAiDwAAPg8AAAJOwAAATsAADs8AAAdPQAAeTsAACK8AADrvAAASTsAAKM8AADMPAAAU70AABK8AAD0uwAA0rwAAF08AAChNwAAbbwAAB28AACZPAAAlLwAAME8AABmvAAAhjsAAPA9AADSvAAABTwAAP48AAAVvAAAdzwAADY8AACGPQAA2DwAAC07AAC0ugAAwTsAAJC8AACdPAAAajwAAKE7AAAiPQAAzjsAAA69AACdvAAAIr0AAEi9AADBOgAAgLsAANU8AACpPAAAP7wAAPq8AAAfPAAACTwAAC49AABhPQAAsjwAAMy7AAB0PQAABb0AAAy9AAAhvQAAWL0AAHy8AAAjPQAAjDwAAGC8AACbvAAADT0AAK08AACivAAAF7wAAL+8AACTPAAAz7wAAPw7AABfvAAAt7wAALi8AAAvPQAAtrsAAJY7AAAKPQAAr7wAACS9AAC8PAAAm7wAALa8AADBvAAA3zsAAIk8AABmOwAAw7wAAPm7AAArPAAAvzsAAF+8AABPuwAAXzwAAK+8AAA3PQAAG7wAAIg8AAAXvAAAprwAADA8AADEvAAAorwAANa8AABePQAAJr0AACG8AAAcvAAAQ70AAPC8AACxPAAAOLsAAOc6AABYPAAAsLwAAN68AACXuwAALbwAAJu7AAD7uwAA2jsAALY7AACWPAAAoLwAALa8AACwuwAA/DsAAEy8AAAiPAAA5bwAAGk8AABnPAAADzwAAF27AAAGOwAAtrsAAIS7AAAqPQAAeLwAAAa9" + } + ], + "model":"embed-v4.0", + "usage": + { + "prompt_tokens":1012, + "completion_tokens":0, + "total_tokens":1012 + } + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = new ImageEmbeddingsClient( + new("https://somwhere"), new AzureKeyCredential("key"), new() + { + Transport = new HttpClientTransport(httpClient), + }).AsIEmbeddingGenerator("text-embedding-004"); + + var response = await generator.GenerateAsync([dotnetPng], + new EmbeddingGenerationOptions + { + Dimensions = 768, + RawRepresentationFactory = (e) => new ImageEmbeddingsOptions(input: []) + { + Dimensions = 1536, + Model = "embed-v-4-0", + EncodingFormat = EmbeddingEncodingFormat.Single, // this will be overwritten, we only support base64. + } + }); + + Assert.NotNull(response); + + foreach (Embedding e in response) + { + Assert.Equal("embed-v4.0", e.ModelId); + Assert.NotNull(e.CreatedAt); + Assert.Equal(1536, e.Vector.Length); + Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); + } + } } From 749caedb584a20811c5ebe1b00db7763fbcaac93 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Thu, 15 May 2025 11:06:50 -0700 Subject: [PATCH 114/472] Mark Microsoft.Extensions.AI and Microsoft.Extensions.AI.Abstractions as stable (#6446) * Bump ICSharpCode.Decompiler for record struct support in ApiChief tool Needed the fix for https://github.com/icsharpcode/ILSpy/issues/3159 to fix "record struct" formatting (it was "recordstruct" before the fix). * Generate ApiChief baselines for MEAI libraries Ran .\scripts\MakeApiBaselines.ps1 and discarded other libraries' updates. * Hand-edit MEAI ApiChief baseline to fix params ReadOnlySpan Params collections are not yet supported in ICSharpCode.Decompiler: https://github.com/icsharpcode/ILSpy/issues/829 The result is an emitted 'scoped' keyword instead of 'params'. This was edited by hand in the baseline MEAI file. * Mark Microsoft.Extensions.AI and Microsoft.Extensions.AI.Abstractions as stable * Update MEAI and MEAI.Abstractions NuGet package documentation * Update NuGet package documentation for MEAI implementation packages * Update MEAI.Templates package references, including SemanticKernel for a coherent build. * Lower OllamaSharp for integration tests to use version available on feed * Empty the ApiChief baselines for Ollama, AzureAIInference, and OpenAI adapters since they are not shipping stable * Apply code review feedback to the MEAI package READMEs * Update MEAI.Templates test snapshots for version bumps * Apply documentation review feedback to the MEAI package READMEs * Add comments to the MEAI API baseline file for the hand-editing required * Restore documentation blurb into Microsoft.Extensions.AI.AzureAIInference per other feedback --- eng/Versions.props | 12 +- eng/packages/General.props | 2 +- eng/packages/TestOnly.props | 2 +- ...icrosoft.Extensions.AI.Abstractions.csproj | 6 +- .../Microsoft.Extensions.AI.Abstractions.json | 2285 +++++++++++++++++ .../README.md | 610 +---- .../README.md | 4 + .../Microsoft.Extensions.AI.Ollama.csproj | 2 +- .../Microsoft.Extensions.AI.Ollama/README.md | 281 +- .../Microsoft.Extensions.AI.OpenAI/README.md | 4 + .../Microsoft.Extensions.AI.csproj | 4 +- .../Microsoft.Extensions.AI.json | 836 ++++++ .../Microsoft.Extensions.AI/README.md | 18 +- .../aichatweb/aichatweb.csproj | 4 +- .../aichatweb.Web/aichatweb.Web.csproj | 4 +- .../aichatweb/aichatweb.csproj | 8 +- 16 files changed, 3188 insertions(+), 894 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 33b9eb6a957..3ca2e225b00 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -158,19 +158,19 @@ 9.2.1-preview.1.25222.1 1.0.0-beta.6 2.2.0-beta.4 - 1.13.2 + 1.14.0 11.6.0 9.4.1-beta.277 9.4.1-beta.277 9.4.1-beta.277 9.4.1-beta.277 9.2.0 - 1.49.0-preview - 1.49.0-preview - 1.49.0 - 5.1.13 + 1.50.0-preview + 1.50.0-preview + 1.50.0 + 5.1.16 1.9.0 - 0.1.9 + 0.1.10 6.0.1 - 4 - 0 + n/a + n/a diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json new file mode 100644 index 00000000000..9b7dd3c022e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json @@ -0,0 +1,289 @@ +{ + "Name": "Microsoft.Extensions.AI.Evaluation.Quality, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.CoherenceEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.CoherenceEvaluator.CoherenceEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.CoherenceEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.CoherenceEvaluator.CoherenceMetricName { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.CoherenceEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluator.CompletenessEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluator.CompletenessMetricName { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluatorContext.CompletenessEvaluatorContext(string groundTruth);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluatorContext.GroundTruth { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluatorContext.GroundTruthContextName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluator.EquivalenceEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluator.EquivalenceMetricName { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluatorContext.EquivalenceEvaluatorContext(string groundTruth);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluatorContext.GroundTruth { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluatorContext.GroundTruthContextName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.FluencyEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.FluencyEvaluator.FluencyEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.FluencyEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.FluencyEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.FluencyEvaluator.FluencyMetricName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluator.GroundednessEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluator.GroundednessMetricName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluatorContext.GroundednessEvaluatorContext(string groundingContext);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluatorContext.GroundingContext { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluatorContext.GroundingContextName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.RelevanceEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.RelevanceEvaluator.RelevanceEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.RelevanceEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.RelevanceEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RelevanceEvaluator.RelevanceMetricName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.RelevanceTruthAndCompletenessEvaluator();", + "Stage": "Experimental" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.CompletenessMetricName { get; }", + "Stage": "Experimental" + }, + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.EvaluationMetricNames { get; }", + "Stage": "Experimental" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.RelevanceMetricName { get; }", + "Stage": "Experimental" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RelevanceTruthAndCompletenessEvaluator.TruthMetricName { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluator.RetrievalEvaluator();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluator.RetrievalMetricName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluatorContext.RetrievalEvaluatorContext(System.Collections.Generic.IEnumerable retrievedContextChunks);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluatorContext.RetrievalEvaluatorContext(params string[] retrievedContextChunks);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyList Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluatorContext.RetrievedContextChunks { get; }", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.Quality.RetrievalEvaluatorContext.RetrievedContextChunksContextName { get; }", + "Stage": "Stable" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj index 237df014d0d..36730ffc322 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj @@ -10,11 +10,11 @@ AIEval - preview + normal true false - 88 - 0 + n/a + n/a diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json new file mode 100644 index 00000000000..98615cb747a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json @@ -0,0 +1,77 @@ +{ + "Name": "Microsoft.Extensions.AI.Evaluation.Reporting.Azure, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageReportingConfiguration", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageReportingConfiguration.Create(Azure.Storage.Files.DataLake.DataLakeDirectoryClient client, System.Collections.Generic.IEnumerable evaluators, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, bool enableResponseCaching = true, System.TimeSpan? timeToLiveForCacheEntries = null, System.Collections.Generic.IEnumerable? cachingKeys = null, string executionName = \"Default\", System.Func? evaluationMetricInterpreter = null, System.Collections.Generic.IEnumerable? tags = null);", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider.AzureStorageResponseCacheProvider(Azure.Storage.Files.DataLake.DataLakeDirectoryClient client, System.TimeSpan? timeToLiveForCacheEntries = null);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider.DeleteExpiredCacheEntriesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider.GetCacheAsync(string scenarioName, string iterationName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider.ResetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.AzureStorageResultStore(Azure.Storage.Files.DataLake.DataLakeDirectoryClient client);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.DeleteResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.GetIterationNamesAsync(string executionName, string scenarioName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.GetLatestExecutionNamesAsync(int? count = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.GetScenarioNamesAsync(string executionName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.ReadResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore.WriteResultsAsync(System.Collections.Generic.IEnumerable results, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj index a06db14fffd..371153aa17a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj @@ -17,11 +17,11 @@ AIEval - preview + normal true false - 66 - 0 + n/a + n/a diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json new file mode 100644 index 00000000000..e4404f0e2c2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json @@ -0,0 +1,437 @@ +{ + "Name": "Microsoft.Extensions.AI.Evaluation.Reporting, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails.ChatDetails(System.Collections.Generic.IList turnDetails);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails.ChatDetails(System.Collections.Generic.IEnumerable turnDetails);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails.ChatDetails(params Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails[] turnDetails);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails.TurnDetails { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetailsExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetailsExtensions.AddTurnDetails(this Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails chatDetails, System.Collections.Generic.IEnumerable turnDetails);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetailsExtensions.AddTurnDetails(this Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails chatDetails, params Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails[] turnDetails);", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.ChatTurnDetails(System.TimeSpan latency, string? model = null, Microsoft.Extensions.AI.UsageDetails? usage = null, string? cacheKey = null, bool? cacheHit = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "bool? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.CacheHit { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.CacheKey { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.TimeSpan Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.Latency { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.Model { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.UsageDetails? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.Usage { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.Defaults", + "Stage": "Stable", + "Fields": [ + { + "Member": "const string Microsoft.Extensions.AI.Evaluation.Reporting.Defaults.DefaultExecutionName", + "Stage": "Stable", + "Value": "Default" + }, + { + "Member": "const string Microsoft.Extensions.AI.Evaluation.Reporting.Defaults.DefaultIterationName", + "Stage": "Stable", + "Value": "1" + } + ], + "Properties": [ + { + "Member": "static System.TimeSpan Microsoft.Extensions.AI.Evaluation.Reporting.Defaults.DefaultTimeToLiveForCacheEntries { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedReportingConfiguration", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedReportingConfiguration.Create(string storageRootPath, System.Collections.Generic.IEnumerable evaluators, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, bool enableResponseCaching = true, System.TimeSpan? timeToLiveForCacheEntries = null, System.Collections.Generic.IEnumerable? cachingKeys = null, string executionName = \"Default\", System.Func? evaluationMetricInterpreter = null, System.Collections.Generic.IEnumerable? tags = null);", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider.DiskBasedResponseCacheProvider(string storageRootPath, System.TimeSpan? timeToLiveForCacheEntries = null);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider.DeleteExpiredCacheEntriesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider.GetCacheAsync(string scenarioName, string iterationName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider.ResetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.DiskBasedResultStore(string storageRootPath);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.DeleteResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.GetIterationNamesAsync(string executionName, string scenarioName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.GetLatestExecutionNamesAsync(int? count = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.GetScenarioNamesAsync(string executionName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.ReadResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResultStore.WriteResultsAsync(System.Collections.Generic.IEnumerable results, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Html.HtmlReportWriter : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Html.HtmlReportWriter.HtmlReportWriter(string reportFilePath);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Html.HtmlReportWriter.WriteReportAsync(System.Collections.Generic.IEnumerable scenarioRunResults, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter.WriteReportAsync(System.Collections.Generic.IEnumerable scenarioRunResults, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider.DeleteExpiredCacheEntriesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider.GetCacheAsync(string scenarioName, string iterationName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider.ResetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.DeleteResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.GetIterationNamesAsync(string executionName, string scenarioName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.GetLatestExecutionNamesAsync(int? count = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.GetScenarioNamesAsync(string executionName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.ReadResultsAsync(string? executionName = null, string? scenarioName = null, string? iterationName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore.WriteResultsAsync(System.Collections.Generic.IEnumerable results, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' + // This is needed until ICSharpCode.Decompiler adds params collection support + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Json.JsonReportWriter : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Json.JsonReportWriter.JsonReportWriter(string reportFilePath);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Json.JsonReportWriter.WriteReportAsync(System.Collections.Generic.IEnumerable scenarioRunResults, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.ReportingConfiguration(System.Collections.Generic.IEnumerable evaluators, Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore resultStore, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider? responseCacheProvider = null, System.Collections.Generic.IEnumerable? cachingKeys = null, string executionName = \"Default\", System.Func? evaluationMetricInterpreter = null, System.Collections.Generic.IEnumerable? tags = null);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.CreateScenarioRunAsync(string scenarioName, string iterationName = \"1\", System.Collections.Generic.IEnumerable? additionalCachingKeys = null, System.Collections.Generic.IEnumerable? additionalTags = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyList Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.CachingKeys { get; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.ChatConfiguration? Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.ChatConfiguration { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Func? Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.EvaluationMetricInterpreter { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IReadOnlyList Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.Evaluators { get; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.ExecutionName { get; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider? Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.ResponseCacheProvider { get; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.ResultStore { get; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IReadOnlyList? Microsoft.Extensions.AI.Evaluation.Reporting.ReportingConfiguration.Tags { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun : System.IAsyncDisposable", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.DisposeAsync();", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.ChatConfiguration? Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.ChatConfiguration { get; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.ExecutionName { get; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.IterationName { get; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun.ScenarioName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, string modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, string userRequest, string modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, Microsoft.Extensions.AI.ChatMessage modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, Microsoft.Extensions.AI.ChatResponse modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, Microsoft.Extensions.AI.ChatMessage userRequest, Microsoft.Extensions.AI.ChatMessage modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRun scenarioRun, Microsoft.Extensions.AI.ChatMessage userRequest, Microsoft.Extensions.AI.ChatResponse modelResponse, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ScenarioRunResult(string scenarioName, string iterationName, string executionName, System.DateTime creationTime, System.Collections.Generic.IList messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.EvaluationResult evaluationResult, Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails? chatDetails = null, System.Collections.Generic.IList? tags = null, int? formatVersion = null);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ScenarioRunResult(string scenarioName, string iterationName, string executionName, System.DateTime creationTime, System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.EvaluationResult evaluationResult, Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails? chatDetails = null, System.Collections.Generic.IEnumerable? tags = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails? Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ChatDetails { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.DateTime Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.CreationTime { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationResult Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.EvaluationResult { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ExecutionName { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.FormatVersion { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.IterationName { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.Messages { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ChatResponse Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ModelResponse { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.ScenarioName { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult.Tags { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResultExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static bool Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResultExtensions.ContainsDiagnostics(this Microsoft.Extensions.AI.Evaluation.Reporting.ScenarioRunResult result, System.Func? predicate = null);", + "Stage": "Stable" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj index 3f098cf3026..cd496877960 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj @@ -1,4 +1,4 @@ - + A library that defines core abstractions and types for supporting evaluation. @@ -8,11 +8,11 @@ AIEval - preview + normal true false - 56 - 0 + n/a + n/a diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.json new file mode 100644 index 00000000000..59fc0cad90a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.json @@ -0,0 +1,491 @@ +{ + "Name": "Microsoft.Extensions.AI.Evaluation, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.BooleanMetric : Microsoft.Extensions.AI.Evaluation.EvaluationMetric", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.BooleanMetric.BooleanMetric(string name, bool? value = null, string? reason = null);", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.ChatConfiguration", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.ChatConfiguration.ChatConfiguration(Microsoft.Extensions.AI.IChatClient chatClient);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.IChatClient Microsoft.Extensions.AI.Evaluation.ChatConfiguration.ChatClient { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.ChatMessageExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.ChatMessageExtensions.RenderText(this Microsoft.Extensions.AI.ChatMessage message);", + "Stage": "Stable" + }, + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.ChatMessageExtensions.RenderText(this System.Collections.Generic.IEnumerable messages);", + "Stage": "Stable" + }, + { + "Member": "static bool Microsoft.Extensions.AI.Evaluation.ChatMessageExtensions.TryGetUserRequest(this System.Collections.Generic.IEnumerable messages, out Microsoft.Extensions.AI.ChatMessage? userRequest);", + "Stage": "Stable" + }, + { + "Member": "static bool Microsoft.Extensions.AI.Evaluation.ChatMessageExtensions.TryGetUserRequest(this System.Collections.Generic.IEnumerable messages, out Microsoft.Extensions.AI.ChatMessage? userRequest, out System.Collections.Generic.IReadOnlyList remainingMessages);", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.ChatResponseExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static string Microsoft.Extensions.AI.Evaluation.ChatResponseExtensions.RenderText(this Microsoft.Extensions.AI.ChatResponse response);", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.CompositeEvaluator : Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.CompositeEvaluator.CompositeEvaluator(params Microsoft.Extensions.AI.Evaluation.IEvaluator[] evaluators);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.CompositeEvaluator.CompositeEvaluator(System.Collections.Generic.IEnumerable evaluators);", + "Stage": "Stable" + }, + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.CompositeEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.CompositeEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "abstract class Microsoft.Extensions.AI.Evaluation.EvaluationContext", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationContext.EvaluationContext(string name, System.Collections.Generic.IEnumerable contents);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationContext.EvaluationContext(string name, params Microsoft.Extensions.AI.AIContent[] contents);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationContext.EvaluationContext(string name, string content);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.Evaluation.EvaluationContext.Contents { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.EvaluationContext.Name { get; set; }", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.EvaluationDiagnostic(Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity severity, string message);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.Error(string message);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.Informational(string message);", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.ToString();", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.Warning(string message);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.Message { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic.Severity { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "enum Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity.EvaluationDiagnosticSeverity();", + "Stage": "Stable" + } + ], + "Fields": [ + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity.Error", + "Stage": "Stable", + "Value": "2" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity.Informational", + "Stage": "Stable", + "Value": "0" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity Microsoft.Extensions.AI.Evaluation.EvaluationDiagnosticSeverity.Warning", + "Stage": "Stable", + "Value": "1" + } + ] + }, + { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "class Microsoft.Extensions.AI.Evaluation.EvaluationMetric", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationMetric.EvaluationMetric(string name, string? reason = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Context { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Diagnostics { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Interpretation { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Metadata { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Name { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Reason { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.Evaluation.EvaluationMetric : Microsoft.Extensions.AI.Evaluation.EvaluationMetric", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationMetric.EvaluationMetric(string name, T? value, string? reason = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "T? Microsoft.Extensions.AI.Evaluation.EvaluationMetric.Value { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddDiagnostics(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, System.Collections.Generic.IEnumerable diagnostics);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddDiagnostics(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, params Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic[] diagnostics);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddOrUpdateChatMetadata(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, Microsoft.Extensions.AI.ChatResponse response, System.TimeSpan? duration = null);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddOrUpdateContext(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, System.Collections.Generic.IEnumerable context);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddOrUpdateContext(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, params Microsoft.Extensions.AI.Evaluation.EvaluationContext[] context);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddOrUpdateMetadata(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, string name, string value);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.AddOrUpdateMetadata(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, System.Collections.Generic.IDictionary metadata);", + "Stage": "Stable" + }, + { + "Member": "static bool Microsoft.Extensions.AI.Evaluation.EvaluationMetricExtensions.ContainsDiagnostics(this Microsoft.Extensions.AI.Evaluation.EvaluationMetric metric, System.Func? predicate = null);", + "Stage": "Stable" + } + ] + }, + { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation.EvaluationMetricInterpretation(Microsoft.Extensions.AI.Evaluation.EvaluationRating rating = Microsoft.Extensions.AI.Evaluation.EvaluationRating.Unknown, bool failed = false, string? reason = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "bool Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation.Failed { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation.Rating { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.Evaluation.EvaluationMetricInterpretation.Reason { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "enum Microsoft.Extensions.AI.Evaluation.EvaluationRating", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationRating.EvaluationRating();", + "Stage": "Stable" + } + ], + "Fields": [ + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Average", + "Stage": "Stable", + "Value": "4" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Exceptional", + "Stage": "Stable", + "Value": "6" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Good", + "Stage": "Stable", + "Value": "5" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Inconclusive", + "Stage": "Stable", + "Value": "1" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Poor", + "Stage": "Stable", + "Value": "3" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Unacceptable", + "Stage": "Stable", + "Value": "2" + }, + { + "Member": "const Microsoft.Extensions.AI.Evaluation.EvaluationRating Microsoft.Extensions.AI.Evaluation.EvaluationRating.Unknown", + "Stage": "Stable", + "Value": "0" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.EvaluationResult", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationResult.EvaluationResult(System.Collections.Generic.IDictionary metrics);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationResult.EvaluationResult(System.Collections.Generic.IEnumerable metrics);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.EvaluationResult.EvaluationResult(params Microsoft.Extensions.AI.Evaluation.EvaluationMetric[] metrics);", + "Stage": "Stable" + }, + { + "Member": "T Microsoft.Extensions.AI.Evaluation.EvaluationResult.Get(string metricName);", + "Stage": "Stable" + }, + { + "Member": "bool Microsoft.Extensions.AI.Evaluation.EvaluationResult.TryGet(string metricName, out T? value);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IDictionary Microsoft.Extensions.AI.Evaluation.EvaluationResult.Metrics { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddDiagnosticsToAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, System.Collections.Generic.IEnumerable diagnostics);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddDiagnosticsToAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, params Microsoft.Extensions.AI.Evaluation.EvaluationDiagnostic[] diagnostics);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddOrUpdateChatMetadataInAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, Microsoft.Extensions.AI.ChatResponse response, System.TimeSpan? duration = null);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddOrUpdateContextInAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, System.Collections.Generic.IEnumerable context);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddOrUpdateContextInAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, params Microsoft.Extensions.AI.Evaluation.EvaluationContext[] context);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddOrUpdateMetadataInAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, string name, string value);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.AddOrUpdateMetadataInAllMetrics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, System.Collections.Generic.IDictionary metadata);", + "Stage": "Stable" + }, + { + "Member": "static bool Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.ContainsDiagnostics(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, System.Func? predicate = null);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.AI.Evaluation.EvaluationResultExtensions.Interpret(this Microsoft.Extensions.AI.Evaluation.EvaluationResult result, System.Func interpretationProvider);", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, string modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, string userRequest, string modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, Microsoft.Extensions.AI.ChatMessage modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, Microsoft.Extensions.AI.ChatMessage userRequest, Microsoft.Extensions.AI.ChatMessage modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.EvaluatorExtensions.EvaluateAsync(this Microsoft.Extensions.AI.Evaluation.IEvaluator evaluator, Microsoft.Extensions.AI.ChatMessage userRequest, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.Evaluation.IEvaluator", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.Evaluation.IEvaluator.EvaluateAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatResponse modelResponse, Microsoft.Extensions.AI.Evaluation.ChatConfiguration? chatConfiguration = null, System.Collections.Generic.IEnumerable? additionalContext = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyCollection Microsoft.Extensions.AI.Evaluation.IEvaluator.EvaluationMetricNames { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.NumericMetric : Microsoft.Extensions.AI.Evaluation.EvaluationMetric", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.NumericMetric.NumericMetric(string name, double? value = null, string? reason = null);", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.Evaluation.StringMetric : Microsoft.Extensions.AI.Evaluation.EvaluationMetric", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.Evaluation.StringMetric.StringMetric(string name, string? value = null, string? reason = null);", + "Stage": "Stable" + } + ] + } + ] +} \ No newline at end of file From 1daacd7cbea7892de9dd61b5d75fbaf61afa5f9b Mon Sep 17 00:00:00 2001 From: Shyam N Date: Thu, 15 May 2025 19:20:24 -0700 Subject: [PATCH 116/472] Fix up comments in eval API json files (#6452) --- ...soft.Extensions.AI.Evaluation.Quality.json | 9 +++++++ ...ensions.AI.Evaluation.Reporting.Azure.json | 10 ++++---- ...ft.Extensions.AI.Evaluation.Reporting.json | 24 +++++++++---------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json index 9b7dd3c022e..d09ee7c900a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.json @@ -50,6 +50,9 @@ ] }, { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.CompletenessEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", "Stage": "Stable", "Methods": [ @@ -94,6 +97,9 @@ ] }, { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.EquivalenceEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", "Stage": "Stable", "Methods": [ @@ -162,6 +168,9 @@ ] }, { + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Quality.GroundednessEvaluatorContext : Microsoft.Extensions.AI.Evaluation.EvaluationContext", "Stage": "Stable", "Methods": [ diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json index 98615cb747a..80e1bcb9377 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.json @@ -12,9 +12,9 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support - // See: https://github.com/icsharpcode/ILSpy/issues/829 + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResponseCacheProvider : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider", "Stage": "Stable", "Methods": [ @@ -37,8 +37,8 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.AzureStorageResultStore : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResultStore", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json index e4404f0e2c2..165fbd37d82 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json @@ -40,9 +40,9 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support - // See: https://github.com/icsharpcode/ILSpy/issues/829 + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails", "Stage": "Stable", "Methods": [ @@ -107,9 +107,9 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support - // See: https://github.com/icsharpcode/ILSpy/issues/829 + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Storage.DiskBasedResponseCacheProvider : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationResponseCacheProvider", "Stage": "Stable", "Methods": [ @@ -166,9 +166,9 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support - // See: https://github.com/icsharpcode/ILSpy/issues/829 + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Html.HtmlReportWriter : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter", "Stage": "Stable", "Methods": [ @@ -241,9 +241,9 @@ ] }, { - // After generating the baseline, manually edit this file to have 'params' instead of 'scoped' - // This is needed until ICSharpCode.Decompiler adds params collection support - // See: https://github.com/icsharpcode/ILSpy/issues/829 + // After generating the baseline, manually edit this file to remove primary constructor portion + // This is needed until ICSharpCode.Decompiler adds support for primary constructors + // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Json.JsonReportWriter : Microsoft.Extensions.AI.Evaluation.Reporting.IEvaluationReportWriter", "Stage": "Stable", "Methods": [ From f833f084ca533fe5da226703a76a002f6eaff8ee Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Fri, 16 May 2025 11:01:30 -0700 Subject: [PATCH 117/472] Fix issues in the MEAI Templates (#6454) * Use stable version for MEAI in templates * Simplify MEAI template package version definitions * Update MEAI Template README with the GitHub models:read PAT permission * Fix vector index name in MEAI template to adhere to Azure AI Search rules * Fix copy pasta in Aspire template readme * Add AzureOpenAI_Qdrant_Aspire and Ollama_Qdrant snapshot template tests --------- Co-authored-by: Mackinnon Buck --- eng/Versions.props | 19 - src/ProjectTemplates/GeneratedContent.targets | 85 +- .../.template.config/template.json | 4 +- ...hatWithCustomData-CSharp.AppHost.csproj.in | 8 +- ...ustomData-CSharp.ServiceDefaults.csproj.in | 14 +- .../ChatWithCustomData-CSharp.Web.csproj.in | 40 +- .../ChatWithCustomData-CSharp.Web/README.md | 4 +- .../src/ChatWithCustomData/README.Aspire.md | 2 +- .../AIChatWebSnapshotTests.cs | 16 +- .../aichatweb/README.md | 90 + .../aichatweb/aichatweb.AppHost/Program.cs | 19 + .../Properties/launchSettings.json | 29 + .../aichatweb.AppHost.csproj | 22 + .../appsettings.Development.json | 8 + .../aichatweb.AppHost/appsettings.json | 9 + .../aichatweb.ServiceDefaults/Extensions.cs | 125 + .../aichatweb.ServiceDefaults.csproj | 22 + .../aichatweb.Web/Components/App.razor | 24 + .../Components/Layout/LoadingSpinner.razor | 1 + .../Layout/LoadingSpinner.razor.css | 89 + .../Components/Layout/MainLayout.razor | 9 + .../Components/Layout/MainLayout.razor.css | 20 + .../Components/Layout/SurveyPrompt.razor | 13 + .../Components/Layout/SurveyPrompt.razor.css | 20 + .../Components/Pages/Chat/Chat.razor | 116 + .../Components/Pages/Chat/Chat.razor.css | 11 + .../Components/Pages/Chat/ChatCitation.razor | 38 + .../Pages/Chat/ChatCitation.razor.css | 37 + .../Components/Pages/Chat/ChatHeader.razor | 17 + .../Pages/Chat/ChatHeader.razor.css | 25 + .../Components/Pages/Chat/ChatInput.razor | 51 + .../Components/Pages/Chat/ChatInput.razor.css | 57 + .../Components/Pages/Chat/ChatInput.razor.js | 43 + .../Pages/Chat/ChatMessageItem.razor | 94 + .../Pages/Chat/ChatMessageItem.razor.css | 120 + .../Pages/Chat/ChatMessageList.razor | 42 + .../Pages/Chat/ChatMessageList.razor.css | 22 + .../Pages/Chat/ChatMessageList.razor.js | 34 + .../Pages/Chat/ChatSuggestions.razor | 78 + .../Pages/Chat/ChatSuggestions.razor.css | 9 + .../Components/Pages/Error.razor | 36 + .../aichatweb.Web/Components/Routes.razor | 6 + .../aichatweb.Web/Components/_Imports.razor | 13 + .../aichatweb/aichatweb.Web/Program.cs | 53 + .../Properties/launchSettings.json | 23 + .../aichatweb.Web/Services/IngestedChunk.cs | 21 + .../Services/IngestedDocument.cs | 22 + .../Services/Ingestion/DataIngestor.cs | 60 + .../Services/Ingestion/IIngestionSource.cs | 14 + .../Services/Ingestion/PDFDirectorySource.cs | 75 + .../aichatweb.Web/Services/SemanticSearch.cs | 22 + .../aichatweb.Web/aichatweb.Web.csproj | 25 + .../appsettings.Development.json | 9 + .../aichatweb/aichatweb.Web/appsettings.json | 10 + .../Data/Example_Emergency_Survival_Kit.pdf | Bin 0 -> 153539 bytes .../wwwroot/Data/Example_GPS_Watch.pdf | Bin 0 -> 117692 bytes .../aichatweb/aichatweb.Web/wwwroot/app.css | 94 + .../aichatweb/aichatweb.Web/wwwroot/app.js | 24 + .../aichatweb.Web/wwwroot/favicon.ico | Bin 0 -> 13126 bytes .../wwwroot/lib/dompurify/README.md | 5 + .../wwwroot/lib/dompurify/dist/purify.es.mjs | 1338 +++ .../wwwroot/lib/marked/README.md | 5 + .../wwwroot/lib/marked/dist/marked.esm.js | 2568 +++++ .../wwwroot/lib/pdf_viewer/viewer.html | 31 + .../wwwroot/lib/pdf_viewer/viewer.mjs | 62 + .../wwwroot/lib/pdfjs-dist/README.md | 10 + .../lib/pdfjs-dist/dist/build/pdf.min.mjs | 21 + .../pdfjs-dist/dist/build/pdf.worker.min.mjs | 21 + .../dist/web/images/loading-icon.gif | Bin 0 -> 2545 bytes .../lib/pdfjs-dist/dist/web/pdf_viewer.css | 3274 +++++++ .../lib/pdfjs-dist/dist/web/pdf_viewer.mjs | 8435 +++++++++++++++++ .../wwwroot/lib/tailwindcss/README.md | 7 + .../lib/tailwindcss/dist/preflight.css | 383 + .../aichatweb/aichatweb.sln | 34 + .../aichatweb/README.md | 2 +- .../Services/Ingestion/DataIngestor.cs | 4 +- .../aichatweb.Web/Services/SemanticSearch.cs | 2 +- .../aichatweb/README.md | 60 + .../aichatweb/aichatweb.AppHost/Program.cs | 22 + .../Properties/launchSettings.json | 29 + .../aichatweb.AppHost.csproj | 24 + .../appsettings.Development.json | 8 + .../aichatweb.AppHost/appsettings.json | 9 + .../aichatweb.ServiceDefaults/Extensions.cs | 133 + .../aichatweb.ServiceDefaults.csproj | 22 + .../aichatweb.Web/Components/App.razor | 24 + .../Components/Layout/LoadingSpinner.razor | 1 + .../Layout/LoadingSpinner.razor.css | 89 + .../Components/Layout/MainLayout.razor | 9 + .../Components/Layout/MainLayout.razor.css | 20 + .../Components/Layout/SurveyPrompt.razor | 13 + .../Components/Layout/SurveyPrompt.razor.css | 20 + .../Components/Pages/Chat/Chat.razor | 109 + .../Components/Pages/Chat/Chat.razor.css | 11 + .../Components/Pages/Chat/ChatCitation.razor | 38 + .../Pages/Chat/ChatCitation.razor.css | 37 + .../Components/Pages/Chat/ChatHeader.razor | 17 + .../Pages/Chat/ChatHeader.razor.css | 25 + .../Components/Pages/Chat/ChatInput.razor | 51 + .../Components/Pages/Chat/ChatInput.razor.css | 57 + .../Components/Pages/Chat/ChatInput.razor.js | 43 + .../Pages/Chat/ChatMessageItem.razor | 94 + .../Pages/Chat/ChatMessageItem.razor.css | 120 + .../Pages/Chat/ChatMessageList.razor | 42 + .../Pages/Chat/ChatMessageList.razor.css | 22 + .../Pages/Chat/ChatMessageList.razor.js | 34 + .../Pages/Chat/ChatSuggestions.razor | 78 + .../Pages/Chat/ChatSuggestions.razor.css | 9 + .../Components/Pages/Error.razor | 36 + .../aichatweb.Web/Components/Routes.razor | 6 + .../aichatweb.Web/Components/_Imports.razor | 13 + .../aichatweb/aichatweb.Web/Program.cs | 53 + .../Properties/launchSettings.json | 23 + .../aichatweb.Web/Services/IngestedChunk.cs | 21 + .../Services/IngestedDocument.cs | 22 + .../Services/Ingestion/DataIngestor.cs | 60 + .../Services/Ingestion/IIngestionSource.cs | 14 + .../Services/Ingestion/PDFDirectorySource.cs | 75 + .../aichatweb.Web/Services/SemanticSearch.cs | 22 + .../aichatweb.Web/aichatweb.Web.csproj | 25 + .../appsettings.Development.json | 9 + .../aichatweb/aichatweb.Web/appsettings.json | 10 + .../Data/Example_Emergency_Survival_Kit.pdf | Bin 0 -> 153539 bytes .../wwwroot/Data/Example_GPS_Watch.pdf | Bin 0 -> 117692 bytes .../aichatweb/aichatweb.Web/wwwroot/app.css | 94 + .../aichatweb/aichatweb.Web/wwwroot/app.js | 24 + .../aichatweb.Web/wwwroot/favicon.ico | Bin 0 -> 13126 bytes .../wwwroot/lib/dompurify/README.md | 5 + .../wwwroot/lib/dompurify/dist/purify.es.mjs | 1338 +++ .../wwwroot/lib/marked/README.md | 5 + .../wwwroot/lib/marked/dist/marked.esm.js | 2568 +++++ .../wwwroot/lib/pdf_viewer/viewer.html | 31 + .../wwwroot/lib/pdf_viewer/viewer.mjs | 62 + .../wwwroot/lib/pdfjs-dist/README.md | 10 + .../lib/pdfjs-dist/dist/build/pdf.min.mjs | 21 + .../pdfjs-dist/dist/build/pdf.worker.min.mjs | 21 + .../dist/web/images/loading-icon.gif | Bin 0 -> 2545 bytes .../lib/pdfjs-dist/dist/web/pdf_viewer.css | 3274 +++++++ .../lib/pdfjs-dist/dist/web/pdf_viewer.mjs | 8435 +++++++++++++++++ .../wwwroot/lib/tailwindcss/README.md | 7 + .../lib/tailwindcss/dist/preflight.css | 383 + .../aichatweb/aichatweb.sln | 34 + .../aichatweb/README.md | 2 +- 143 files changed, 36067 insertions(+), 101 deletions(-) create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/App.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Routes.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/_Imports.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.Development.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_GPS_Watch.pdf create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/favicon.ico create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/dompurify/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/dompurify/dist/purify.es.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.sln create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/App.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Routes.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/_Imports.razor create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.Development.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_GPS_Watch.pdf create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/favicon.ico create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/dompurify/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/dompurify/dist/purify.es.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.sln diff --git a/eng/Versions.props b/eng/Versions.props index 27329a85d6a..e401281109f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -155,25 +155,6 @@ 4.8.0 3.3.4 - - 9.2.1 - 9.2.1-preview.1.25222.1 - 1.0.0-beta.6 - 2.2.0-beta.4 - 1.14.0 - 11.6.0 - 9.4.1-beta.277 - 9.4.1-beta.277 - 9.4.1-beta.277 - 9.4.1-beta.277 - 9.2.0 - 1.50.0-preview - 1.50.0-preview - 1.50.0 - 5.1.16 - 1.9.0 - 0.1.10 - 6.0.1 - - 9.4.0 - 9.4.0-preview.1.25207.5 - 9.0.4 - - - false - false - - - $(TemplatePinnedRepoPackagesVersion) - $(TemplatePinnedRepoAIPackagesVersion) - $(TemplatePinnedMicrosoftEntityFrameworkCoreSqliteVersion) + $(Version) + $(Version) + $(Version) + - - $(Version) - $(Version) - $(MicrosoftEntityFrameworkCoreSqliteVersion) + + + 9.2.1 + 9.2.1-preview.1.25222.1 + 2.2.0-beta.4 + 1.0.0-beta.6 + 1.14.0 + 11.6.0 + 9.4.1-beta.277 + 9.2.0 + 1.50.0 + 1.50.0-preview + 5.1.16 + 1.9.0 + 0.1.10 + 6.0.1 + - <_TemplateUsingJustBuiltPackages Condition="'$(TemplateRepoAIPackagesVersion)' == '$(Version)'">true + + <_TemplateUsingJustBuiltPackages Condition="'$(TemplatePackageVersion_MicrosoftExtensionsAI)' == '$(Version)' OR '$(TemplatePackageVersion_MicrosoftExtensionsAI_Preview)' == '$(Version)'">true @@ -43,27 +54,23 @@ ArtifactsShippingPackagesDir=$(ArtifactsShippingPackagesDir); - AspireVersion=$(AspireVersion); - AspireAzureAIOpenAIVersion=$(AspireAzureAIOpenAIVersion); - AzureAIProjectsVersion=$(AzureAIProjectsVersion); - AzureAIOpenAIVersion=$(AzureAIOpenAIVersion); - AzureIdentityVersion=$(AzureIdentityVersion); - AzureSearchDocumentsVersion=$(AzureSearchDocumentsVersion); - CommunityToolkitAspireHostingOllamaVersion=$(CommunityToolkitAspireHostingOllamaVersion); - CommunityToolkitAspireHostingSqliteVersion=$(CommunityToolkitAspireHostingSqliteVersion); - CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion=$(CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion); - CommunityToolkitAspireOllamaSharpVersion=$(CommunityToolkitAspireOllamaSharpVersion); - MicrosoftEntityFrameworkCoreSqliteVersion=$(TemplateMicrosoftEntityFrameworkCoreSqliteVersion); - MicrosoftExtensionsAIVersion=$(TemplateRepoAIPackagesVersion); - MicrosoftExtensionsHttpResilienceVersion=$(TemplateRepoPackagesVersion); - MicrosoftExtensionsServiceDiscoveryVersion=$(MicrosoftExtensionsServiceDiscoveryVersion); - MicrosoftSemanticKernelConnectorsAzureAISearchVersion=$(MicrosoftSemanticKernelConnectorsAzureAISearchVersion); - MicrosoftSemanticKernelConnectorsQdrantVersion=$(MicrosoftSemanticKernelConnectorsQdrantVersion); - MicrosoftSemanticKernelCoreVersion=$(MicrosoftSemanticKernelCoreVersion); - OllamaSharpVersion=$(OllamaSharpVersion); - OpenTelemetryVersion=$(OpenTelemetryVersion); - PdfPigVersion=$(PdfPigVersion); - SystemLinqAsyncVersion=$(SystemLinqAsyncVersion); + TemplatePackageVersion_MicrosoftExtensionsAI=$(TemplatePackageVersion_MicrosoftExtensionsAI); + TemplatePackageVersion_MicrosoftExtensionsAI_Preview=$(TemplatePackageVersion_MicrosoftExtensionsAI_Preview); + TemplatePackageVersion_MicrosoftExtensionsHttpResilience=$(TemplatePackageVersion_MicrosoftExtensionsHttpResilience); + TemplatePackageVersion_Aspire=$(TemplatePackageVersion_Aspire); + TemplatePackageVersion_Aspire_Preview=$(TemplatePackageVersion_Aspire_Preview); + TemplatePackageVersion_AzureAIOpenAI=$(TemplatePackageVersion_AzureAIOpenAI); + TemplatePackageVersion_AzureAIProjects=$(TemplatePackageVersion_AzureAIProjects); + TemplatePackageVersion_AzureIdentity=$(TemplatePackageVersion_AzureIdentity); + TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); + TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire); + TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery); + TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel); + TemplatePackageVersion_MicrosoftSemanticKernel_Preview=$(TemplatePackageVersion_MicrosoftSemanticKernel_Preview); + TemplatePackageVersion_OllamaSharp=$(TemplatePackageVersion_OllamaSharp); + TemplatePackageVersion_OpenTelemetry=$(TemplatePackageVersion_OpenTelemetry); + TemplatePackageVersion_PdfPig=$(TemplatePackageVersion_PdfPig); + TemplatePackageVersion_SystemLinqAsync=$(TemplatePackageVersion_SystemLinqAsync); LocalChatTemplateVariant=$(_LocalChatTemplateVariant); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index e895dc5db26..2f9a293b32e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -491,7 +491,7 @@ "type": "derived", "valueSource": "name", "valueTransform": "vectorStoreIndexNameTransform", - "replaces": "data-ChatWithCustomData-CSharp.Web-ingestion" + "replaces": "data-ChatWithCustomData-CSharp.Web-" }, "webProjectNamespaceAdjuster": { "type": "generated", @@ -553,7 +553,7 @@ "vectorStoreIndexName_PrefixSuffix": { "identifier": "replace", "pattern": "^(.*)$", - "replacement": "data-$1-ingested", + "replacement": "data-$1-", "description": "Produces a meaningful name parameterized by project name; ensures first, second, and last characters are valid" } }, diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index fa5d922d53b..d7287e9301a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -1,6 +1,6 @@ - + Exe @@ -12,12 +12,12 @@ - + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/ChatWithCustomData-CSharp.ServiceDefaults.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/ChatWithCustomData-CSharp.ServiceDefaults.csproj.in index 77276eab4a0..3b67ba158cd 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/ChatWithCustomData-CSharp.ServiceDefaults.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/ChatWithCustomData-CSharp.ServiceDefaults.csproj.in @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 25798eeb26c..e13c7eb63ee 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -9,42 +9,42 @@ - - + + - - + + - + - - - - + + + + - - + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md index 37f10b83ce2..a828e164ff8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md @@ -22,7 +22,7 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See #### ---#endif # Configure the AI Model Provider #### ---#if (IsGHModels) -To use models hosted by GitHub Models, you will need to create a GitHub personal access token. The token should not have any scopes or permissions. See [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). +To use models hosted by GitHub Models, you will need to create a GitHub personal access token with `models:read` permissions, but no other scopes or permissions. See [Prototyping with AI models](https://docs.github.com/github-models/prototyping-with-ai-models) and [Managing your personal access tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) in the GitHub Docs for more information. #### ---#if (hostIdentifier == "vs") Configure your token for this project using .NET User Secrets: @@ -168,7 +168,7 @@ To use Azure AI Search, you will need an Azure account and an Azure AI Search re ### 1. Create an Azure AI Search Resource Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-ingestion` index using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. +Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-chunks` and `data-ChatWithCustomData-CSharp.Web-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. #### ---#if (UseManagedIdentity) ### 2. Configure Azure AI Search for Keyless Authentication diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index f7c944dacc8..ba4b5bf788b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -125,7 +125,7 @@ To use Azure AI Search, you will need an Azure account and an Azure AI Search re ### 1. Create an Azure AI Search Resource Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-ingestion` index using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. +Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-chunks` and `data-ChatWithCustomData-CSharp.Web-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. ### 3. Configure API Key and Endpoint Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs index 1e4cf0415f4..e02ac53de0d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs @@ -49,9 +49,9 @@ public async Task BasicTest() } [Fact] - public async Task BasicAspireTest() + public async Task Ollama_Qdrant() { - await TestTemplateCoreAsync(scenarioName: "BasicAspire", templateArgs: ["--aspire"]); + await TestTemplateCoreAsync(scenarioName: "Ollama_Qdrant", templateArgs: ["--provider", "ollama", "--vector-store", "qdrant"]); } [Fact] @@ -60,6 +60,18 @@ public async Task OpenAI_AzureAISearch() await TestTemplateCoreAsync(scenarioName: "OpenAI_AzureAISearch", templateArgs: ["--provider", "openai", "--vector-store", "azureaisearch"]); } + [Fact] + public async Task BasicAspireTest() + { + await TestTemplateCoreAsync(scenarioName: "BasicAspire", templateArgs: ["--aspire"]); + } + + [Fact] + public async Task AzureOpenAI_AzureAISearch_Aspire() + { + await TestTemplateCoreAsync(scenarioName: "AzureOpenAI_Qdrant_Aspire", templateArgs: ["--provider", "azureopenai", "--vector-store", "azureaisearch", "--aspire"]); + } + private async Task TestTemplateCoreAsync(string scenarioName, IEnumerable? templateArgs = null) { string workingDir = TestUtils.CreateTemporaryFolder(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md new file mode 100644 index 00000000000..57c0375d302 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md @@ -0,0 +1,90 @@ +# AI Chat with Custom Data + +This project is an AI chat application that demonstrates how to chat with custom data using an AI language model. Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](https://aka.ms/dotnet-chat-templatePreview2-survey). + +>[!NOTE] +> Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. + +### Known Issues + +#### Errors running Ollama or Docker + +A recent incompatibility was found between Ollama and Docker Desktop. This issue results in runtime errors when connecting to Ollama, and the workaround for that can lead to Docker not working for Aspire projects. + +This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See [ollama/ollama#9509](https://github.com/ollama/ollama/issues/9509#issuecomment-2842461831) for more information and a link to install the version of Docker Desktop with the fix. + +# Configure the AI Model Provider + +## Using Azure OpenAI + +To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service resource. For detailed instructions, see the [Azure OpenAI Service documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource). + +### 1. Create an Azure OpenAI Service Resource +[Create an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). + +### 2. Deploy the Models +Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). + +### 3. Configure API Key and Endpoint +Configure your Azure OpenAI API key and endpoint for this project, using .NET User Secrets: + 1. In the Azure Portal, navigate to your Azure OpenAI resource. + 2. Copy the "Endpoint" URL and "Key 1" from the "Keys and Endpoint" section. + 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: + + ```sh + cd aichatweb.AppHost + dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" + ``` + +Make sure to replace `YOUR-API-KEY` and `YOUR-DEPLOYMENT-NAME` with your actual Azure OpenAI key and endpoint. Make sure your endpoint URL is formatted like https://YOUR-DEPLOYMENT-NAME.openai.azure.com/ (do not include any path after .openai.azure.com/). + +## Configure Azure AI Search + +To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). + +### 1. Create an Azure AI Search Resource +Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. + +Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-aichatweb-chunks` and `data-aichatweb-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. + +### 3. Configure API Key and Endpoint + Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: + 1. In the Azure Portal, navigate to your Azure AI Search resource. + 2. Copy the "Endpoint" URL and "Primary admin key" from the "Keys" section. + 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: + + ```sh + cd aichatweb.AppHost + dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" + ``` + +Make sure to replace `YOUR-DEPLOYMENT-NAME` and `YOUR-API-KEY` with your actual Azure AI Search deployment name and key. + +# Running the application + +## Using Visual Studio + +1. Open the `.sln` file in Visual Studio. +2. Press `Ctrl+F5` or click the "Start" button in the toolbar to run the project. + +## Using Visual Studio Code + +1. Open the project folder in Visual Studio Code. +2. Install the [C# Dev Kit extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) for Visual Studio Code. +3. Once installed, Open the `Program.cs` file in the aichatweb.AppHost project. +4. Run the project by clicking the "Run" button in the Debug view. + +## Trust the localhost certificate + +Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. + +See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. + +# Updating JavaScript dependencies + +This template leverages JavaScript libraries to provide essential functionality. These libraries are located in the wwwroot/lib folder of the aichatweb.Web project. For instructions on updating each dependency, please refer to the README.md file in each respective folder. + +# Learn More +To learn more about development with .NET and AI, check out the following links: + +* [AI for .NET Developers](https://learn.microsoft.com/dotnet/ai/) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs new file mode 100644 index 00000000000..80803d78d74 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs @@ -0,0 +1,19 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// You will need to set the connection string to your own value +// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: +// cd this-project-directory +// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" +var openai = builder.AddConnectionString("openai"); + +// You will need to set the connection string to your own value +// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: +// cd this-project-directory +// dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" +var azureAISearch = builder.AddConnectionString("azureAISearch"); + +var webApp = builder.AddProject("aichatweb-app"); +webApp.WithReference(openai); +webApp.WithReference(azureAISearch); + +builder.Build().Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..4444e808585 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:9999;http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj new file mode 100644 index 00000000000..1eb887382c9 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -0,0 +1,22 @@ + + + + + + Exe + net9.0 + enable + enable + true + secret + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json new file mode 100644 index 00000000000..b0bacf42851 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.json new file mode 100644 index 00000000000..bfad98588cd --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000000..f56908872e0 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { +#pragma warning disable EXTEXP0001 // RemoveAllResilienceHandlers is experimental + http.RemoveAllResilienceHandlers(); +#pragma warning restore EXTEXP0001 + + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddMeter("Experimental.Microsoft.Extensions.AI"); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddSource("Experimental.Microsoft.Extensions.AI"); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj new file mode 100644 index 00000000000..74656777aaf --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/App.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/App.razor new file mode 100644 index 00000000000..262359d5f5a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/App.razor @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + +@code { + private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor new file mode 100644 index 00000000000..116455ce45b --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor @@ -0,0 +1 @@ +
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css new file mode 100644 index 00000000000..d85b851a679 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css @@ -0,0 +1,89 @@ +/* Used under CC0 license */ + +.lds-ellipsis { + color: #666; + animation: fade-in 1s; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + + .lds-ellipsis, + .lds-ellipsis div { + box-sizing: border-box; + } + +.lds-ellipsis { + margin: auto; + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .lds-ellipsis div { + position: absolute; + top: 33.33333px; + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + + .lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; + } + + .lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; + } + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(0); + } +} + +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(24px, 0); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor new file mode 100644 index 00000000000..96fbbe6cc42 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000000..cda2020dcb0 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor new file mode 100644 index 00000000000..77557f20173 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor @@ -0,0 +1,13 @@ +
+ + +
+ How well is this template working for you? Please take a + brief survey + and tell us what you think. +
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css new file mode 100644 index 00000000000..c939b902afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css @@ -0,0 +1,20 @@ +.surveyContainer { + display: flex; + justify-content: center; + gap: 0.5rem; + font-size: 0.9em; + margin: 0.5rem auto -0.7rem auto; + max-width: 1024px; + color: #444; +} + + .surveyContainer a { + text-decoration: underline; + } + + .surveyContainer .tool-icon { + margin-top: 0.15rem; + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor new file mode 100644 index 00000000000..a7b1502d894 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -0,0 +1,116 @@ +@page "/" +@using System.ComponentModel +@inject IChatClient ChatClient +@inject NavigationManager Nav +@inject SemanticSearch Search +@implements IDisposable + +Chat + + + + + +
To get started, try asking about these example documents. You can replace these with your own data and replace this message.
+ + +
+
+ +
+ + + @* Remove this line to eliminate the template survey message *@ +
+ +@code { + private const string SystemPrompt = @" + You are an assistant who answers questions about information you retrieve. + Do not answer questions about anything else. + Use only simple markdown to format your responses. + + Use the search tool to find relevant information. When you do this, end your + reply with citations in the special XML format: + + exact quote here + + Always include the citation in your response if there are results. + + The quote must be max 5 words, taken word-for-word from the search result, and is the basis for why the citation is relevant. + Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. + "; + + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + } + + [Description("Searches for information using a phrase or keyword")] + private async Task> SearchAsync( + [Description("The phrase to search for.")] string searchPhrase, + [Description("If possible, specify the filename to search that file only. If not provided or empty, the search includes all files.")] string? filenameFilter = null) + { + await InvokeAsync(StateHasChanged); + var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); + return results.Select(result => + $"{result.Text}"); + } + + public void Dispose() + => currentResponseCancellation?.Cancel(); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css new file mode 100644 index 00000000000..98ed1ba7d1e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css @@ -0,0 +1,11 @@ +.chat-container { + position: sticky; + bottom: 0; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 1.5rem; + border-top-width: 1px; + background-color: #F3F4F6; + border-color: #E5E7EB; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor new file mode 100644 index 00000000000..ccb5853cec4 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor @@ -0,0 +1,38 @@ +@using System.Web +@if (!string.IsNullOrWhiteSpace(viewerUrl)) +{ + + + + +
+
@File
+
@Quote
+
+
+} + +@code { + [Parameter] + public required string File { get; set; } + + [Parameter] + public int? PageNumber { get; set; } + + [Parameter] + public required string Quote { get; set; } + + private string? viewerUrl; + + protected override void OnParametersSet() + { + viewerUrl = null; + + // If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here + if (File.EndsWith(".pdf")) + { + var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\''); + viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true"; + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css new file mode 100644 index 00000000000..0ca029b7e64 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css @@ -0,0 +1,37 @@ +.citation { + display: inline-flex; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + margin-top: 1rem; + margin-right: 1rem; + border-bottom: 2px solid #a770de; + gap: 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + background-color: #ffffff; +} + + .citation[href]:hover { + outline: 1px solid #865cb1; + } + + .citation svg { + width: 1.5rem; + height: 1.5rem; + } + + .citation:active { + background-color: rgba(0,0,0,0.05); + } + +.citation-content { + display: flex; + flex-direction: column; +} + +.citation-file { + font-weight: 600; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor new file mode 100644 index 00000000000..12b1d524e23 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor @@ -0,0 +1,17 @@ +
+
+ +
+ +

aichatweb.Web

+
+ +@code { + [Parameter] + public EventCallback OnNewChat { get; set; } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css new file mode 100644 index 00000000000..6adcc414540 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css @@ -0,0 +1,25 @@ +.chat-header-container { + top: 0; + padding: 1.5rem; +} + +.chat-header-controls { + margin-bottom: 1.5rem; +} + +h1 { + overflow: hidden; + text-overflow: ellipsis; +} + +.new-chat-icon { + width: 1.25rem; + height: 1.25rem; + color: rgb(55, 65, 81); +} + +@media (min-width: 768px) { + .chat-header-container { + position: sticky; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor new file mode 100644 index 00000000000..e87ac6ccf47 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor @@ -0,0 +1,51 @@ +@inject IJSRuntime JS + + + + + +@code { + private ElementReference textArea; + private string? messageText; + + [Parameter] + public EventCallback OnSend { get; set; } + + public ValueTask FocusAsync() + => textArea.FocusAsync(); + + private async Task SendMessageAsync() + { + if (messageText is { Length: > 0 } text) + { + messageText = null; + await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text)); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js"); + await module.InvokeVoidAsync("init", textArea); + await module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css new file mode 100644 index 00000000000..3b26c9af316 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css @@ -0,0 +1,57 @@ +.input-box { + display: flex; + flex-direction: column; + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 8px; + padding: 0.5rem 0.75rem; + margin-top: 0.75rem; +} + + .input-box:focus-within { + outline: 2px solid #4152d5; + } + +textarea { + resize: none; + border: none; + outline: none; + flex-grow: 1; +} + + textarea:placeholder-shown + .tools { + --send-button-color: #aaa; + } + +.tools { + display: flex; + margin-top: 1rem; + align-items: center; +} + +.tool-icon { + width: 1.25rem; + height: 1.25rem; +} + +.send-button { + color: var(--send-button-color); + margin-left: auto; +} + + .send-button:hover { + color: black; + } + +.attach { + background-color: white; + border-style: dashed; + color: #888; + border-color: #888; + padding: 3px 8px; +} + + .attach:hover { + background-color: #f0f0f0; + color: black; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js new file mode 100644 index 00000000000..39e18ac7b74 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js @@ -0,0 +1,43 @@ +export function init(elem) { + elem.focus(); + + // Auto-resize whenever the user types or if the value is set programmatically + elem.addEventListener('input', () => resizeToFit(elem)); + afterPropertyWritten(elem, 'value', () => resizeToFit(elem)); + + // Auto-submit the form on 'enter' keypress + elem.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + elem.dispatchEvent(new CustomEvent('change', { bubbles: true })); + elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true })); + } + }); +} + +function resizeToFit(elem) { + const lineHeight = parseFloat(getComputedStyle(elem).lineHeight); + + elem.rows = 1; + const numLines = Math.ceil(elem.scrollHeight / lineHeight); + elem.rows = Math.min(5, Math.max(1, numLines)); +} + +function afterPropertyWritten(target, propName, callback) { + const descriptor = getPropertyDescriptor(target, propName); + Object.defineProperty(target, propName, { + get: function () { + return descriptor.get.apply(this, arguments); + }, + set: function () { + const result = descriptor.set.apply(this, arguments); + callback(); + return result; + } + }); +} + +function getPropertyDescriptor(target, propertyName) { + return Object.getOwnPropertyDescriptor(target, propertyName) + || getPropertyDescriptor(Object.getPrototypeOf(target), propertyName); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor new file mode 100644 index 00000000000..92c20c70667 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor @@ -0,0 +1,94 @@ +@using System.Runtime.CompilerServices +@using System.Text.RegularExpressions +@using System.Linq + +@if (Message.Role == ChatRole.User) +{ +
+ @Message.Text +
+} +else if (Message.Role == ChatRole.Assistant) +{ + foreach (var content in Message.Contents) + { + if (content is TextContent { Text: { Length: > 0 } text }) + { +
+
+
+ + + +
+
+
Assistant
+
+ + + @foreach (var citation in citations ?? []) + { + + } +
+
+ } + else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true) + { + + } + } +} + +@code { + private static readonly ConditionalWeakTable SubscribersLookup = new(); + private static readonly Regex CitationRegex = new(@"(?.*?)", RegexOptions.NonBacktracking); + + private List<(string File, int? Page, string Quote)>? citations; + + [Parameter, EditorRequired] + public required ChatMessage Message { get; set; } + + [Parameter] + public bool InProgress { get; set;} + + protected override void OnInitialized() + { + SubscribersLookup.AddOrUpdate(Message, this); + + if (!InProgress && Message.Role == ChatRole.Assistant && Message.Text is { Length: > 0 } text) + { + ParseCitations(text); + } + } + + public static void NotifyChanged(ChatMessage source) + { + if (SubscribersLookup.TryGetValue(source, out var subscriber)) + { + subscriber.StateHasChanged(); + } + } + + private void ParseCitations(string text) + { + var matches = CitationRegex.Matches(text); + citations = matches.Any() + ? matches.Select(m => (m.Groups["file"].Value, int.TryParse(m.Groups["page"].Value, out var page) ? page : (int?)null, m.Groups["quote"].Value)).ToList() + : null; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css new file mode 100644 index 00000000000..10453454be8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css @@ -0,0 +1,120 @@ +.user-message { + background: rgb(182 215 232); + align-self: flex-end; + min-width: 25%; + max-width: calc(100% - 5rem); + padding: 0.5rem 1.25rem; + border-radius: 0.25rem; + color: #1F2937; + white-space: pre-wrap; +} + +.assistant-message, .assistant-search { + display: grid; + grid-template-rows: min-content; + grid-template-columns: 2rem minmax(0, 1fr); + gap: 0.25rem; +} + +.assistant-message-header { + font-weight: 600; +} + +.assistant-message-text { + grid-column-start: 2; +} + +.assistant-message-icon { + display: flex; + justify-content: center; + align-items: center; + border-radius: 9999px; + width: 1.5rem; + height: 1.5rem; + color: #ffffff; + background: #9b72ce; +} + + .assistant-message-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.assistant-search-icon { + display: flex; + justify-content: center; + align-items: center; + width: 1.5rem; + height: 1.5rem; +} + + .assistant-search-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search-content { + align-content: center; +} + +.assistant-search-phrase { + font-weight: 600; +} + +/* Default styling for markdown-formatted assistant messages */ +::deep ul { + list-style-type: disc; + margin-left: 1.5rem; +} + +::deep ol { + list-style-type: decimal; + margin-left: 1.5rem; +} + +::deep li { + margin: 0.5rem 0; +} + +::deep strong { + font-weight: 600; +} + +::deep h3 { + margin: 1rem 0; + font-weight: 600; +} + +::deep p + p { + margin-top: 1rem; +} + +::deep table { + margin: 1rem 0; +} + +::deep th { + text-align: left; + border-bottom: 1px solid silver; +} + +::deep th, ::deep td { + padding: 0.1rem 0.5rem; +} + +::deep th, ::deep tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); +} + +::deep pre > code { + background-color: white; + display: block; + padding: 0.5rem 1rem; + margin: 1rem 0; + overflow-x: auto; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor new file mode 100644 index 00000000000..d245f455f11 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor @@ -0,0 +1,42 @@ +@inject IJSRuntime JS + +
+ + @foreach (var message in Messages) + { + + } + + @if (InProgressMessage is not null) + { + + + } + else if (IsEmpty) + { +
@NoMessagesContent
+ } +
+
+ +@code { + [Parameter] + public required IEnumerable Messages { get; set; } + + [Parameter] + public ChatMessage? InProgressMessage { get; set; } + + [Parameter] + public RenderFragment? NoMessagesContent { get; set; } + + private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text)); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Activates the auto-scrolling behavior + await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js"); + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css new file mode 100644 index 00000000000..6fbf083c7fa --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css @@ -0,0 +1,22 @@ +.message-list-container { + margin: 2rem 1.5rem; + flex-grow: 1; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.no-messages { + text-align: center; + font-size: 1.25rem; + color: #999; + margin-top: calc(40vh - 18rem); +} + +chat-messages > ::deep div:last-of-type { + /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */ + margin-bottom: 2rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js new file mode 100644 index 00000000000..3de8de273b8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js @@ -0,0 +1,34 @@ +// The following logic provides auto-scroll behavior for the chat messages list. +// If you don't want that behavior, you can simply not load this module. + +window.customElements.define('chat-messages', class ChatMessages extends HTMLElement { + static _isFirstAutoScroll = true; + + connectedCallback() { + this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations)); + this._observer.observe(this, { childList: true, attributes: true }); + } + + disconnectedCallback() { + this._observer.disconnect(); + } + + _scheduleAutoScroll(mutations) { + // Debounce the calls in case multiple DOM updates occur together + cancelAnimationFrame(this._nextAutoScroll); + this._nextAutoScroll = requestAnimationFrame(() => { + const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message'))); + const elem = this.lastElementChild; + if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) { + elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' }); + ChatMessages._isFirstAutoScroll = false; + } + }); + } + + _elemIsNearScrollBoundary(elem, threshold) { + const maxScrollPos = document.body.scrollHeight - window.innerHeight; + const remainingScrollDistance = maxScrollPos - window.scrollY; + return remainingScrollDistance < elem.offsetHeight + threshold; + } +}); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor new file mode 100644 index 00000000000..69ca922a8ce --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor @@ -0,0 +1,78 @@ +@inject IChatClient ChatClient + +@if (suggestions is not null) +{ +
+ @foreach (var suggestion in suggestions) + { + + } +
+} + +@code { + private static string Prompt = @" + Suggest up to 3 follow-up questions that I could ask you to help me complete my task. + Each suggestion must be a complete sentence, maximum 6 words. + Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message, + for example 'How do I do that?' or 'Explain ...'. + If there are no suggestions, reply with an empty list. + "; + + private string[]? suggestions; + private CancellationTokenSource? cancellation; + + [Parameter] + public EventCallback OnSelected { get; set; } + + public void Clear() + { + suggestions = null; + cancellation?.Cancel(); + } + + public void Update(IReadOnlyList messages) + { + // Runs in the background and handles its own cancellation/errors + _ = UpdateSuggestionsAsync(messages); + } + + private async Task UpdateSuggestionsAsync(IReadOnlyList messages) + { + cancellation?.Cancel(); + cancellation = new CancellationTokenSource(); + + try + { + var response = await ChatClient.GetResponseAsync( + [.. ReduceMessages(messages), new(ChatRole.User, Prompt)], + cancellationToken: cancellation.Token); + if (!response.TryGetResult(out suggestions)) + { + suggestions = null; + } + + StateHasChanged(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + await DispatchExceptionAsync(ex); + } + } + + private async Task AddSuggestionAsync(string text) + { + await OnSelected.InvokeAsync(new(ChatRole.User, text)); + } + + private IEnumerable ReduceMessages(IReadOnlyList messages) + { + // Get any leading system messages, plus up to 5 user/assistant messages + // This should be enough context to generate suggestions without unnecessarily resending entire conversations when long + var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System); + var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5); + return systemMessages.Concat(otherMessages); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css new file mode 100644 index 00000000000..b291042c6d4 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css @@ -0,0 +1,9 @@ +.suggestions { + text-align: right; + white-space: nowrap; + gap: 0.5rem; + justify-content: flex-end; + flex-wrap: wrap; + display: flex; + margin-bottom: 0.75rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor new file mode 100644 index 00000000000..576cc2d2f4d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Routes.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Routes.razor new file mode 100644 index 00000000000..f756e19dfbc --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/_Imports.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/_Imports.razor new file mode 100644 index 00000000000..fa7cadef6ea --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.Extensions.AI +@using Microsoft.JSInterop +@using aichatweb.Web +@using aichatweb.Web.Components +@using aichatweb.Web.Components.Layout +@using aichatweb.Web.Services diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs new file mode 100644 index 00000000000..4711722548a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; +using aichatweb.Web.Components; +using aichatweb.Web.Services; +using aichatweb.Web.Services.Ingestion; +using OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureAISearch; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +var openai = builder.AddAzureOpenAIClient("openai"); +openai.AddChatClient("gpt-4o-mini") + .UseFunctionInvocation() + .UseOpenTelemetry(configure: c => + c.EnableSensitiveData = builder.Environment.IsDevelopment()); +openai.AddEmbeddingGenerator("text-embedding-3-small"); + +builder.AddAzureSearchClient("azureAISearch"); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseAntiforgery(); + +app.UseStaticFiles(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// By default, we ingest PDF files from the /wwwroot/Data directory. You can ingest from +// other sources by implementing IIngestionSource. +// Important: ensure that any content you ingest is trusted, as it may be reflected back +// to users or could be a source of prompt injection risk. +await DataIngestor.IngestDataAsync( + app.Services, + new PDFDirectorySource(Path.Combine(builder.Environment.WebRootPath, "Data"))); + +app.Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json new file mode 100644 index 00000000000..e2d900a219d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:9999;http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs new file mode 100644 index 00000000000..cff3717518e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class IngestedChunk +{ + [VectorStoreRecordKey] + public required string Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public int PageNumber { get; set; } + + [VectorStoreRecordData] + public required string Text { get; set; } + + [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model + public ReadOnlyMemory Vector { get; set; } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs new file mode 100644 index 00000000000..4be3b2980d7 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] + public required string Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs new file mode 100644 index 00000000000..448c2ce43b7 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services.Ingestion; + +public class DataIngestor( + ILogger logger, + IEmbeddingGenerator> embeddingGenerator, + IVectorStore vectorStore) +{ + public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) + { + using var scope = services.CreateScope(); + var ingestor = scope.ServiceProvider.GetRequiredService(); + await ingestor.IngestDataAsync(source); + } + + public async Task IngestDataAsync(IIngestionSource source) + { + var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); + + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); + + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) + { + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); + } + + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) + { + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); + + await documentsCollection.UpsertAsync(modifiedDocument); + + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } + + logger.LogInformation("Ingestion is up-to-date"); + + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) + { + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs new file mode 100644 index 00000000000..208b32b2fdf --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.AI; + +namespace aichatweb.Web.Services.Ingestion; + +public interface IIngestionSource +{ + string SourceId { get; } + + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); + + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); + + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs new file mode 100644 index 00000000000..01c370d9dec --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -0,0 +1,75 @@ +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.Text; +using UglyToad.PdfPig; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; + +namespace aichatweb.Web.Services.Ingestion; + +public class PDFDirectorySource(string sourceDirectory) : IIngestionSource +{ + public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); + + public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; + + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) + { + var results = new List(); + var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); + + foreach (var sourceFile in sourceFiles) + { + var sourceFileId = SourceFileId(sourceFile); + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) + { + results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + } + } + + return Task.FromResult((IEnumerable)results); + } + + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) + { + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); + } + + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + { + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); + var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); + + var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); + + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + { + Key = Guid.CreateVersion7().ToString(), + DocumentId = document.DocumentId, + PageNumber = pair.First.PageNumber, + Text = pair.First.Text, + Vector = pair.Second.Vector, + }); + } + + private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) + { + var letters = pdfPage.Letters; + var words = NearestNeighbourWordExtractor.Instance.GetWords(letters); + var textBlocks = DocstrumBoundingBoxes.Instance.GetBlocks(words); + var pageText = string.Join(Environment.NewLine + Environment.NewLine, + textBlocks.Select(t => t.Text.ReplaceLineEndings(" "))); + +#pragma warning disable SKEXP0050 // Type is for evaluation purposes only + return TextChunker.SplitPlainTextParagraphs([pageText], 200) + .Select((text, index) => (pdfPage.Number, index, text)); +#pragma warning restore SKEXP0050 // Type is for evaluation purposes only + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs new file mode 100644 index 00000000000..47b30dd0646 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class SemanticSearch( + IEmbeddingGenerator> embeddingGenerator, + IVectorStore vectorStore) +{ + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) + { + var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); + var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + { + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, + }); + + return await nearest.Select(result => result.Record).ToListAsync(); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj new file mode 100644 index 00000000000..4ef46e9f924 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + secret + + + + + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.Development.json new file mode 100644 index 00000000000..e22bd83cf3a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.json new file mode 100644 index 00000000000..d286041f99d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..94625f0e0e0617c1324cf5243b06d0e8a2da6177 GIT binary patch literal 153539 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

VE%a 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 W4tn!Pf-WddMQHNc+UE8a^8W?ym9>ok literal 0 HcmV?d00001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.css new file mode 100644 index 00000000000..0dec580e2fd --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.css @@ -0,0 +1,94 @@ +@import url('lib/tailwindcss/dist/preflight.css'); + +html { + min-height: 100vh; +} + +html, .main-background-gradient { + background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem); +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + + html::after { + content: ''; + background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red); + width: 100%; + height: 2px; + position: fixed; + top: 0; + } + +h1 { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: 600; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.btn-default { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #9CA3AF; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + background-color: #D1D5DB; +} + + .btn-default:hover { + background-color: #E5E7EB; + } + +.btn-subtle { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #D1D5DB; + font-size: 0.875rem; + line-height: 1.25rem; +} + + .btn-subtle:hover { + border-color: #93C5FD; + background-color: #DBEAFE; + } + +.page-width { + max-width: 1024px; + margin: auto; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.js new file mode 100644 index 00000000000..8b2cecd007d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/app.js @@ -0,0 +1,24 @@ +import DOMPurify from './lib/dompurify/dist/purify.es.mjs'; +import * as marked from './lib/marked/dist/marked.esm.js'; + +const purify = DOMPurify(window); + +customElements.define('assistant-message', class extends HTMLElement { + static observedAttributes = ['markdown']; + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'markdown') { + newValue = newValue.replace(//gs, ''); + const elements = marked.parse(newValue.replace(/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 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return apply(func, thisArg, args); + }; +} +/** + * Creates a new function that constructs an instance of the given constructor function with the provided arguments. + * + * @param func - The constructor function to be wrapped and called. + * @returns A new function that constructs an instance of the given constructor function with the provided arguments. + */ +function unconstruct(func) { + return function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return construct(func, args); + }; +} +/** + * Add properties to a lookup table + * + * @param set - The set to which elements will be added. + * @param array - The array containing elements to be added to the set. + * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. + * @returns The modified set with added elements. + */ +function addToSet(set, array) { + let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; + if (setPrototypeOf) { + // Make 'in' and truthy checks like Boolean(set.constructor) + // independent of any properties defined on Object.prototype. + // Prevent prototype setters from intercepting set as a this value. + setPrototypeOf(set, null); + } + let l = array.length; + while (l--) { + let element = array[l]; + if (typeof element === 'string') { + const lcElement = transformCaseFunc(element); + if (lcElement !== element) { + // Config presets (e.g. tags.js, attrs.js) are immutable. + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set[element] = true; + } + return set; +} +/** + * Clean up an array to harden against CSPP + * + * @param array - The array to be cleaned. + * @returns The cleaned version of the array + */ +function cleanArray(array) { + for (let index = 0; index < array.length; index++) { + const isPropertyExist = objectHasOwnProperty(array, index); + if (!isPropertyExist) { + array[index] = null; + } + } + return array; +} +/** + * Shallow clone an object + * + * @param object - The object to be cloned. + * @returns A new object that copies the original. + */ +function clone(object) { + const newObject = create(null); + for (const [property, value] of entries(object)) { + const isPropertyExist = objectHasOwnProperty(object, property); + if (isPropertyExist) { + if (Array.isArray(value)) { + newObject[property] = cleanArray(value); + } else if (value && typeof value === 'object' && value.constructor === Object) { + newObject[property] = clone(value); + } else { + newObject[property] = value; + } + } + } + return newObject; +} +/** + * This method automatically checks if the prop is function or getter and behaves accordingly. + * + * @param object - The object to look up the getter function in its prototype chain. + * @param prop - The property name for which to find the getter function. + * @returns The getter function found in the prototype chain or a fallback function. + */ +function lookupGetter(object, prop) { + while (object !== null) { + const desc = getOwnPropertyDescriptor(object, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === 'function') { + return unapply(desc.value); + } + } + object = getPrototypeOf(object); + } + function fallbackValue() { + return null; + } + return fallbackValue; +} + +const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); +const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); +const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); +// List of SVG elements that are disallowed by default. +// We still need to know them so that we can do namespace +// checks properly in case one wants to add them to +// allow-list. +const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); +const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); +// Similarly to SVG, we want to know all MathML elements, +// even those that we disallow by default. +const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); +const text = freeze(['#text']); + +const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']); +const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); +const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); +const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); + +// eslint-disable-next-line unicorn/better-regex +const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode +const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); +const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex +const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape +const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape +const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape +); +const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); +const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex +); +const DOCTYPE_NAME = seal(/^html$/i); +const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); + +var EXPRESSIONS = /*#__PURE__*/Object.freeze({ + __proto__: null, + ARIA_ATTR: ARIA_ATTR, + ATTR_WHITESPACE: ATTR_WHITESPACE, + CUSTOM_ELEMENT: CUSTOM_ELEMENT, + DATA_ATTR: DATA_ATTR, + DOCTYPE_NAME: DOCTYPE_NAME, + ERB_EXPR: ERB_EXPR, + IS_ALLOWED_URI: IS_ALLOWED_URI, + IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, + MUSTACHE_EXPR: MUSTACHE_EXPR, + TMPLIT_EXPR: TMPLIT_EXPR +}); + +/* eslint-disable @typescript-eslint/indent */ +// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType +const NODE_TYPE = { + element: 1, + attribute: 2, + text: 3, + cdataSection: 4, + entityReference: 5, + // Deprecated + entityNode: 6, + // Deprecated + progressingInstruction: 7, + comment: 8, + document: 9, + documentType: 10, + documentFragment: 11, + notation: 12 // Deprecated +}; +const getGlobal = function getGlobal() { + return typeof window === 'undefined' ? null : window; +}; +/** + * Creates a no-op policy for internal use only. + * Don't export this function outside this module! + * @param trustedTypes The policy factory. + * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). + * @return The policy created (or null, if Trusted Types + * are not supported or creating the policy failed). + */ +const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { + if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { + return null; + } + // Allow the callers to control the unique policy name + // by adding a data-tt-policy-suffix to the script element with the DOMPurify. + // Policy creation with duplicate names throws in Trusted Types. + let suffix = null; + const ATTR_NAME = 'data-tt-policy-suffix'; + if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { + suffix = purifyHostElement.getAttribute(ATTR_NAME); + } + const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); + try { + return trustedTypes.createPolicy(policyName, { + createHTML(html) { + return html; + }, + createScriptURL(scriptUrl) { + return scriptUrl; + } + }); + } catch (_) { + // Policy creation failed (most likely another DOMPurify script has + // already run). Skip creating the policy, as this will only cause errors + // if TT are enforced. + console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); + return null; + } +}; +const _createHooksMap = function _createHooksMap() { + return { + afterSanitizeAttributes: [], + afterSanitizeElements: [], + afterSanitizeShadowDOM: [], + beforeSanitizeAttributes: [], + beforeSanitizeElements: [], + beforeSanitizeShadowDOM: [], + uponSanitizeAttribute: [], + uponSanitizeElement: [], + uponSanitizeShadowNode: [] + }; +}; +function createDOMPurify() { + let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); + const DOMPurify = root => createDOMPurify(root); + DOMPurify.version = '3.2.4'; + DOMPurify.removed = []; + if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { + // Not running in a browser, provide a factory function + // so that you can pass your own Window + DOMPurify.isSupported = false; + return DOMPurify; + } + let { + document + } = window; + const originalDocument = document; + const currentScript = originalDocument.currentScript; + const { + DocumentFragment, + HTMLTemplateElement, + Node, + Element, + NodeFilter, + NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, + HTMLFormElement, + DOMParser, + trustedTypes + } = window; + const ElementPrototype = Element.prototype; + const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); + const remove = lookupGetter(ElementPrototype, 'remove'); + const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); + const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); + const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); + // As per issue #47, the web-components registry is inherited by a + // new document created via createHTMLDocument. As per the spec + // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) + // a new empty registry is used when creating a template contents owner + // document, so we use that as our parent document to ensure nothing + // is inherited. + if (typeof HTMLTemplateElement === 'function') { + const template = document.createElement('template'); + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument; + } + } + let trustedTypesPolicy; + let emptyHTML = ''; + const { + implementation, + createNodeIterator, + createDocumentFragment, + getElementsByTagName + } = document; + const { + importNode + } = originalDocument; + let hooks = _createHooksMap(); + /** + * Expose whether this browser supports running the full DOMPurify. + */ + DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; + const { + MUSTACHE_EXPR, + ERB_EXPR, + TMPLIT_EXPR, + DATA_ATTR, + ARIA_ATTR, + IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE, + CUSTOM_ELEMENT + } = EXPRESSIONS; + let { + IS_ALLOWED_URI: IS_ALLOWED_URI$1 + } = EXPRESSIONS; + /** + * We consider the elements and attributes below to be safe. Ideally + * don't add any new ones but feel free to remove unwanted ones. + */ + /* allowed element names */ + let ALLOWED_TAGS = null; + const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); + /* Allowed attribute names */ + let ALLOWED_ATTR = null; + const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); + /* + * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. + * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) + * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) + * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. + */ + let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })); + /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ + let FORBID_TAGS = null; + /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ + let FORBID_ATTR = null; + /* Decide if ARIA attributes are okay */ + let ALLOW_ARIA_ATTR = true; + /* Decide if custom data attributes are okay */ + let ALLOW_DATA_ATTR = true; + /* Decide if unknown protocols are okay */ + let ALLOW_UNKNOWN_PROTOCOLS = false; + /* Decide if self-closing tags in attributes are allowed. + * Usually removed due to a mXSS issue in jQuery 3.0 */ + let ALLOW_SELF_CLOSE_IN_ATTR = true; + /* Output should be safe for common template engines. + * This means, DOMPurify removes data attributes, mustaches and ERB + */ + let SAFE_FOR_TEMPLATES = false; + /* Output should be safe even for XML used within HTML and alike. + * This means, DOMPurify removes comments when containing risky content. + */ + let SAFE_FOR_XML = true; + /* Decide if document with ... should be returned */ + let WHOLE_DOCUMENT = false; + /* Track whether config is already set on this instance of DOMPurify. */ + let SET_CONFIG = false; + /* Decide if all elements (e.g. style, script) must be children of + * document.body. By default, browsers might move them to document.head */ + let FORCE_BODY = false; + /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported). + * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead + */ + let RETURN_DOM = false; + /* Decide if a DOM `DocumentFragment` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported) */ + let RETURN_DOM_FRAGMENT = false; + /* Try to return a Trusted Type object instead of a string, return a string in + * case Trusted Types are not supported */ + let RETURN_TRUSTED_TYPE = false; + /* Output should be free from DOM clobbering attacks? + * This sanitizes markups named with colliding, clobberable built-in DOM APIs. + */ + let SANITIZE_DOM = true; + /* Achieve full DOM Clobbering protection by isolating the namespace of named + * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. + * + * HTML/DOM spec rules that enable DOM Clobbering: + * - Named Access on Window (§7.3.3) + * - DOM Tree Accessors (§3.1.5) + * - Form Element Parent-Child Relations (§4.10.3) + * - Iframe srcdoc / Nested WindowProxies (§4.8.5) + * - HTMLCollection (§4.2.10.2) + * + * Namespace isolation is implemented by prefixing `id` and `name` attributes + * with a constant string, i.e., `user-content-` + */ + let SANITIZE_NAMED_PROPS = false; + const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; + /* Keep element content when removing element? */ + let KEEP_CONTENT = true; + /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead + * of importing it into a new Document and returning a sanitized copy */ + let IN_PLACE = false; + /* Allow usage of profiles like html, svg and mathMl */ + let USE_PROFILES = {}; + /* Tags to ignore content of when KEEP_CONTENT is true */ + let FORBID_CONTENTS = null; + const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); + /* Tags that are safe for data: URIs */ + let DATA_URI_TAGS = null; + const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); + /* Attributes safe for values like "javascript:" */ + let URI_SAFE_ATTRIBUTES = null; + const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); + const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; + const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + /* Document namespace */ + let NAMESPACE = HTML_NAMESPACE; + let IS_EMPTY_INPUT = false; + /* Allowed XHTML+XML namespaces */ + let ALLOWED_NAMESPACES = null; + const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); + let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); + let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']); + // Certain elements are allowed in both SVG and HTML + // namespace. We need to specify them explicitly + // so that they don't get erroneously deleted from + // HTML namespace. + const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); + /* Parsing of strict XHTML documents */ + let PARSER_MEDIA_TYPE = null; + const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; + const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; + let transformCaseFunc = null; + /* Keep a reference to config to pass to hooks */ + let CONFIG = null; + /* Ideally, do not touch anything below this line */ + /* ______________________________________________ */ + const formElement = document.createElement('form'); + const isRegexOrFunction = function isRegexOrFunction(testValue) { + return testValue instanceof RegExp || testValue instanceof Function; + }; + /** + * _parseConfig + * + * @param cfg optional config literal + */ + // eslint-disable-next-line complexity + const _parseConfig = function _parseConfig() { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (CONFIG && CONFIG === cfg) { + return; + } + /* Shield configuration object from tampering */ + if (!cfg || typeof cfg !== 'object') { + cfg = {}; + } + /* Shield configuration object from prototype pollution */ + cfg = clone(cfg); + PARSER_MEDIA_TYPE = + // eslint-disable-next-line unicorn/prefer-includes + SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; + // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. + transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; + /* Set configuration parameters */ + ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; + ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; + URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS; + FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; + FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; + FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; + USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false + ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false + SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false + RETURN_DOM = cfg.RETURN_DOM || false; // Default false + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false + FORCE_BODY = cfg.FORCE_BODY || false; // Default false + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true + SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true + IN_PLACE = cfg.IN_PLACE || false; // Default false + IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; + NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; + MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS; + HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS; + CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { + CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { + CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { + CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; + } + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + /* Parse profile info */ + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, text); + ALLOWED_ATTR = []; + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html$1); + addToSet(ALLOWED_ATTR, html); + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg$1); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl$1); + addToSet(ALLOWED_ATTR, mathMl); + addToSet(ALLOWED_ATTR, xml); + } + } + /* Merge configuration parameters */ + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); + } + if (cfg.FORBID_CONTENTS) { + if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { + FORBID_CONTENTS = clone(FORBID_CONTENTS); + } + addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); + } + /* Add #text in case KEEP_CONTENT is set to true */ + if (KEEP_CONTENT) { + ALLOWED_TAGS['#text'] = true; + } + /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); + } + /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ['tbody']); + delete FORBID_TAGS.tbody; + } + if (cfg.TRUSTED_TYPES_POLICY) { + if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); + } + if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); + } + // Overwrite existing TrustedTypes policy. + trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; + // Sign local variables required by `sanitize`. + emptyHTML = trustedTypesPolicy.createHTML(''); + } else { + // Uninitialized policy, attempt to initialize the internal dompurify policy. + if (trustedTypesPolicy === undefined) { + trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); + } + // If creating the internal policy succeeded sign internal variables. + if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { + emptyHTML = trustedTypesPolicy.createHTML(''); + } + } + // Prevent further manipulation of configuration. + // Not available in IE8, Safari 5, etc. + if (freeze) { + freeze(cfg); + } + CONFIG = cfg; + }; + /* Keep track of all possible SVG and MathML tags + * so that we can perform the namespace checks + * correctly. */ + const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); + const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); + /** + * @param element a DOM element whose namespace is being checked + * @returns Return false if the element has a + * namespace that a spec-compliant parser would never + * return. Return true otherwise. + */ + const _checkValidNamespace = function _checkValidNamespace(element) { + let parent = getParentNode(element); + // In JSDOM, if we're inside shadow DOM, then parentNode + // can be null. We just simulate parent in this case. + if (!parent || !parent.tagName) { + parent = { + namespaceURI: NAMESPACE, + tagName: 'template' + }; + } + const tagName = stringToLowerCase(element.tagName); + const parentTagName = stringToLowerCase(parent.tagName); + if (!ALLOWED_NAMESPACES[element.namespaceURI]) { + return false; + } + if (element.namespaceURI === SVG_NAMESPACE) { + // The only way to switch from HTML namespace to SVG + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'svg'; + } + // The only way to switch from MathML to SVG is via` + // svg if parent is either or MathML + // text integration points. + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } + // We only allow elements that are defined in SVG + // spec. All others are disallowed in SVG namespace. + return Boolean(ALL_SVG_TAGS[tagName]); + } + if (element.namespaceURI === MATHML_NAMESPACE) { + // The only way to switch from HTML namespace to MathML + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'math'; + } + // The only way to switch from SVG to MathML is via + // and HTML integration points + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; + } + // We only allow elements that are defined in MathML + // spec. All others are disallowed in MathML namespace. + return Boolean(ALL_MATHML_TAGS[tagName]); + } + if (element.namespaceURI === HTML_NAMESPACE) { + // The only way to switch from SVG to HTML is via + // HTML integration points, and from MathML to HTML + // is via MathML text integration points + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } + // We disallow tags that are specific for MathML + // or SVG and should never appear in HTML namespace + return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); + } + // For XHTML and XML documents that support custom namespaces + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { + return true; + } + // The code should never reach this place (this means + // that the element somehow got namespace that is not + // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). + // Return false just in case. + return false; + }; + /** + * _forceRemove + * + * @param node a DOM node + */ + const _forceRemove = function _forceRemove(node) { + arrayPush(DOMPurify.removed, { + element: node + }); + try { + // eslint-disable-next-line unicorn/prefer-dom-node-remove + getParentNode(node).removeChild(node); + } catch (_) { + remove(node); + } + }; + /** + * _removeAttribute + * + * @param name an Attribute name + * @param element a DOM node + */ + const _removeAttribute = function _removeAttribute(name, element) { + try { + arrayPush(DOMPurify.removed, { + attribute: element.getAttributeNode(name), + from: element + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: element + }); + } + element.removeAttribute(name); + // We void attribute values for unremovable "is" attributes + if (name === 'is') { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(element); + } catch (_) {} + } else { + try { + element.setAttribute(name, ''); + } catch (_) {} + } + } + }; + /** + * _initDocument + * + * @param dirty - a string of dirty markup + * @return a DOM, filled with the dirty markup + */ + const _initDocument = function _initDocument(dirty) { + /* Create a HTML document */ + let doc = null; + let leadingWhitespace = null; + if (FORCE_BODY) { + dirty = '' + dirty; + } else { + /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ + const matches = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches && matches[0]; + } + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { + // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) + dirty = '' + dirty + ''; + } + const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + /* + * Use the DOMParser API by default, fallback later if needs be + * DOMParser not work for svg when has multiple root element. + */ + if (NAMESPACE === HTML_NAMESPACE) { + try { + doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); + } catch (_) {} + } + /* Use createHTMLDocument in case DOMParser is not available */ + if (!doc || !doc.documentElement) { + doc = implementation.createDocument(NAMESPACE, 'template', null); + try { + doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; + } catch (_) { + // Syntax error if dirtyPayload is invalid xml + } + } + const body = doc.body || doc.documentElement; + if (dirty && leadingWhitespace) { + body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); + } + /* Work on whole document or just its body */ + if (NAMESPACE === HTML_NAMESPACE) { + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; + } + return WHOLE_DOCUMENT ? doc.documentElement : body; + }; + /** + * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. + * + * @param root The root element or node to start traversing on. + * @return The created NodeIterator + */ + const _createNodeIterator = function _createNodeIterator(root) { + return createNodeIterator.call(root.ownerDocument || root, root, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); + }; + /** + * _isClobbered + * + * @param element element to check for clobbering attacks + * @return true if clobbered, false if safe + */ + const _isClobbered = function _isClobbered(element) { + return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function'); + }; + /** + * Checks whether the given object is a DOM node. + * + * @param value object to check whether it's a DOM node + * @return true is object is a DOM node + */ + const _isNode = function _isNode(value) { + return typeof Node === 'function' && value instanceof Node; + }; + function _executeHooks(hooks, currentNode, data) { + arrayForEach(hooks, hook => { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + } + /** + * _sanitizeElements + * + * @protect nodeName + * @protect textContent + * @protect removeChild + * @param currentNode to check for permission to exist + * @return true if node was killed, false if left alive + */ + const _sanitizeElements = function _sanitizeElements(currentNode) { + let content = null; + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeElements, currentNode, null); + /* Check if element is clobbered or can clobber */ + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + return true; + } + /* Now let's check the element's type and name */ + const tagName = transformCaseFunc(currentNode.nodeName); + /* Execute a hook if present */ + _executeHooks(hooks.uponSanitizeElement, currentNode, { + tagName, + allowedTags: ALLOWED_TAGS + }); + /* Detect mXSS attempts abusing namespace confusion */ + if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + return true; + } + /* Remove any occurrence of processing instructions */ + if (currentNode.nodeType === NODE_TYPE.progressingInstruction) { + _forceRemove(currentNode); + return true; + } + /* Remove any kind of possibly harmful comments */ + if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) { + _forceRemove(currentNode); + return true; + } + /* Remove element if anything forbids its presence */ + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + /* Check if we have a custom element to handle */ + if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { + return false; + } + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { + return false; + } + } + /* Keep content except for bad-listed elements */ + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { + const parentNode = getParentNode(currentNode) || currentNode.parentNode; + const childNodes = getChildNodes(currentNode) || currentNode.childNodes; + if (childNodes && parentNode) { + const childCount = childNodes.length; + for (let i = childCount - 1; i >= 0; --i) { + const childClone = cloneNode(childNodes[i], true); + childClone.__removalCount = (currentNode.__removalCount || 0) + 1; + parentNode.insertBefore(childClone, getNextSibling(currentNode)); + } + } + } + _forceRemove(currentNode); + return true; + } + /* Check whether element has a valid namespace */ + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode); + return true; + } + /* Make sure that older browsers don't get fallback-tag mXSS */ + if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { + _forceRemove(currentNode); + return true; + } + /* Sanitize element content to be template-safe */ + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { + /* Get the element's text content */ + content = currentNode.textContent; + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + content = stringReplace(content, expr, ' '); + }); + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { + element: currentNode.cloneNode() + }); + currentNode.textContent = content; + } + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeElements, currentNode, null); + return false; + }; + /** + * _isValidAttribute + * + * @param lcTag Lowercase tag name of containing element. + * @param lcName Lowercase attribute name. + * @param value Attribute value. + * @return Returns true if `value` is valid, otherwise false. + */ + // eslint-disable-next-line complexity + const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { + /* Make sure attribute cannot clobber */ + if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { + return false; + } + /* Allow valid data-* attributes: At least one character after "-" + (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) + XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) + We don't need to check the value; it's always URI safe. */ + if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + if ( + // First condition does a very basic check if a) it's basically a valid custom element tagname AND + // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck + _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || + // Alternative, second condition checks if it's an `is`-attribute, AND + // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { + return false; + } + /* Check value is safe. First, is attr inert? If so, is safe */ + } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { + return false; + } else ; + return true; + }; + /** + * _isBasicCustomElement + * checks if at least one dash is included in tagName, and it's not the first char + * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name + * + * @param tagName name of the tag of the node to sanitize + * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. + */ + const _isBasicCustomElement = function _isBasicCustomElement(tagName) { + return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT); + }; + /** + * _sanitizeAttributes + * + * @protect attributes + * @protect nodeName + * @protect removeAttribute + * @protect setAttribute + * + * @param currentNode to sanitize + */ + const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); + const { + attributes + } = currentNode; + /* Check if we have attributes; if not we might have a text node */ + if (!attributes || _isClobbered(currentNode)) { + return; + } + const hookEvent = { + attrName: '', + attrValue: '', + keepAttr: true, + allowedAttributes: ALLOWED_ATTR, + forceKeepAttr: undefined + }; + let l = attributes.length; + /* Go backwards over all attributes; safely remove bad ones */ + while (l--) { + const attr = attributes[l]; + const { + name, + namespaceURI, + value: attrValue + } = attr; + const lcName = transformCaseFunc(name); + let value = name === 'value' ? attrValue : stringTrim(attrValue); + /* Execute a hook if present */ + hookEvent.attrName = lcName; + hookEvent.attrValue = value; + hookEvent.keepAttr = true; + hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set + _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); + value = hookEvent.attrValue; + /* Full DOM Clobbering protection via namespace isolation, + * Prefix id and name attributes with `user-content-` + */ + if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { + // Remove the attribute with this value + _removeAttribute(name, currentNode); + // Prefix the value and later re-create the attribute with the sanitized value + value = SANITIZE_NAMED_PROPS_PREFIX + value; + } + /* Work around a security issue with comments inside attributes */ + if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + /* Did the hooks approve of the attribute? */ + if (hookEvent.forceKeepAttr) { + continue; + } + /* Remove attribute */ + _removeAttribute(name, currentNode); + /* Did the hooks approve of the attribute? */ + if (!hookEvent.keepAttr) { + continue; + } + /* Work around a security issue in jQuery 3.0 */ + if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + /* Sanitize attribute content to be template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + value = stringReplace(value, expr, ' '); + }); + } + /* Is `value` valid for this attribute? */ + const lcTag = transformCaseFunc(currentNode.nodeName); + if (!_isValidAttribute(lcTag, lcName, value)) { + continue; + } + /* Handle attributes that require Trusted Types */ + if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { + if (namespaceURI) ; else { + switch (trustedTypes.getAttributeType(lcTag, lcName)) { + case 'TrustedHTML': + { + value = trustedTypesPolicy.createHTML(value); + break; + } + case 'TrustedScriptURL': + { + value = trustedTypesPolicy.createScriptURL(value); + break; + } + } + } + } + /* Handle invalid data-* attribute set by try-catching it */ + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value); + } else { + /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ + currentNode.setAttribute(name, value); + } + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + } else { + arrayPop(DOMPurify.removed); + } + } catch (_) {} + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); + }; + /** + * _sanitizeShadowDOM + * + * @param fragment to iterate over recursively + */ + const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { + let shadowNode = null; + const shadowIterator = _createNodeIterator(fragment); + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); + while (shadowNode = shadowIterator.nextNode()) { + /* Execute a hook if present */ + _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); + /* Sanitize tags and elements */ + _sanitizeElements(shadowNode); + /* Check attributes next */ + _sanitizeAttributes(shadowNode); + /* Deep shadow DOM detected */ + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(shadowNode.content); + } + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); + }; + // eslint-disable-next-line complexity + DOMPurify.sanitize = function (dirty) { + let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + let body = null; + let importedNode = null; + let currentNode = null; + let returnNode = null; + /* Make sure we have a string to sanitize. + DO NOT return early, as this will return the wrong type if + the user has requested a DOM object rather than a string */ + IS_EMPTY_INPUT = !dirty; + if (IS_EMPTY_INPUT) { + dirty = ''; + } + /* Stringify, in case dirty is an object */ + if (typeof dirty !== 'string' && !_isNode(dirty)) { + if (typeof dirty.toString === 'function') { + dirty = dirty.toString(); + if (typeof dirty !== 'string') { + throw typeErrorCreate('dirty is not a string, aborting'); + } + } else { + throw typeErrorCreate('toString is not a function'); + } + } + /* Return dirty HTML if DOMPurify cannot run */ + if (!DOMPurify.isSupported) { + return dirty; + } + /* Assign config vars */ + if (!SET_CONFIG) { + _parseConfig(cfg); + } + /* Clean up removed elements */ + DOMPurify.removed = []; + /* Check if dirty is correctly typed for IN_PLACE */ + if (typeof dirty === 'string') { + IN_PLACE = false; + } + if (IN_PLACE) { + /* Do some early pre-sanitization to avoid unsafe root nodes */ + if (dirty.nodeName) { + const tagName = transformCaseFunc(dirty.nodeName); + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); + } + } + } else if (dirty instanceof Node) { + /* If dirty is a DOM element, append to an empty document to avoid + elements being stripped by the parser */ + body = _initDocument(''); + importedNode = body.ownerDocument.importNode(dirty, true); + if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { + /* Node is already a body, use as is */ + body = importedNode; + } else if (importedNode.nodeName === 'HTML') { + body = importedNode; + } else { + // eslint-disable-next-line unicorn/prefer-dom-node-append + body.appendChild(importedNode); + } + } else { + /* Exit directly if we have nothing to do */ + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && + // eslint-disable-next-line unicorn/prefer-includes + dirty.indexOf('<') === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; + } + /* Initialize the document to work on */ + body = _initDocument(dirty); + /* Check we have a DOM node from the data */ + if (!body) { + return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; + } + } + /* Remove first element node (ours) if FORCE_BODY is set */ + if (body && FORCE_BODY) { + _forceRemove(body.firstChild); + } + /* Get node iterator */ + const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); + /* Now start iterating over the created document */ + while (currentNode = nodeIterator.nextNode()) { + /* Sanitize tags and elements */ + _sanitizeElements(currentNode); + /* Check attributes next */ + _sanitizeAttributes(currentNode); + /* Shadow DOM detected, sanitize it */ + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); + } + } + /* If we sanitized `dirty` in-place, return it. */ + if (IN_PLACE) { + return dirty; + } + /* Return sanitized string or DOM */ + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument); + while (body.firstChild) { + // eslint-disable-next-line unicorn/prefer-dom-node-append + returnNode.appendChild(body.firstChild); + } + } else { + returnNode = body; + } + if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { + /* + AdoptNode() is not used because internal state is not reset + (e.g. the past names map of a HTMLFormElement), this is safe + in theory but we would rather not risk another attack vector. + The state that is cloned by importNode() is explicitly defined + by the specs. + */ + returnNode = importNode.call(originalDocument, returnNode, true); + } + return returnNode; + } + let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + /* Serialize doctype if allowed */ + if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { + serializedHTML = '\n' + serializedHTML; + } + /* Sanitize final string template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + serializedHTML = stringReplace(serializedHTML, expr, ' '); + }); + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; + }; + DOMPurify.setConfig = function () { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + _parseConfig(cfg); + SET_CONFIG = true; + }; + DOMPurify.clearConfig = function () { + CONFIG = null; + SET_CONFIG = false; + }; + DOMPurify.isValidAttribute = function (tag, attr, value) { + /* Initialize shared config vars if necessary. */ + if (!CONFIG) { + _parseConfig({}); + } + const lcTag = transformCaseFunc(tag); + const lcName = transformCaseFunc(attr); + return _isValidAttribute(lcTag, lcName, value); + }; + DOMPurify.addHook = function (entryPoint, hookFunction) { + if (typeof hookFunction !== 'function') { + return; + } + arrayPush(hooks[entryPoint], hookFunction); + }; + DOMPurify.removeHook = function (entryPoint, hookFunction) { + if (hookFunction !== undefined) { + const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); + return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; + } + return arrayPop(hooks[entryPoint]); + }; + DOMPurify.removeHooks = function (entryPoint) { + hooks[entryPoint] = []; + }; + DOMPurify.removeAllHooks = function () { + hooks = _createHooksMap(); + }; + return DOMPurify; +} +var purify = createDOMPurify(); + +export { purify as default }; +//# sourceMappingURL=purify.es.mjs.map diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md new file mode 100644 index 00000000000..352b52d5503 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md @@ -0,0 +1,5 @@ +marked version 15.0.6 +https://github.com/markedjs/marked +License: MIT + +To update, replace the `dist/marked.esm.js` file with with an updated version from https://www.npmjs.com/package/marked. diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js new file mode 100644 index 00000000000..a32cd778363 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js @@ -0,0 +1,2568 @@ +/** + * marked v15.0.6 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +/** + * Gets the original marked default options. + */ +function _getDefaults() { + return { + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }; +} +let _defaults = _getDefaults(); +function changeDefaults(newDefaults) { + _defaults = newDefaults; +} + +const noopTest = { exec: () => null }; +function edit(regex, opt = '') { + let source = typeof regex === 'string' ? regex : regex.source; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(other.caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; +} +const other = { + codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, + outputLinkReplace: /\\([\[\]])/g, + indentCodeCompensation: /^(\s+)(?:```)/, + beginningSpace: /^\s+/, + endingHash: /#$/, + startingSpaceChar: /^ /, + endingSpaceChar: / $/, + nonSpaceChar: /[^ ]/, + newLineCharGlobal: /\n/g, + tabCharGlobal: /\t/g, + multipleSpaceGlobal: /\s+/g, + blankLine: /^[ \t]*$/, + doubleBlankLine: /\n[ \t]*\n[ \t]*$/, + blockquoteStart: /^ {0,3}>/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceTabs: /^\t+/, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] /, + listReplaceTask: /^\[[ xX]\] +/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^/i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`), + hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), + fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`), + headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`), + htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'), +}; +/** + * Block-Level Grammar + */ +const newline = /^(?:[ \t]*(?:\n|$))+/; +const blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; +const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; +const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; +const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; +const bullet = /(?:[*+-]|\d{1,9}[.)])/; +const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); +const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; +const blockText = /^[^\n]+/; +const _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; +const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); +const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); +const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; +const _comment = /|$))/; +const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); +const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); +const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); +/** + * Normal Block Grammar + */ +const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, +}; +/** + * GFM Block Grammar + */ +const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', '(?: {4}| {0,3}\t)[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); +const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), +}; +/** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ +const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), +}; +/** + * Inline-Level Grammar + */ +const escape$1 = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; +const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; +const br = /^( {2,}|\\)\n(?!\s*$)/; +const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ +const blockSkip = /\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g; +const emStrongLDelimCore = /^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/; +const emStrongLDelim = edit(emStrongLDelimCore, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongLDelimGfm = edit(emStrongLDelimCore, 'u') + .replace(/punct/g, _punctuationGfmStrongEm) + .getRegex(); +const emStrongRDelimAstCore = '^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + + '|notPunctSpace(\\*+)(?=notPunctSpace)'; // (6) a***a can be either Left or Right Delimiter +const emStrongRDelimAst = edit(emStrongRDelimAstCore, 'gu') + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongRDelimAstGfm = edit(emStrongRDelimAstCore, 'gu') + .replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmStrongEm) + .replace(/punctSpace/g, _punctuationOrSpaceGfmStrongEm) + .replace(/punct/g, _punctuationGfmStrongEm) + .getRegex(); +// (6) Not allowed for _ +const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + + '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const anyPunctuation = edit(/\\(punct)/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); +const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); +const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); +const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); +const _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); +const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); +const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); +const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); +/** + * Normal Inline Grammar + */ +const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape: escape$1, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, +}; +/** + * Pedantic Inline Grammar + */ +const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), +}; +/** + * GFM Inline Grammar + */ +const inlineGfm = { + ...inlineNormal, + emStrongRDelimAst: emStrongRDelimAstGfm, + emStrongLDelim: emStrongLDelimGfm, + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\': '>', + '"': '"', + "'": ''', +}; +const getEscapeReplacement = (ch) => escapeReplacements[ch]; +function escape(html, encode) { + if (encode) { + if (other.escapeTest.test(html)) { + return html.replace(other.escapeReplace, getEscapeReplacement); + } + } + else { + if (other.escapeTestNoEncode.test(html)) { + return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; +} +function cleanUrl(href) { + try { + href = encodeURI(href).replace(other.percentDecode, '%'); + } + catch { + return null; + } + return href; +} +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(other.findPipe, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(other.splitPipe); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells.at(-1)?.trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(other.slashPipe, '|'); + } + return cells; +} +/** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ +function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && true) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); +} +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +function outputLink(cap, link, raw, lexer, rules) { + const href = link.href; + const title = link.title || null; + const text = cap[1].replace(rules.other.outputLinkReplace, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text, + }; +} +function indentCodeCompensation(raw, text, rules) { + const matchIndentToCode = raw.match(rules.other.indentCodeCompensation); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(rules.other.beginningSpace); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); +} +/** + * Tokenizer + */ +class _Tokenizer { + options; + rules; // set by the lexer + lexer; // set by the lexer + constructor(options) { + this.options = options || _defaults; + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(this.rules.other.codeRemoveIndent, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || '', this.rules); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (this.rules.other.endingHash.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (this.rules.other.blockquoteStart.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(this.rules.other.blockquoteSetextReplace, '\n $1') + .replace(this.rules.other.blockquoteSetextReplace2, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens.at(-1); + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens.at(-1).raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = this.rules.other.listItemRegex(bull); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(this.rules.other.listReplaceTabs, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(this.rules.other.nonSpaceChar); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && this.rules.other.blankLine.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = this.rules.other.nextBulletRegex(indent); + const hrRegex = this.rules.other.hrRegex(indent); + const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent); + const headingBeginRegex = this.rules.other.headingBeginRegex(indent); + const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + let nextLineWithoutTabs; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(this.rules.other.listReplaceNesting, ' '); + nextLineWithoutTabs = nextLine; + } + else { + nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of html block + if (htmlBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(nextLine)) { + break; + } + if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLineWithoutTabs.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.replace(this.rules.other.tabCharGlobal, ' ').search(this.rules.other.nonSpaceChar) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLineWithoutTabs.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (this.rules.other.doubleBlankLine.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = this.rules.other.listIsTask.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(this.rules.other.listReplaceTask, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + const lastItem = list.items.at(-1); + if (lastItem) { + lastItem.raw = lastItem.raw.trimEnd(); + lastItem.text = lastItem.text.trimEnd(); + } + else { + // not a list since there were no items + return; + } + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, ' '); + const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!this.rules.other.tableDelimiter.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(this.rules.other.tableAlignChars, '').split('|'); + const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (this.rules.other.tableAlignRight.test(align)) { + item.align.push('right'); + } + else if (this.rules.other.tableAlignCenter.test(align)) { + item.align.push('center'); + } + else if (this.rules.other.tableAlignLeft.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: cap[1], + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) { + this.lexer.state.inLink = true; + } + else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) { + // commonmark requires matching angle brackets + if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = this.rules.other.pedanticHrefTitle.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (this.rules.other.startAngleBracket.test(href)) { + if (this.options.pedantic && !(this.rules.other.endAngleBracket.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer, this.rules); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer, this.rules); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(this.rules.other.newLineCharGlobal, ' '); + const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text); + const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = cap[1]; + href = 'mailto:' + text; + } + else { + text = cap[1]; + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = cap[0]; + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = cap[0]; + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + const escaped = this.lexer.state.inRawBlock; + return { + type: 'text', + raw: cap[0], + text: cap[0], + escaped, + }; + } + } +} + +/** + * Block Lexer + */ +class _Lexer { + tokens; + options; + state; + tokenizer; + inlineQueue; + constructor(options) { + // TokenList cannot be created in one go + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || _defaults; + this.options.tokenizer = this.options.tokenizer || new _Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true, + }; + const rules = { + other, + block: block.normal, + inline: inline.normal, + }; + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } + else if (this.options.gfm) { + rules.block = block.gfm; + if (this.options.breaks) { + rules.inline = inline.breaks; + } + else { + rules.inline = inline.gfm; + } + } + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + static get rules() { + return { + block, + inline, + }; + } + /** + * Static Lex Method + */ + static lex(src, options) { + const lexer = new _Lexer(options); + return lexer.lex(src); + } + /** + * Static Lex Inline Method + */ + static lexInline(src, options) { + const lexer = new _Lexer(options); + return lexer.inlineTokens(src); + } + /** + * Preprocessing + */ + lex(src) { + src = src.replace(other.carriageReturn, '\n'); + this.blockTokens(src, this.tokens); + for (let i = 0; i < this.inlineQueue.length; i++) { + const next = this.inlineQueue[i]; + this.inlineTokens(next.src, next.tokens); + } + this.inlineQueue = []; + return this.tokens; + } + blockTokens(src, tokens = [], lastParagraphClipped = false) { + if (this.options.pedantic) { + src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''); + } + while (src) { + let token; + if (this.options.extensions?.block?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.raw.length === 1 && lastToken !== undefined) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + lastToken.raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + // An indented code block cannot interrupt a paragraph. + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue.at(-1).src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + const lastToken = tokens.at(-1); + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = cutSrc.length !== src.length; + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match = null; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + + '[' + 'a'.repeat(match[0].length - 2) + ']' + + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + let keepPrevChar = false; + let prevChar = ''; + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + let token; + // extensions + if (this.options.extensions?.inline?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.type === 'text' && lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } +} + +/** + * Renderer + */ +class _Renderer { + options; + parser; // set by the parser + constructor(options) { + this.options = options || _defaults; + } + space(token) { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(other.notSpaceStart)?.[0]; + const code = text.replace(other.endingNewline, '') + '\n'; + if (!langString) { + return '

'
+                + (escaped ? code : escape(code, true))
+                + '
\n'; + } + return '
'
+            + (escaped ? code : escape(code, true))
+            + '
\n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
\n${body}
\n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr(token) { + return '
\n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens[0]?.type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + escape(item.tokens[0].tokens[0].text); + item.tokens[0].tokens[0].escaped = true; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + escaped: true, + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${escape(text, true)}`; + } + br(token) { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return escape(text); + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (['options', 'block'].includes(prop)) { + // ignore options and block properties + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(blockType) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + opt.hooks.block = blockType; + } + const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +                    + escape(e.message + '', true)
    +                    + '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } +} + +const markedInstance = new Marked(); +function marked(src, opt) { + return markedInstance.parse(src, opt); +} +/** + * Sets the default options. + * + * @param options Hash of options + */ +marked.options = + marked.setOptions = function (options) { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; +/** + * Gets the original marked default options. + */ +marked.getDefaults = _getDefaults; +marked.defaults = _defaults; +/** + * Use Extension + */ +marked.use = function (...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; +/** + * Run callback for every token + */ +marked.walkTokens = function (tokens, callback) { + return markedInstance.walkTokens(tokens, callback); +}; +/** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ +marked.parseInline = markedInstance.parseInline; +/** + * Expose + */ +marked.Parser = _Parser; +marked.parser = _Parser.parse; +marked.Renderer = _Renderer; +marked.TextRenderer = _TextRenderer; +marked.Lexer = _Lexer; +marked.lexer = _Lexer.lex; +marked.Tokenizer = _Tokenizer; +marked.Hooks = _Hooks; +marked.parse = marked; +const options = marked.options; +const setOptions = marked.setOptions; +const use = marked.use; +const walkTokens = marked.walkTokens; +const parseInline = marked.parseInline; +const parse = marked; +const parser = _Parser.parse; +const lexer = _Lexer.lex; + +export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; +//# sourceMappingURL=marked.esm.js.map diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html new file mode 100644 index 00000000000..32ac36286a7 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html @@ -0,0 +1,31 @@ + + + + + + PDF viewer + + + + + + +
    +
    +
    + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs new file mode 100644 index 00000000000..8a4a6b76f5e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs @@ -0,0 +1,62 @@ +import { GlobalWorkerOptions } from '../pdfjs-dist/dist/build/pdf.min.mjs'; +import { EventBus, PDFLinkService, PDFFindController, PDFViewer } from '../pdfjs-dist/dist/web/pdf_viewer.mjs'; + +GlobalWorkerOptions.workerSrc = '../pdfjs-dist/dist/build/pdf.worker.min.mjs'; + +// Extract the file path from the URL query string. +const url = new URL(window.location); +const fileUrl = url.searchParams.get('file'); +if (!fileUrl) { + throw new Error('File not specified in the URL query string'); +} + +const container = document.getElementById('viewerContainer'); +const eventBus = new EventBus(); + +// Enable hyperlinks within PDF files. +const pdfLinkService = new PDFLinkService({ + eventBus, +}); + +// Enable the find controller. +const pdfFindController = new PDFFindController({ + eventBus, + linkService: pdfLinkService, +}); + +// Create the PDF viewer. +const pdfViewer = new PDFViewer({ + container, + eventBus, + linkService: pdfLinkService, + findController: pdfFindController, +}); +pdfLinkService.setViewer(pdfViewer); + +// Allow navigation to a citation from the URL hash. +eventBus.on('pagesinit', function () { + pdfLinkService.setHash(window.location.hash.substring(1)); +}); + +// Define how the "search" query parameter is handled. +eventBus.on('findfromurlhash', function(evt) { + eventBus.dispatch('find', { + source: evt.source, + type: '', + query: evt.query, + caseSensitive: false, + entireWord: false, + highlightAll: false, + findPrevious: false, + matchDiacritics: true, + }); +}); + +// Load and initialize the document. +const pdfDocument = await pdfjsLib.getDocument({ + url: fileUrl, + enableXfa: true, +}).promise; + +pdfViewer.setDocument(pdfDocument); +pdfLinkService.setDocument(pdfDocument, null); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md new file mode 100644 index 00000000000..8e77fba7d43 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md @@ -0,0 +1,10 @@ +pdfjs-dist version 4.10.38 +https://github.com/mozilla/pdf.js +License: Apache-2.0 + +To update, replace the following files with updated versions from https://www.npmjs.com/package/pdfjs-dist: +* `build/pdf.min.mjs` +* `build/pdf.worker.min.mjs` +* `web/pdf_viewer.css` +* `web/pdf_viewer.mjs` +* `web/images/loading-icon.gif` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs new file mode 100644 index 00000000000..d7cfa914562 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs @@ -0,0 +1,21 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},__webpack_exports__ = globalThis.pdfjsLib = {};t.d(__webpack_exports__,{AbortException:()=>AbortException,AnnotationEditorLayer:()=>AnnotationEditorLayer,AnnotationEditorParamsType:()=>m,AnnotationEditorType:()=>g,AnnotationEditorUIManager:()=>AnnotationEditorUIManager,AnnotationLayer:()=>AnnotationLayer,AnnotationMode:()=>p,ColorPicker:()=>ColorPicker,DOMSVGFactory:()=>DOMSVGFactory,DrawLayer:()=>DrawLayer,FeatureTest:()=>util_FeatureTest,GlobalWorkerOptions:()=>GlobalWorkerOptions,ImageKind:()=>_,InvalidPDFException:()=>InvalidPDFException,MissingPDFException:()=>MissingPDFException,OPS:()=>X,OutputScale:()=>OutputScale,PDFDataRangeTransport:()=>PDFDataRangeTransport,PDFDateString:()=>PDFDateString,PDFWorker:()=>PDFWorker,PasswordResponses:()=>K,PermissionFlag:()=>f,PixelsPerInch:()=>PixelsPerInch,RenderingCancelledException:()=>RenderingCancelledException,TextLayer:()=>TextLayer,TouchManager:()=>TouchManager,UnexpectedResponseException:()=>UnexpectedResponseException,Util:()=>Util,VerbosityLevel:()=>q,XfaLayer:()=>XfaLayer,build:()=>Nt,createValidAbsoluteUrl:()=>createValidAbsoluteUrl,fetchData:()=>fetchData,getDocument:()=>getDocument,getFilenameFromUrl:()=>getFilenameFromUrl,getPdfFilenameFromUrl:()=>getPdfFilenameFromUrl,getXfaPageViewport:()=>getXfaPageViewport,isDataScheme:()=>isDataScheme,isPdfFile:()=>isPdfFile,noContextMenu:()=>noContextMenu,normalizeUnicode:()=>normalizeUnicode,setLayerDimensions:()=>setLayerDimensions,shadow:()=>shadow,stopEvent:()=>stopEvent,version:()=>Ot});const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),i=[1,0,0,1,0,0],s=[.001,0,0,.001,0,0],n=1.35,a=1,r=2,o=4,l=16,h=32,d=64,c=128,u=256,p={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},g={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15},m={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,HIGHLIGHT_COLOR:31,HIGHLIGHT_DEFAULT_COLOR:32,HIGHLIGHT_THICKNESS:33,HIGHLIGHT_FREE:34,HIGHLIGHT_SHOW_ALL:35,DRAW_STEP:41},f={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},b=0,A=1,w=2,v=3,y=3,x=4,_={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},E=1,S=2,C=3,T=4,M=5,P=6,D=7,k=8,R=9,I=10,F=11,L=12,O=13,N=14,B=15,H=16,z=17,U=20,G=1,$=2,V=3,j=4,W=5,q={ERRORS:0,WARNINGS:1,INFOS:5},X={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93},K={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let Y=q.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(Y=t)}function getVerbosityLevel(){return Y}function info(t){Y>=q.INFOS&&console.log(`Info: ${t}`)}function warn(t){Y>=q.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;try{if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s))return s}catch{}return null}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const Q=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends Q{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends Q{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends Q{constructor(t){super(t,"InvalidPDFException")}}class MissingPDFException extends Q{constructor(t){super(t,"MissingPDFException")}}class UnexpectedResponseException extends Q{constructor(t,e){super(t,"UnexpectedResponseException");this.status=e}}class FormatError extends Q{constructor(t){super(t,"FormatError")}}class AbortException extends Q{constructor(t){super(t,"AbortException")}}function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,i){return`#${J[t]}${J[e]}${J[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e){return[t[0]*e[0]+t[1]*e[2]+e[4],t[0]*e[1]+t[1]*e[3]+e[5]]}static applyInverseTransform(t,e){const i=e[0]*e[3]-e[1]*e[2];return[(t[0]*e[3]-t[1]*e[2]+e[2]*e[5]-e[4]*e[3])/i,(-t[0]*e[1]+t[1]*e[0]+e[4]*e[1]-e[5]*e[0])/i]}static getAxialAlignedBoundingBox(t,e){const i=this.applyTransform(t,e),s=this.applyTransform(t.slice(2,4),e),n=this.applyTransform([t[0],t[3]],e),a=this.applyTransform([t[2],t[1]],e);return[Math.min(i[0],s[0],n[0],a[0]),Math.min(i[1],s[1],n[1],a[1]),Math.max(i[0],s[0],n[0],a[0]),Math.max(i[1],s[1],n[1],a[1])]}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t){const e=[t[0],t[2],t[1],t[3]],i=t[0]*e[0]+t[1]*e[2],s=t[0]*e[1]+t[1]*e[3],n=t[2]*e[0]+t[3]*e[2],a=t[2]*e[1]+t[3]*e[3],r=(i+a)/2,o=Math.sqrt((i+a)**2-4*(i*a-n*s))/2,l=r+o||1,h=r-o||1;return[Math.sqrt(l),Math.sqrt(h)]}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static#t(t,e,i,s,n,a,r,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*a)+3*c*r)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,a,r,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,a,r,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,a,r,o,(-h+p)/g,c);this.#t(t,e,i,s,n,a,r,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,a,r,o,l){if(l){l[0]=Math.min(l[0],t,r);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,r);l[3]=Math.max(l[3],e,o)}else l=[Math.min(t,r),Math.min(e,o),Math.max(t,r),Math.max(e,o)];this.#e(t,i,n,r,e,s,a,o,3*(3*(i-n)-t+r),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,r,e,s,a,o,3*(3*(s-a)-e+o),6*(e-2*s+a),3*(s-e),l);return l}}let Z=null,tt=null;function normalizeUnicode(t){if(!Z){Z=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;tt=new Map([["ſt","ſt"]])}return t.replaceAll(Z,((t,e,i)=>e?e.normalize("NFKC"):tt.get(i)))}const et="pdfjs_internal_id_";"function"!=typeof Promise.try&&(Promise.try=function(t,...e){return new Promise((i=>{i(t(...e))}))});const it="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"arraybuffer":return i.arrayBuffer();case"blob":return i.blob();case"json":return i.json()}return i.text()}return new Promise(((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType=e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"arraybuffer":case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)}))}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:a=0,dontFlip:r=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=a;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,m,f;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(r){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+a;m=(t[3]-t[1])*i;f=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+a;m=(t[2]-t[0])*i;f=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=m;this.height=f}get rawDims(){const{userUnit:t,viewBox:e}=this,i=e.map((e=>e*t));return shadow(this,"rawDims",{pageWidth:i[2]-i[0],pageHeight:i[3]-i[1],pageX:i[0],pageY:i[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){return Util.applyTransform([t,e],this.transform)}convertToViewportRectangle(t){const e=Util.applyTransform([t[0],t[1]],this.transform),i=Util.applyTransform([t[2],t[3]],this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){return Util.applyInverseTransform([t,e],this.transform)}}class RenderingCancelledException extends Q{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let a=parseInt(e[4],10);a=a>=0&&a<=23?a:0;let r=parseInt(e[5],10);r=r>=0&&r<=59?r:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){a+=h;r+=d}else if("+"===l){a-=h;r-=d}return new Date(Date.UTC(i,s,n,a,r,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,a=[0,0,parseInt(s),parseInt(n)];return new PageViewport({viewBox:a,userUnit:1,scale:e,rotation:i})}function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);warn(`Not a valid color format: "${t}"`);return[0,0,0]}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform();return[e,i,s,n,a,r]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform().invertSelf();return[e,i,s,n,a,r]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:a}=t,r=util_FeatureTest.isCSSRoundSupported,o=`var(--scale-factor) * ${s}px`,l=`var(--scale-factor) * ${n}px`,h=r?`round(down, ${o}, var(--scale-round-x, 1px))`:`calc(${o})`,d=r?`round(down, ${l}, var(--scale-round-y, 1px))`:`calc(${l})`;if(i&&e.rotation%180!=0){a.width=d;a.height=h}else{a.width=h;a.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const t=window.devicePixelRatio||1;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}}class EditorToolbar{#s=null;#n=null;#a;#r=null;#o=null;static#l=null;constructor(t){this.#a=t;EditorToolbar.#l||=Object.freeze({freetext:"pdfjs-editor-remove-freetext-button",highlight:"pdfjs-editor-remove-highlight-button",ink:"pdfjs-editor-remove-ink-button",stamp:"pdfjs-editor-remove-stamp-button"})}render(){const t=this.#s=document.createElement("div");t.classList.add("editToolbar","hidden");t.setAttribute("role","toolbar");const e=this.#a._uiManager._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.addEventListener("pointerdown",EditorToolbar.#h,{signal:e});const i=this.#r=document.createElement("div");i.className="buttons";t.append(i);const s=this.#a.toolbarPosition;if(s){const{style:e}=t,i="ltr"===this.#a._uiManager.direction?1-s[0]:s[0];e.insetInlineEnd=100*i+"%";e.top=`calc(${100*s[1]}% + var(--editor-toolbar-vert-offset))`}this.#d();return t}get div(){return this.#s}static#h(t){t.stopPropagation()}#c(t){this.#a._focusEventsAllowed=!1;stopEvent(t)}#u(t){this.#a._focusEventsAllowed=!0;stopEvent(t)}#p(t){const e=this.#a._uiManager._signal;t.addEventListener("focusin",this.#c.bind(this),{capture:!0,signal:e});t.addEventListener("focusout",this.#u.bind(this),{capture:!0,signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e})}hide(){this.#s.classList.add("hidden");this.#n?.hideDropdown()}show(){this.#s.classList.remove("hidden");this.#o?.shown()}#d(){const{editorType:t,_uiManager:e}=this.#a,i=document.createElement("button");i.className="delete";i.tabIndex=0;i.setAttribute("data-l10n-id",EditorToolbar.#l[t]);this.#p(i);i.addEventListener("click",(t=>{e.delete()}),{signal:e._signal});this.#r.append(i)}get#g(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#p(e);this.#r.prepend(e,this.#g);this.#o=t}addColorPicker(t){this.#n=t;const e=t.renderButton();this.#p(e);this.#r.prepend(e,this.#g)}remove(){this.#s.remove();this.#n?.destroy();this.#n=null}}class HighlightToolbar{#r=null;#s=null;#m;constructor(t){this.#m=t}#f(){const t=this.#s=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");t.addEventListener("contextmenu",noContextMenu,{signal:this.#m._signal});const e=this.#r=document.createElement("div");e.className="buttons";t.append(e);this.#b();return t}#A(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=a;i=t}else e?a>s&&(s=a):a{this.#m.highlightSelection("floating_button")}),{signal:i});this.#r.append(t)}}function bindEvents(t,e,i){for(const s of i)e.addEventListener(s,t[s].bind(t))}class IdManager{#w=0;get id(){return"pdfjs_internal_editor_"+this.#w++}}class ImageManager{#v=function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return bytesToString(t)}();#w=0;#y=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then((()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]})))}async#x(t,e){this.#y||=new Map;let i=this.#y.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#v}_${this.#w++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,a=new Promise(((t,a)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=a}));s.readAsDataURL(t);await a}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#y.set(t,i);i&&this.#y.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#x(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#x(t,t)}async getFromBlob(t,e){const i=await e;return this.#x(t,i)}async getFromId(t){this.#y||=new Map;const e=this.#y.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#y||=new Map;let i=this.#y.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#v}_${this.#w++}`,refCounter:1,isSvg:!1};this.#y.set(t,i);this.#y.set(i.id,i);return i}getSvgUrl(t){const e=this.#y.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#y||=new Map;const e=this.#y.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#v}_`)}}class CommandManager{#_=[];#E=!1;#S;#C=-1;constructor(t=128){this.#S=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:a=!1,keepUndo:r=!1}){s&&t();if(this.#E)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#C){this.#_.length>0&&(this.#_.length=0);this.#C=0;this.#_.push(o);return}if(a&&this.#_[this.#C].type===n){r&&(o.undo=this.#_[this.#C].undo);this.#_[this.#C]=o;return}const l=this.#C+1;if(l===this.#S)this.#_.splice(0,1);else{this.#C=l;l=0;e--)if(this.#_[e].type!==t){this.#_.splice(e+1,this.#C-e);this.#C=e;return}this.#_.length=0;this.#C=-1}}destroy(){this.#_=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=util_FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#T(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#T(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:a=[],checker:r=null}}=i;if(!r||r(t,e)){s.bind(t,...a,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every(((t,i)=>t===e[i])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#M=new AbortController;#P=null;#D=new Map;#k=new Map;#R=null;#I=null;#F=null;#L=new CommandManager;#O=null;#N=null;#B=0;#H=new Set;#z=null;#U=null;#G=new Set;_editorUndoBar=null;#$=!1;#V=!1;#j=!1;#W=null;#q=null;#X=null;#K=null;#Y=!1;#Q=null;#J=new IdManager;#Z=!1;#tt=!1;#et=null;#it=null;#st=null;#nt=null;#at=g.NONE;#rt=new Set;#ot=null;#lt=null;#ht=null;#dt={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#ct=[0,0];#ut=null;#pt=null;#gt=null;#mt=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#pt.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#pt.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#pt.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,a,r,o,l,h,d,c,u){const p=this._signal=this.#M.signal;this.#pt=t;this.#gt=e;this.#R=i;this._eventBus=s;s._on("editingaction",this.onEditingAction.bind(this),{signal:p});s._on("pagechanging",this.onPageChanging.bind(this),{signal:p});s._on("scalechanging",this.onScaleChanging.bind(this),{signal:p});s._on("rotationchanging",this.onRotationChanging.bind(this),{signal:p});s._on("setpreference",this.onSetPreference.bind(this),{signal:p});s._on("switchannotationeditorparams",(t=>this.updateParams(t.type,t.value)),{signal:p});this.#ft();this.#bt();this.#At();this.#I=n.annotationStorage;this.#W=n.filterFactory;this.#lt=a;this.#K=r||null;this.#$=o;this.#V=l;this.#j=h;this.#nt=d||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=c||null;this._supportsPinchToZoom=!1!==u}destroy(){this.#mt?.resolve();this.#mt=null;this.#M?.abort();this.#M=null;this._signal=null;for(const t of this.#k.values())t.destroy();this.#k.clear();this.#D.clear();this.#G.clear();this.#P=null;this.#rt.clear();this.#L.destroy();this.#R?.destroy();this.#Q?.hide();this.#Q=null;if(this.#q){clearTimeout(this.#q);this.#q=null}if(this.#ut){clearTimeout(this.#ut);this.#ut=null}this._editorUndoBar?.destroy()}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#nt}get useNewAltTextFlow(){return this.#V}get useNewAltTextWhenAddingImage(){return this.#j}get hcmFilter(){return shadow(this,"hcmFilter",this.#lt?this.#W.addHCMFilter(this.#lt.foreground,this.#lt.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#pt).direction)}get highlightColors(){return shadow(this,"highlightColors",this.#K?new Map(this.#K.split(",").map((t=>t.split("=").map((t=>t.trim()))))):null)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,(t=>t.reverse()))):null)}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#N=t}setMainHighlightColorPicker(t){this.#st=t}editAltText(t,e=!1){this.#R?.editAltText(this,t,e)}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#j=e}onPageChanging({pageNumber:t}){this.#B=t-1}focusMainContainer(){this.#pt.focus()}findParent(t,e){for(const i of this.#k.values()){const{x:s,y:n,width:a,height:r}=i.div.getBoundingClientRect();if(t>=s&&t<=s+a&&e>=n&&e<=n+r)return i}return null}disableUserSelect(t=!1){this.#gt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#G.add(t)}removeShouldRescale(t){this.#G.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#G)t.onScaleChanging();this.#N?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#wt({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#vt(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#k.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t=""){const e=document.getSelection();if(!e||e.isCollapsed)return;const{anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a}=e,r=e.toString(),o=this.#wt(e).closest(".textLayer"),l=this.getSelectionBoxes(o);if(!l)return;e.empty();const h=this.#vt(o),d=this.#at===g.NONE,callback=()=>{h?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:l,anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a,text:r});d&&this.showAllEditors("highlight",!0,!0)};d?this.switchToMode(g.HIGHLIGHT,callback):callback()}#yt(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#wt(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#Q||=new HighlightToolbar(this);this.#Q.show(e,i,"ltr"===this.direction)}}addToAnnotationStorage(t){t.isEmpty()||!this.#I||this.#I.has(t.id)||this.#I.setValue(t.id,t)}#xt(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#ot){this.#Q?.hide();this.#ot=null;this.#_t({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#ot)return;const i=this.#wt(t).closest(".textLayer");if(i){this.#Q?.hide();this.#ot=e;this.#_t({hasSelectedText:!0});if(this.#at===g.HIGHLIGHT||this.#at===g.NONE){this.#at===g.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#Y=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#at===g.HIGHLIGHT?this.#vt(i):null;t?.toggleDrawing();const e=new AbortController,s=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Et("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:s});window.addEventListener("blur",pointerup,{signal:s})}}}else if(this.#ot){this.#Q?.hide();this.#ot=null;this.#_t({hasSelectedText:!1})}}#Et(t=""){this.#at===g.HIGHLIGHT?this.highlightSelection(t):this.#$&&this.#yt()}#ft(){document.addEventListener("selectionchange",this.#xt.bind(this),{signal:this._signal})}#St(){if(this.#X)return;this.#X=new AbortController;const t=this.combinedSignal(this.#X);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Ct(){this.#X?.abort();this.#X=null}blur(){this.isShiftKeyDown=!1;if(this.#Y){this.#Y=!1;this.#Et("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#rt)if(e.div.contains(t)){this.#it=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#it)return;const[t,e]=this.#it;this.#it=null;e.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this._signal});e.focus()}#At(){if(this.#et)return;this.#et=new AbortController;const t=this.combinedSignal(this.#et);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Tt(){this.#et?.abort();this.#et=null}#Mt(){if(this.#O)return;this.#O=new AbortController;const t=this.combinedSignal(this.#O);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#Pt(){this.#O?.abort();this.#O=null}#bt(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#At();this.#Mt()}removeEditListeners(){this.#Tt();this.#Pt()}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#U)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#U)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#P?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#rt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#U)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#Dt(e);this.#kt(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#at===g.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#Y){this.#Y=!1;this.#Et("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu")}}#_t(t){if(Object.entries(t).some((([t,e])=>this.#dt[t]!==e))){this._eventBus.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#dt,t)});this.#at===g.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#Rt([[m.HIGHLIGHT_FREE,!0]])}}#Rt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#St();this.#Mt();this.#_t({isEditing:this.#at!==g.NONE,isEmpty:this.#It(),hasSomethingToUndo:this.#L.hasSomethingToUndo(),hasSomethingToRedo:this.#L.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Ct();this.#Pt();this.#_t({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#U){this.#U=t;for(const t of this.#U)this.#Rt(t.defaultPropertiesToUpdate)}}getId(){return this.#J.id}get currentLayer(){return this.#k.get(this.#B)}getLayer(t){return this.#k.get(t)}get currentPageIndex(){return this.#B}addLayer(t){this.#k.set(t.pageIndex,t);this.#Z?t.enable():t.disable()}removeLayer(t){this.#k.delete(t.pageIndex)}async updateMode(t,e=null,i=!1){if(this.#at!==t){if(this.#mt){await this.#mt.promise;if(!this.#mt)return}this.#mt=Promise.withResolvers();this.#at=t;if(t!==g.NONE){this.setEditingState(!0);await this.#Ft();this.unselectAll();for(const e of this.#k.values())e.updateMode(t);if(e){for(const t of this.#D.values())if(t.annotationElementId===e){this.setSelected(t);t.enterInEditMode()}else t.unselect();this.#mt.resolve()}else{i&&this.addNewEditorFromKeyboard();this.#mt.resolve()}}else{this.setEditingState(!1);this.#Lt();this._editorUndoBar?.hide();this.#mt.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t!==this.#at&&this._eventBus.dispatch("switchannotationeditormode",{source:this,mode:t})}updateParams(t,e){if(this.#U){switch(t){case m.CREATE:this.currentLayer.addNewEditor();return;case m.HIGHLIGHT_DEFAULT_COLOR:this.#st?.updateColor(e);break;case m.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#ht||=new Map).set(t,e);this.showAllEditors("highlight",e)}for(const i of this.#rt)i.updateParams(t,e);for(const i of this.#U)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#D.values())i.editorType===t&&i.show(e);(this.#ht?.get(m.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#Rt([[m.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#tt!==t){this.#tt=t;for(const e of this.#k.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Ft(){if(!this.#Z){this.#Z=!0;const t=[];for(const e of this.#k.values())t.push(e.enable());await Promise.all(t);for(const t of this.#D.values())t.enable()}}#Lt(){this.unselectAll();if(this.#Z){this.#Z=!1;for(const t of this.#k.values())t.disable();for(const t of this.#D.values())t.disable()}}getEditors(t){const e=[];for(const i of this.#D.values())i.pageIndex===t&&e.push(i);return e}getEditor(t){return this.#D.get(t)}addEditor(t){this.#D.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#q&&clearTimeout(this.#q);this.#q=setTimeout((()=>{this.focusMainContainer();this.#q=null}),0)}this.#D.delete(t.id);this.unselect(t);t.annotationElementId&&this.#H.has(t.annotationElementId)||this.#I?.remove(t.id)}addDeletedAnnotationElement(t){this.#H.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#H.has(t)}removeDeletedAnnotationElement(t){this.#H.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#Dt(t){const e=this.#k.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#P!==t){this.#P=t;t&&this.#Rt(t.propertiesToUpdate)}}get#Ot(){let t=null;for(t of this.#rt);return t}updateUI(t){this.#Ot===t&&this.#Rt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#Rt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#rt.has(t)){this.#rt.delete(t);t.unselect();this.#_t({hasSelectedEditor:this.hasSelection})}else{this.#rt.add(t);t.select();this.#Rt(t.propertiesToUpdate);this.#_t({hasSelectedEditor:!0})}}setSelected(t){this.#N?.commitOrRemove();for(const e of this.#rt)e!==t&&e.unselect();this.#rt.clear();this.#rt.add(t);t.select();this.#Rt(t.propertiesToUpdate);this.#_t({hasSelectedEditor:!0})}isSelected(t){return this.#rt.has(t)}get firstSelectedEditor(){return this.#rt.values().next().value}unselect(t){t.unselect();this.#rt.delete(t);this.#_t({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#rt.size}get isEnterHandled(){return 1===this.#rt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#L.undo();this.#_t({hasSomethingToUndo:this.#L.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#It()});this._editorUndoBar?.hide()}redo(){this.#L.redo();this.#_t({hasSomethingToUndo:!0,hasSomethingToRedo:this.#L.hasSomethingToRedo(),isEmpty:this.#It()})}addCommands(t){this.#L.add(t);this.#_t({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#It()})}cleanUndoStack(t){this.#L.cleanType(t)}#It(){if(0===this.#D.size)return!0;if(1===this.#D.size)for(const t of this.#D.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#rt],undo=()=>{for(const t of e)this.#Dt(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#P?.commitOrRemove()}hasSomethingToControl(){return this.#P||this.hasSelection}#kt(t){for(const t of this.#rt)t.unselect();this.#rt.clear();for(const e of t)if(!e.isEmpty()){this.#rt.add(e);e.select()}this.#_t({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#rt)t.commit();this.#kt(this.#D.values())}unselectAll(){if(this.#P){this.#P.commitOrRemove();if(this.#at!==g.NONE)return}if(!this.#N?.commitOrRemove()&&this.hasSelection){for(const t of this.#rt)t.unselect();this.#rt.clear();this.#_t({hasSelectedEditor:!1})}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#ct[0]+=t;this.#ct[1]+=e;const[s,n]=this.#ct,a=[...this.#rt];this.#ut&&clearTimeout(this.#ut);this.#ut=setTimeout((()=>{this.#ut=null;this.#ct[0]=this.#ct[1]=0;this.addCommands({cmd:()=>{for(const t of a)this.#D.has(t.id)&&t.translateInPage(s,n)},undo:()=>{for(const t of a)this.#D.has(t.id)&&t.translateInPage(-s,-n)},mustExec:!1})}),1e3);for(const i of a)i.translateInPage(t,e)}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#z=new Map;for(const t of this.#rt)this.#z.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#z)return!1;this.disableUserSelect(!1);const t=this.#z;this.#z=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},a]of t){a.newX=i;a.newY=s;a.newPageIndex=n;e||=i!==a.savedX||s!==a.savedY||n!==a.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#D.has(t.id)){const n=this.#k.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#z)for(const i of this.#z.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#rt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#P===t}getActive(){return this.#P}getMode(){return this.#at}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/a,y:1-(t+r-i)/n,width:o/a,height:r/n});break;case"180":r=(t,e,r,o)=>({x:1-(t+r-i)/n,y:1-(e+o-s)/a,width:r/n,height:o/a});break;case"270":r=(t,e,r,o)=>({x:1-(e+o-s)/a,y:(t-i)/n,width:o/a,height:r/n});break;default:r=(t,e,r,o)=>({x:(t-i)/n,y:(e-s)/a,width:r/n,height:o/a})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation()),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#Wt&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#Xt}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",(e=>{if(e.target===t&&"Enter"===e.key){this.#Gt=!0;onClick(e)}}),{signal:i});await this.#Kt();return t}get#Xt(){return(this.#o?"added":null===this.#o&&this.guessedText&&"review")||"missing"}finish(){if(this.#Bt){this.#Bt.focus({focusVisible:this.#Gt});this.#Gt=!1}}isEmpty(){return this.#Wt?null===this.#o:!this.#o&&!this.#Nt}hasData(){return this.#Wt?null!==this.#o||!!this.#Vt:this.isEmpty()}get guessedText(){return this.#Vt}async setGuessedText(t){if(null===this.#o){this.#Vt=t;this.#jt=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#Kt()}}toggleAltTextBadge(t=!1){if(this.#Wt&&!this.#o){if(!this.#$t){const t=this.#$t=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#$t.classList.toggle("hidden",!t)}else{this.#$t?.remove();this.#$t=null}}serialize(t){let e=this.#o;t||this.#Vt!==e||(e=this.#jt);return{altText:e,decorative:this.#Nt,guessedText:this.#Vt,textWithDisclaimer:this.#jt}}get data(){return{altText:this.#o,decorative:this.#Nt}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#Vt=i;this.#jt=s}if(this.#o!==t||this.#Nt!==e){if(!n){this.#o=t;this.#Nt=e}this.#Kt()}}toggle(t=!1){if(this.#Bt){if(!t&&this.#Ut){clearTimeout(this.#Ut);this.#Ut=null}this.#Bt.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#Xt}})}destroy(){this.#Bt?.remove();this.#Bt=null;this.#Ht=null;this.#zt=null;this.#$t?.remove();this.#$t=null}async#Kt(){const t=this.#Bt;if(!t)return;if(this.#Wt){t.classList.toggle("done",!!this.#o);t.setAttribute("data-l10n-id",AltText.#qt[this.#Xt]);this.#Ht?.setAttribute("data-l10n-id",AltText.#qt[`${this.#Xt}-label`]);if(!this.#o){this.#zt?.remove();return}}else{if(!this.#o&&!this.#Nt){t.classList.remove("done");this.#zt?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#zt;if(!e){this.#zt=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",(()=>{clearTimeout(this.#Ut);this.#Ut=null}),{once:!0});t.addEventListener("mouseenter",(()=>{this.#Ut=setTimeout((()=>{this.#Ut=null;this.#zt.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})}),i)}),{signal:s});t.addEventListener("mouseleave",(()=>{if(this.#Ut){clearTimeout(this.#Ut);this.#Ut=null}this.#zt?.classList.remove("show")}),{signal:s})}if(this.#Nt)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#o}e.parentNode||t.append(e);const i=this.#a.getImageForAltText();i?.setAttribute("aria-describedby",e.id)}}class TouchManager{#pt;#Yt=!1;#Qt=null;#Jt;#Zt;#te;#ee;#ie;#se=null;#ne;#ae=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:a=null,signal:r}){this.#pt=t;this.#Qt=i;this.#Jt=e;this.#Zt=s;this.#te=n;this.#ee=a;this.#ne=new AbortController;this.#ie=AbortSignal.any([r,this.#ne.signal]);t.addEventListener("touchstart",this.#re.bind(this),{passive:!1,signal:this.#ie})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return shadow(this,"MIN_TOUCH_DISTANCE_TO_PINCH",35/(window.devicePixelRatio||1))}#re(t){if(this.#Jt?.()||t.touches.length<2)return;if(!this.#ae){this.#ae=new AbortController;const t=AbortSignal.any([this.#ie,this.#ae.signal]),e=this.#pt,i={signal:t,passive:!1};e.addEventListener("touchmove",this.#oe.bind(this),i);e.addEventListener("touchend",this.#le.bind(this),i);e.addEventListener("touchcancel",this.#le.bind(this),i);this.#Zt?.()}stopEvent(t);if(2!==t.touches.length||this.#Qt?.()){this.#se=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#se={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#oe(t){if(!this.#se||2!==t.touches.length)return;let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:a,screenY:r}=i,o=this.#se,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=a-s,m=r-n,f=Math.hypot(g,m)||1,b=Math.hypot(u,p)||1;if(!this.#Yt&&Math.abs(b-f)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=a;o.touch1Y=r;t.preventDefault();if(!this.#Yt){this.#Yt=!0;return}const A=[(s+a)/2,(n+r)/2];this.#te?.(A,b,f)}#le(t){this.#ae.abort();this.#ae=null;this.#ee?.();if(this.#se){t.preventDefault();this.#se=null;this.#Yt=!1}}destroy(){this.#ne?.abort();this.#ne=null}}class AnnotationEditor{#he=null;#de=null;#o=null;#ce=!1;#ue=null;#pe="";#ge=!1;#me=null;#fe=null;#be=null;#Ae=null;#we="";#ve=!1;#ye=null;#xe=!1;#_e=!1;#Ee=!1;#Se=null;#Ce=0;#Te=0;#Me=null;#Pe=null;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#De=!1;#ke=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:a}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,a];const[r,o]=this.parentDimensions;this.x=t.x/r;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#De}set _isDraggable(t){this.#De=t;this.div?.classList.toggle("draggable",t)}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#ke}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else this.#Re();this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#ve?this.#ve=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}#Ie([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#Ie(this.parentDimensions,t,e)}translateInPage(t,e){this.#ye||=[this.x,this.y,this.width,this.height];this.#Ie(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}drag(t,e){this.#ye||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:a,y:r}=this;const[o,l]=this.getBaseTranslation();a+=o;r+=l;const{style:h}=i;h.left=`${(100*a).toFixed(2)}%`;h.top=`${(100*r).toFixed(2)}%`;this._onTranslating(a,r);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#ye&&(this.#ye[0]!==this.x||this.#ye[1]!==this.y)}get _hasBeenResized(){return!!this.#ye&&(this.#ye[2]!==this.width||this.#ye[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:a,width:r,height:o}=this;r*=i;o*=s;n*=i;a*=s;if(this._mustFixPosition)switch(t){case 0:n=Math.max(0,Math.min(i-r,n));a=Math.max(0,Math.min(s-o,a));break;case 90:n=Math.max(0,Math.min(i-o,n));a=Math.min(s,Math.max(r,a));break;case 180:n=Math.min(i,Math.max(r,n));a=Math.min(s,Math.max(o,a));break;case 270:n=Math.min(i,Math.max(o,n));a=Math.max(0,Math.min(s-r,a))}this.x=n/=i;this.y=a/=s;const[l,h]=this.getBaseTranslation();n+=l;a+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*a).toFixed(2)}%`;this.moveInDOM()}static#Fe(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#Fe(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#Fe(t,e,360-this.parentRotation)}#Le(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(t,e){const[i,s]=this.parentDimensions,{style:n}=this.div;n.width=`${(100*t/i).toFixed(2)}%`;this.#ge||(n.height=`${(100*e/s).toFixed(2)}%`)}fixDims(){const{style:t}=this.div,{height:e,width:i}=t,s=i.endsWith("%"),n=!this.#ge&&e.endsWith("%");if(s&&n)return;const[a,r]=this.parentDimensions;s||(t.width=`${(100*parseFloat(i)/a).toFixed(2)}%`);this.#ge||n||(t.height=`${(100*parseFloat(e)/r).toFixed(2)}%`)}getInitialTranslation(){return[0,0]}#Oe(){if(this.#me)return;this.#me=document.createElement("div");this.#me.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#me.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#Ne.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#me)}#Ne(t,e){e.preventDefault();const{isMac:i}=util_FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#o?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#fe=[e.screenX,e.screenY];const n=new AbortController,a=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#Be.bind(this,t),{passive:!0,capture:!0,signal:a});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:a});window.addEventListener("contextmenu",noContextMenu,{signal:a});this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const r=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#o?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=r;this.div.style.cursor=o;this.#He()};window.addEventListener("pointerup",pointerUpCallback,{signal:a});window.addEventListener("blur",pointerUpCallback,{signal:a})}#ze(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;const[n,a]=this.parentDimensions;this.setDims(n*i,a*s);this.fixAndSetPosition();this._onResized()}_onResized(){}#He(){if(!this.#be)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#be;this.#be=null;const n=this.x,a=this.y,r=this.width,o=this.height;n===t&&a===e&&r===i&&o===s||this.addCommands({cmd:this.#ze.bind(this,n,a,r,o),undo:this.#ze.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#Be(t,e){const[i,s]=this.parentDimensions,n=this.x,a=this.y,r=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#Le(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#Le(360-this.rotation);let u,p,g=!1,m=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":m=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":m=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const f=u(r,o),b=p(r,o);let A=transf(...b);const w=AnnotationEditor._round(n+A[0]),v=AnnotationEditor._round(a+A[1]);let y,x,_=1,E=1;if(e.fromKeyboard)({deltaX:y,deltaY:x}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#fe;[y,x]=this.screenToPageTranslation(t-s,i-n);this.#fe[0]=t;this.#fe[1]=i}[y,x]=(S=y/i,C=x/s,[c[0]*S+c[2]*C,c[1]*S+c[3]*C]);var S,C;if(g){const t=Math.hypot(r,o);_=E=Math.max(Math.min(Math.hypot(b[0]-f[0]-y,b[1]-f[1]-x)/t,1/r,1/o),l/r,h/o)}else m?_=Math.max(l,Math.min(1,Math.abs(b[0]-f[0]-y)))/r:E=Math.max(h,Math.min(1,Math.abs(b[1]-f[1]-x)))/o;const T=AnnotationEditor._round(r*_),M=AnnotationEditor._round(o*E);A=transf(...p(T,M));const P=w-A[0],D=v-A[1];this.#ye||=[this.x,this.y,this.width,this.height];this.width=T;this.height=M;this.x=P;this.y=D;this.setDims(i*T,s*M);this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#o?.finish()}async addEditToolbar(){if(this._editToolbar||this.#_e)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());this.#o&&await this._editToolbar.addAltText(this.#o);return this._editToolbar}removeEditToolbar(){if(this._editToolbar){this._editToolbar.remove();this._editToolbar=null;this.#o?.destroy()}}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}async addAltTextButton(){if(!this.#o){AltText.initialize(AnnotationEditor._l10n);this.#o=new AltText(this);if(this.#he){this.#o.data=this.#he;this.#he=null}await this.addEditToolbar()}}get altTextData(){return this.#o?.data}set altTextData(t){this.#o&&(this.#o.data=t)}get guessedAltText(){return this.#o?.guessedText}async setGuessedAltText(t){await(this.#o?.setGuessedText(t))}serializeAltText(t){return this.#o?.serialize(t)}hasAltText(){return!!this.#o&&!this.#o.isEmpty()}hasAltTextData(){return this.#o?.hasData()??!1}render(){this.div=document.createElement("div");this.div.setAttribute("data-editor-rotation",(360-this.rotation)%360);this.div.className=this.name;this.div.setAttribute("id",this.id);this.div.tabIndex=this.#ce?-1:0;this._isVisible||this.div.classList.add("hidden");this.setInForeground();this.#Ue();const[t,e]=this.parentDimensions;if(this.parentRotation%180!=0){this.div.style.maxWidth=`${(100*e/t).toFixed(2)}%`;this.div.style.maxHeight=`${(100*t/e).toFixed(2)}%`}const[i,s]=this.getInitialTranslation();this.translate(i,s);bindEvents(this,this.div,["pointerdown"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#Pe||=new TouchManager({container:this.div,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#Ge.bind(this),onPinching:this.#$e.bind(this),onPinchEnd:this.#Ve.bind(this),signal:this._uiManager._signal}));this._uiManager._editorUndoBar?.hide();return this.div}#Ge(){this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#o?.toggle(!1);this.parent.togglePointerEvents(!1)}#$e(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#Le(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[a,r]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/a,u=AnnotationEditor.MIN_SIZE/r;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#ye||=[o,l,h,d];const m=transf(h/2,d/2),f=AnnotationEditor._round(o+m[0]),b=AnnotationEditor._round(l+m[1]),A=transf(p/2,g/2);this.x=f-A[0];this.y=b-A[1];this.width=p;this.height=g;this.setDims(a*p,r*g);this.fixAndSetPosition();this._onResizing()}#Ve(){this.#o?.toggle(!0);this.parent.togglePointerEvents(!0);this.#He()}pointerdown(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#ve=!0;this._isDraggable?this.#je(t):this.#We(t)}}get isSelected(){return this._uiManager.isSelected(this)}#We(t){const{isMac:e}=util_FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#je(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),a={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#ue=null;this.#ve=!1;this._uiManager.endDragSession()||this.#We(t);i&&this._onStopDragging()};if(e){this.#Ce=t.clientX;this.#Te=t.clientY;this.#ue=t.pointerId;this.#pe=t.pointerType;window.addEventListener("pointermove",(t=>{if(!i){i=!0;this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#ue){stopEvent(t);return}const[a,r]=this.screenToPageTranslation(e-this.#Ce,s-this.#Te);this.#Ce=e;this.#Te=s;this._uiManager.dragSelectedEditors(a,r)}),a);window.addEventListener("touchmove",stopEvent,a);window.addEventListener("pointerdown",(t=>{t.pointerType===this.#pe&&(this.#Pe||t.isPrimary)&&cancelDrag(t);stopEvent(t)}),a)}const pointerUpCallback=t=>{this.#ue&&this.#ue!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#Se&&clearTimeout(this.#Se);this.#Se=setTimeout((()=>{this.#Se=null;this.parent?.moveEditorInDOM(this)}),0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,a]=this.pageDimensions,[r,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*a,u=this.width*n,p=this.height*a;switch(i){case 0:return[d+l+r,a-c-h-p+o,d+l+u+r,a-c-h+o];case 90:return[d+h+r,a-c+l+o,d+h+p+r,a-c+l+u+o];case 180:return[d-l-u+r,a-c+h+o,d-l+r,a-c+h+p+o];case 270:return[d-h-p+r,a-c-l-u+o,d-h+r,a-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,a]=t,r=n-i,o=a-s;switch(this.rotation){case 0:return[i,e-a,r,o];case 90:return[i,e-s,o,r];case 180:return[n,e-s,r,o];case 270:return[n,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){this.#_e=!0}disableEditMode(){this.#_e=!1}isInEditMode(){return this.#_e}shouldGetKeyboardEvents(){return this.#Ee}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:a}=window;return e0&&t0}#Ue(){if(this.#Ae||!this.div)return;this.#Ae=new AbortController;const t=this._uiManager.combinedSignal(this.#Ae);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#Ue()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){unreachable("An editor must be serializable")}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:i});s.rotation=t.rotation;s.#he=t.accessibilityData;const[n,a]=s.pageDimensions,[r,o,l,h]=s.getRectInCurrentCoords(t.rect,a);s.x=r/n;s.y=o/a;s.width=l/n;s.height=h/a;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#Ae?.abort();this.#Ae=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);if(this.#Se){clearTimeout(this.#Se);this.#Se=null}this.#Re();this.removeEditToolbar();if(this.#Me){for(const t of this.#Me.values())clearTimeout(t);this.#Me=null}this.parent=null;this.#Pe?.destroy();this.#Pe=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#Oe();this.#me.classList.remove("hidden");bindEvents(this,this.div,["keydown"])}}get toolbarPosition(){return null}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#me.children;if(!this.#de){this.#de=Array.from(e);const t=this.#qe.bind(this),i=this.#Xe.bind(this),s=this._uiManager._signal;for(const e of this.#de){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#Ke.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#de[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#de.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()}))}unselect(){this.#me?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#o?.toggleAltTextBadge(!0)}updateParams(t,e){}disableEditing(){}enableEditing(){}enterInEditMode(){}getImageForAltText(){return null}get contentDiv(){return this.div}get isEditing(){return this.#xe}set isEditing(t){this.#xe=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}setAspectRatio(t,e){this.#ge=!0;const i=t/e,{style:s}=this.div;s.aspectRatio=i;s.height="auto"}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#Me||=new Map;const{action:e}=t;let i=this.#Me.get(e);i&&clearTimeout(i);i=setTimeout((()=>{this._reportTelemetry(t);this.#Me.delete(e);0===this.#Me.size&&(this.#Me=null)}),AnnotationEditor._telemetryTimeout);this.#Me.set(e,i)}else{t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#ce=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#ce=!0}renderAnnotationElement(t){let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const st=3285377520,nt=4294901760,at=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:st;this.h2=t?4294967295&t:st}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,a=new Uint32Array(e.buffer,0,s);let r=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;r=r*c&nt|r*p&at;l^=r;l=l<<13|l>>>19;l=5*l+3864292196}else{o=a[t];o=o*d&nt|o*u&at;o=o<<15|o>>>17;o=o*c&nt|o*p&at;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}r=0;switch(n){case 3:r^=e[4*s+2]<<16;case 2:r^=e[4*s+1]<<8;case 1:r^=e[4*s];r=r*d&nt|r*u&at;r=r<<15|r>>>17;r=r*c&nt|r*p&at;1&s?l^=r:h^=r}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&nt|36045*t&at;e=4283543511*e&nt|(2950163797*(e<<16|t>>>16)&nt)>>>16;t^=e>>>1;t=444984403*t&nt|60499*t&at;e=3301882366*e&nt|(3120437893*(e<<16|t>>>16)&nt)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const rt=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#Qe=!1;#Je=null;#Ze=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#Ze.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#Ze.get(t)}remove(t){this.#Ze.delete(t);0===this.#Ze.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#Ze.values())if(t instanceof AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#Ze.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#Ze.set(t,e)}s&&this.#ti();e instanceof AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#Ze.has(t)}getAll(){return this.#Ze.size>0?objectFromMap(this.#Ze):null}setAll(t){for(const[e,i]of Object.entries(t))this.setValue(e,i)}get size(){return this.#Ze.size}#ti(){if(!this.#Qe){this.#Qe=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#Qe){this.#Qe=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#Ze.size)return rt;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,a]of this.#Ze){const r=a instanceof AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);n||=!!r.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:rt}get editorStats(){let t=null;const e=new Map;for(const i of this.#Ze.values()){if(!(i instanceof AnnotationEditor))continue;const s=i.telemetryFinalData;if(!s)continue;const{type:n}=s;e.has(n)||e.set(n,Object.getPrototypeOf(i).constructor);t||=Object.create(null);const a=t[n]||=new Map;for(const[t,e]of Object.entries(s)){if("type"===t)continue;let i=a.get(t);if(!i){i=new Map;a.set(t,i)}const s=i.get(e)??0;i.set(e,s+1)}}for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#Je=null}get modifiedIds(){if(this.#Je)return this.#Je;const t=[];for(const e of this.#Ze.values())e instanceof AnnotationEditor&&e.annotationElementId&&e.serialize()&&t.push(e.annotationElementId);return this.#Je={ids:new Set(t),hash:t.join(",")}}}class PrintAnnotationStorage extends AnnotationStorage{#ei;constructor(t){super();const{map:e,hash:i,transfer:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#ei={map:n,hash:i,transfer:s}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#ei}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}class FontLoader{#ii=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ii.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,_inspectFont:e}){if(t&&!this.#ii.has(t.loadedName)){assert(!this.disableFontFace,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:i,src:s,style:n}=t,a=new FontFace(i,s,n);this.addNativeFontFace(a);try{await a.load();this.#ii.add(i);e?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(a)}}else unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)}))}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){let t=!1;(e||"undefined"!=typeof navigator&&"string"==typeof navigator?.userAgent&&/Mozilla\/5.0.*?rv:\d+.*? Gecko/.test(navigator.userAgent))&&(t=!0);return shadow(this,"isSyncFontLoadingSupported",t)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const a=n.getContext("2d");let r=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++r>30){warn("Load test font never loaded.");e();return}a.font="30px "+t;a.fillText(".",0,20);a.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,(()=>{u.remove();e.complete()}))}}class FontFaceObject{constructor(t,{disableFontFace:e=!1,fontExtraProperties:i=!1,inspectFont:s=null}){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this.disableFontFace=!0===e;this.fontExtraProperties=!0===i;this._inspectFont=s}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${function toBase64Util(t){return Uint8Array.prototype.toBase64?t.toBase64():btoa(bytesToString(t))}(this.data)});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=new Path2D(s||"");this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}}const ot=1,lt=2,ht=1,dt=2,ct=3,ut=4,pt=5,gt=6,mt=7,ft=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof MissingPDFException||t instanceof PasswordException||t instanceof UnexpectedResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"MissingPDFException":return new MissingPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"UnexpectedResponseException":return new UnexpectedResponseException(t.message,t.status);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#si=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#ni.bind(this),{signal:this.#si.signal})}#ni({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#ai(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===ot)i.resolve(t.data);else{if(t.callback!==lt)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then((function(e){n.postMessage({sourceName:i,targetName:s,callback:ot,callbackId:t.callbackId,data:e})}),(function(e){n.postMessage({sourceName:i,targetName:s,callback:lt,callbackId:t.callbackId,reason:wrapReason(e)})}))}else t.streamId?this.#ri(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,a=this.sourceName,r=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:a,targetName:r,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:a,targetName:r,stream:gt,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:a,targetName:r,stream:ht,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#ri(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this,r=this.actionHandler[t.action],o={enqueue(t,a=1,r){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=a;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:ut,streamId:e,chunk:t},r)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:ct,streamId:e});delete a.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:pt,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(r,t.data,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:ft,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:ft,streamId:e,reason:wrapReason(t)})}))}#ai(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this.streamControllers[e],r=this.streamSinks[e];switch(t.stream){case ft:t.success?a.startCall.resolve():a.startCall.reject(wrapReason(t.reason));break;case mt:t.success?a.pullCall.resolve():a.pullCall.reject(wrapReason(t.reason));break;case gt:if(!r){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,success:!0});break}r.desiredSize<=0&&t.desiredSize>0&&r.sinkCapability.resolve();r.desiredSize=t.desiredSize;Promise.try(r.onPull||onFn).then((function(){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,reason:wrapReason(t)})}));break;case ut:assert(a,"enqueue should have stream controller");if(a.isClosed)break;a.controller.enqueue(t.chunk);break;case ct:assert(a,"close should have stream controller");if(a.isClosed)break;a.isClosed=!0;a.controller.close();this.#oi(a,e);break;case pt:assert(a,"error should have stream controller");a.controller.error(wrapReason(t.reason));this.#oi(a,e);break;case dt:t.success?a.cancelCall.resolve():a.cancelCall.reject(wrapReason(t.reason));this.#oi(a,e);break;case ht:if(!r)break;const o=wrapReason(t.reason);Promise.try(r.onCancel||onFn,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:dt,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:dt,streamId:e,reason:wrapReason(t)})}));r.sinkCapability.reject(o);r.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#oi(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#si?.abort();this.#si=null}}class BaseCanvasFactory{#li=!1;constructor({enableHWA:t=!1}){this.#li=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#li})}}reset(t,e,i){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=i}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!0}){this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error("Ensure that the `cMapUrl` and `cMapPacked` API parameters are provided.");if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":"");return this._fetch(e).then((t=>({cMapData:t,isCompressed:this.isCompressed}))).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){const e=await fetchData(t,this.isCompressed?"arraybuffer":"text");return e instanceof ArrayBuffer?new Uint8Array(e):stringToBytes(e)}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `standardFontDataUrl` API parameter is provided.");if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}e&&warn("Please use the `legacy` build in Node.js environments.");async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs"),i=await e.promises.readFile(t);return new Uint8Array(i)}const bt="Fill",At="Stroke",wt="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,i,s){let n;if(s===At||s===bt){const a=e.current.getClippedPathBoundingBox(s,getCurrentTransform(t))||[0,0,0,0],r=Math.ceil(a[2]-a[0])||1,o=Math.ceil(a[3]-a[1])||1,l=e.cachedCanvases.getCanvas("pattern",r,o),h=l.context;h.clearRect(0,0,h.canvas.width,h.canvas.height);h.beginPath();h.rect(0,0,h.canvas.width,h.canvas.height);h.translate(-a[0],-a[1]);i=Util.transform(i,[1,0,0,1,a[0],a[1]]);h.transform(...e.baseTransform);this.matrix&&h.transform(...this.matrix);applyBoundingBox(h,this._bbox);h.fillStyle=this._createGradient(h);h.fill();n=t.createPattern(l.canvas,"no-repeat");const d=new DOMMatrix(i);n.setTransform(d)}else{applyBoundingBox(t,this._bbox);n=this._createGradient(t)}return n}}function drawTriangle(t,e,i,s,n,a,r,o){const l=e.coords,h=e.colors,d=t.data,c=4*t.width;let u;if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=r;r=o;o=u}if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}const p=(l[i]+e.offsetX)*e.scaleX,g=(l[i+1]+e.offsetY)*e.scaleY,m=(l[s]+e.offsetX)*e.scaleX,f=(l[s+1]+e.offsetY)*e.scaleY,b=(l[n]+e.offsetX)*e.scaleX,A=(l[n+1]+e.offsetY)*e.scaleY;if(g>=A)return;const w=h[a],v=h[a+1],y=h[a+2],x=h[r],_=h[r+1],E=h[r+2],S=h[o],C=h[o+1],T=h[o+2],M=Math.round(g),P=Math.round(A);let D,k,R,I,F,L,O,N;for(let t=M;t<=P;t++){if(tA?1:f===A?0:(f-t)/(f-A);D=m-(m-b)*e;k=x-(x-S)*e;R=_-(_-C)*e;I=E-(E-T)*e}let e;e=tA?1:(g-t)/(g-A);F=p-(p-b)*e;L=w-(w-S)*e;O=v-(v-C)*e;N=y-(y-T)*e;const i=Math.round(Math.min(D,F)),s=Math.round(Math.max(D,F));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(D-t)/(D-F);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-L)*e|0;d[n++]=R-(R-O)*e|0;d[n++]=I-(I-N)*e|0;d[n++]=255}}}function drawFigure(t,e,i){const s=e.coords,n=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(s.length/o)-1,h=o-1;for(a=0;a=Math.ceil(p*b)?w=o:y=!0;E>=Math.ceil(g*A)?v=l:x=!0;const S=this.getSizeAndScale(w,this.ctx.canvas.width,b),C=this.getSizeAndScale(v,this.ctx.canvas.height,A),T=t.cachedCanvases.getCanvas("pattern",S.size,C.size),M=T.context,P=r.createCanvasGraphics(M);P.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(P,s,a);M.translate(-S.scale*h,-C.scale*d);P.transform(S.scale,0,0,C.scale,0,0);M.save();this.clipBbox(P,h,d,c,u);P.baseTransform=getCurrentTransform(P.ctx);P.executeOperatorList(i);P.endDrawing();M.restore();if(y||x){const e=T.canvas;y&&(w=o);x&&(v=l);const i=this.getSizeAndScale(w,this.ctx.canvas.width,b),s=this.getSizeAndScale(v,this.ctx.canvas.height,A),n=i.size,a=s.size,r=t.cachedCanvases.getCanvas("pattern-workaround",n,a),c=r.context,u=y?Math.floor(p/o):0,m=x?Math.floor(g/l):0;for(let t=0;t<=u;t++)for(let i=0;i<=m;i++)c.drawImage(e,n*t,a*i,n,a,0,0,n,a);return{canvas:r.canvas,scaleX:i.scale,scaleY:s.scale,offsetX:h,offsetY:d}}return{canvas:T.canvas,scaleX:S.scale,scaleY:C.scale,offsetX:h,offsetY:d}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const a=s-e,r=n-i;t.ctx.rect(e,i,a,r);t.current.updateRectMinMax(getCurrentTransform(t.ctx),[e,i,s,n]);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;switch(e){case vt:const t=this.ctx;s.fillStyle=t.fillStyle;s.strokeStyle=t.strokeStyle;n.fillColor=t.fillStyle;n.strokeColor=t.strokeStyle;break;case yt:const a=Util.makeHexColor(i[0],i[1],i[2]);s.fillStyle=a;s.strokeStyle=a;n.fillColor=a;n.strokeColor=a;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}getPattern(t,e,i,s){let n=i;if(s!==wt){n=Util.transform(n,e.baseTransform);this.matrix&&(n=Util.transform(n,this.matrix))}const a=this.createPatternCanvas(e);let r=new DOMMatrix(n);r=r.translate(a.offsetX,a.offsetY);r=r.scale(1/a.scaleX,1/a.scaleY);const o=t.createPattern(a.canvas,"repeat");o.setTransform(r);return o}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:a=4294967295,inverseDecode:r=!1}){const o=util_FeatureTest.isLittleEndian?4278190080:255,[l,h]=r?[a,o]:[o,a],d=s>>3,c=7&s,u=t.length;i=new Uint32Array(i.buffer);let p=0;for(let s=0;s>2),m=i.length,f=s+7>>3,b=4294967295,A=util_FeatureTest.isLittleEndian?4278190080:255;for(u=0;uf?s:8*t-7,r=-8&a;let o=0,c=0;for(;n>=1}}for(;l=a){g=n;m=s*g}l=0;for(p=m;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*xt)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%xt,a=(i-n)/xt,r=0===n?a:a+1,o=t.createImageData(s,xt);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,h=l?Date.now()+15:0;let d=0;const c=this.commonObjs,u=this.objs;let p;for(;;){if(void 0!==s&&r===s.nextBreakPoint){s.breakIt(r,i);return r}p=a[r];if(p!==X.dependency)this[p].apply(this,n[r]);else for(const t of n[r]){const e=t.startsWith("g_")?c:u;if(!e.has(t)){e.get(t,i);return r}}r++;if(r===o)return r;if(l&&++d>10){if(Date.now()>h){i();return r}d=0}}}#hi(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#hi();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#di()}#di(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,d="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(d,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;d="prescale1"===d?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:s}=t,n=this.current.fillColor,a=this.current.patternFill,r=getCurrentTransform(e);let o,l,h,d;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;l=JSON.stringify(a?r:[r.slice(0,4),n]);o=this._cachedBitmapsMap.get(e);if(!o){o=new Map;this._cachedBitmapsMap.set(e,o)}const i=o.get(l);if(i&&!a){return{canvas:i,offsetX:Math.round(Math.min(r[0],r[2])+r[4]),offsetY:Math.round(Math.min(r[1],r[3])+r[5])}}h=i}if(!h){d=this.cachedCanvases.getCanvas("maskCanvas",i,s);putBinaryImageMask(d.context,t)}let c=Util.transform(r,[1/i,0,0,-1/s,0,0]);c=Util.transform(c,[1,0,0,1,0,-s]);const[u,p,g,m]=Util.getAxialAlignedBoundingBox([0,0,i,s],c),f=Math.round(g-u)||1,b=Math.round(m-p)||1,A=this.cachedCanvases.getCanvas("fillCanvas",f,b),w=A.context,v=u,y=p;w.translate(-v,-y);w.transform(...c);if(!h){h=this._scaleImage(d.canvas,getCurrentTransformInverse(w));h=h.img;o&&a&&o.set(l,h)}w.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(w),t.interpolate);drawImageAtIntegerCoords(w,h,0,0,h.width,h.height,0,0,i,s);w.globalCompositeOperation="source-in";const x=Util.transform(getCurrentTransformInverse(w),[1,0,0,1,-v,-y]);w.fillStyle=a?n.getPattern(e,this,x,bt):n;w.fillRect(0,0,i,s);if(o&&!a){this.cachedCanvases.delete("fillCanvas");o.set(l,A.canvas)}return{canvas:A.canvas,offsetX:Math.round(v),offsetY:Math.round(y)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=_t[t]}setLineJoin(t){this.ctx.lineJoin=Et[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.current.fillAlpha=i;this.ctx.globalAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;this.ctx=s.context;const n=this.ctx;n.setTransform(...getCurrentTransform(this.suspendedCtx));copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function ctxSave(){e.save();this.__originalSave()};t.restore=function ctxRestore(){e.restore();this.__originalRestore()};t.translate=function ctxTranslate(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function ctxScale(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function ctxTransform(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function ctxSetTransform(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function ctxResetTransform(){e.resetTransform();this.__originalResetTransform()};t.rotate=function ctxRotate(t){e.rotate(t);this.__originalRotate(t)};t.clip=function ctxRotate(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this.setGState([["BM","source-over"],["ca",1],["CA",1]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){this.genericComposeSMask(e.context,i,r,o,e.subtype,e.backdrop,e.transferMap,n,a,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation="source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,0,0);t.restore()}}genericComposeSMask(t,e,i,s,n,a,r,o,l,h,d){let c=t.canvas,u=o-h,p=l-d;if(a){const e=Util.makeHexColor(...a);if(u<0||p<0||u+i>c.width||p+s>c.height){const t=this.cachedCanvases.getCanvas("maskExtension",i,s),n=t.context;n.drawImage(c,-u,-p);n.globalCompositeOperation="destination-atop";n.fillStyle=e;n.fillRect(0,0,i,s);n.globalCompositeOperation="source-over";c=t.canvas;u=p=0}else{t.save();t.globalAlpha=1;t.setTransform(1,0,0,1,0,0);const n=new Path2D;n.rect(u,p,i,s);t.clip(n);t.globalCompositeOperation="destination-atop";t.fillStyle=e;t.fillRect(u,p,i,s);t.restore()}}e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);"Alpha"===n&&r?e.filter=this.filterFactory.addAlphaFilter(r):"Luminosity"===n&&(e.filter=this.filterFactory.addLuminosityFilter(r));const g=new Path2D;g.rect(o,l,i,s);e.clip(g);e.globalCompositeOperation="destination-in";e.drawImage(c,u,p,i,s,o,l,i,s);e.restore()}save(){if(this.inSMaskMode){copyCtxState(this.ctx,this.suspendedCtx);this.suspendedCtx.save()}else this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){0===this.stateStack.length&&this.inSMaskMode&&this.endSMaskMode();if(0!==this.stateStack.length){this.current=this.stateStack.pop();if(this.inSMaskMode){this.suspendedCtx.restore();copyCtxState(this.suspendedCtx,this.ctx)}else this.ctx.restore();this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){const s=this.ctx,n=this.current;let a,r,o=n.x,l=n.y;const h=getCurrentTransform(s),d=0===h[0]&&0===h[3]||0===h[1]&&0===h[2],c=d?i.slice(0):null;for(let i=0,u=0,p=t.length;i100&&(h=100);this.current.fontSizeScale=e/h;this.ctx.font=`${l} ${o} ${h}px ${r}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t,e,i,s,n,a){this.current.textMatrix=[t,e,i,s,n,a];this.current.textMatrixScale=Math.hypot(t,e);this.current.x=this.current.lineX=0;this.current.y=this.current.lineY=0}nextLine(){this.moveText(0,this.current.leading)}#ci(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n){const a=this.ctx,r=this.current,o=r.font,l=r.textRenderingMode,h=r.fontSize/r.fontSizeScale,d=l&y,c=!!(l&x),u=r.patternFill&&!o.missingFile,p=r.patternStroke&&!o.missingFile;let g;(o.disableFontFace||c||u||p)&&(g=o.getPathGenerator(this.commonObjs,t));if(o.disableFontFace||u||p){a.save();a.translate(e,i);a.scale(h,-h);if(d===b||d===w)if(s){const t=a.getTransform();a.setTransform(...s);a.fill(this.#ci(g,t,s))}else a.fill(g);if(d===A||d===w)if(n){const t=a.getTransform();a.setTransform(...n);a.stroke(this.#ci(g,t,n))}else{a.lineWidth/=h;a.stroke(g)}a.restore()}else{d!==b&&d!==w||a.fillText(t,e,i);d!==A&&d!==w||a.strokeText(t,e,i)}if(c){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:e,y:i,fontSize:h,path:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let i=!1;for(let t=3;t0&&e[t]<255){i=!0;break}return shadow(this,"isFontSubpixelAAEnabled",i)}showText(t){const e=this.current,i=e.font;if(i.isType3Font)return this.showType3Text(t);const s=e.fontSize;if(0===s)return;const n=this.ctx,a=e.fontSizeScale,r=e.charSpacing,o=e.wordSpacing,l=e.fontDirection,h=e.textHScale*l,d=t.length,c=i.vertical,u=c?1:-1,p=i.defaultVMetrics,g=s*e.fontMatrix[0],m=e.textRenderingMode===b&&!i.disableFontFace&&!e.patternFill;n.save();n.transform(...e.textMatrix);n.translate(e.x,e.y+e.textRise);l>0?n.scale(h,-1):n.scale(h,1);let f,v;if(e.patternFill){n.save();const t=e.fillColor.getPattern(n,this,getCurrentTransformInverse(n),bt);f=getCurrentTransform(n);n.restore();n.fillStyle=t}if(e.patternStroke){n.save();const t=e.strokeColor.getPattern(n,this,getCurrentTransformInverse(n),At);v=getCurrentTransform(n);n.restore();n.strokeStyle=t}let x=e.lineWidth;const _=e.textMatrixScale;if(0===_||0===x){const t=e.textRenderingMode&y;t!==A&&t!==w||(x=this.getSinglePixelWidth())}else x/=_;if(1!==a){n.scale(a,a);x/=a}n.lineWidth=x;if(i.isInvalidPDFjsFont){const i=[];let s=0;for(const e of t){i.push(e.unicode);s+=e.width}n.fillText(i.join(""),0,0);e.x+=s*g*h;n.restore();this.compose();return}let E,S=0;for(E=0;E0){const t=1e3*n.measureText(b).width/s*a;if(xnew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new TilingPattern(t,i,this.ctx,n,s)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments);this.current.patternStroke=!0}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t,e,i){this.ctx.strokeStyle=this.current.strokeColor=Util.makeHexColor(t,e,i);this.current.patternStroke=!1}setStrokeTransparent(){this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t,e,i){this.ctx.fillStyle=this.current.fillColor=Util.makeHexColor(t,e,i);this.current.patternFill=!1}setFillTransparent(){this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1}_getPattern(t,e=null){let i;if(this.cachedPatterns.has(t))i=this.cachedPatterns.get(t);else{i=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t));this.cachedPatterns.set(t,i)}e&&(i.matrix=e);return i}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const i=this._getPattern(t);e.fillStyle=i.getPattern(e,this,getCurrentTransformInverse(e),wt);const s=getCurrentTransformInverse(e);if(s){const{width:t,height:i}=e.canvas,[n,a,r,o]=Util.getAxialAlignedBoundingBox([0,0,t,i],s);this.ctx.fillRect(n,a,r-n,o-a)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);t&&this.transform(...t);this.baseTransform=getCurrentTransform(this.ctx);if(e){const t=e[2]-e[0],i=e[3]-e[1];this.ctx.rect(e[0],e[1],t,i);this.current.updateRectMinMax(getCurrentTransform(this.ctx),e);this.clip();this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||info("TODO: Support non-isolated groups.");t.knockout&&warn("Knockout groups not supported.");const i=getCurrentTransform(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let s=Util.getAxialAlignedBoundingBox(t.bbox,getCurrentTransform(e));const n=[0,0,e.canvas.width,e.canvas.height];s=Util.intersect(s,n)||[0,0,0,0];const a=Math.floor(s[0]),r=Math.floor(s[1]),o=Math.max(Math.ceil(s[2])-a,1),l=Math.max(Math.ceil(s[3])-r,1);this.current.startNewPathAndClipBox([0,0,o,l]);let h="groupAt"+this.groupLevel;t.smask&&(h+="_smask_"+this.smaskCounter++%2);const d=this.cachedCanvases.getCanvas(h,o,l),c=d.context;c.translate(-a,-r);c.transform(...i);if(t.smask)this.smaskStack.push({canvas:d.canvas,context:c,offsetX:a,offsetY:r,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(a,r);e.save()}copyCtxState(e,c);this.ctx=c;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,i=this.groupStack.pop();this.ctx=i;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=getCurrentTransform(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const i=Util.getAxialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(i)}}beginAnnotation(t,e,i,s,n){this.#hi();resetCtxToDefault(this.ctx);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(e){const s=e[2]-e[0],a=e[3]-e[1];if(n&&this.annotationCanvasMap){(i=i.slice())[4]-=e[0];i[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=s;e[3]=a;const[n,r]=Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx)),{viewportScale:o}=this,l=Math.ceil(s*this.outputScaleX*o),h=Math.ceil(a*this.outputScaleY*o);this.annotationCanvas=this.canvasFactory.create(l,h);const{canvas:d,context:c}=this.annotationCanvas;this.annotationCanvasMap.set(t,d);this.annotationCanvas.savedCtx=this.ctx;this.ctx=c;this.ctx.save();this.ctx.setTransform(n,0,0,-r,0,a*r);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath();this.ctx.rect(e[0],e[1],s,a);this.ctx.clip();this.ctx.beginPath()}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...i);this.transform(...s)}endAnnotation(){if(this.annotationCanvas){this.ctx.restore();this.#di();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const i=this.ctx,s=this.processingType3;if(s){void 0===s.compiled&&(s.compiled=function compileType3Glyph(t){const{width:e,height:i}=t;if(e>1e3||i>1e3)return null;const s=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),n=e+1;let a,r,o,l=new Uint8Array(n*(i+1));const h=e+7&-8;let d=new Uint8Array(h*i),c=0;for(const e of t.data){let t=128;for(;t>0;){d[c++]=e&t?0:255;t>>=1}}let u=0;c=0;if(0!==d[c]){l[0]=1;++u}for(r=1;r>2)+(d[c+1]?4:0)+(d[c-h+1]?8:0);if(s[t]){l[o+r]=s[t];++u}c++}if(d[c-h]!==d[c]){l[o+r]=d[c]?2:4;++u}if(u>1e3)return null}c=h*(i-1);o=a*n;if(0!==d[c]){l[o]=8;++u}for(r=1;r1e3)return null;const p=new Int32Array([0,n,-1,0,-n,0,0,0,1]),g=new Path2D;for(a=0;u&&a<=i;a++){let t=a*n;const i=t+e;for(;t>4;l[t]&=r>>2|r<<2}g.lineTo(t%n,t/n|0);l[t]||--u}while(s!==t);--a}d=null;l=null;return function(t){t.save();t.scale(1/e,-1/i);t.translate(0,-i);t.fill(g);t.beginPath();t.restore()}}(t));if(s.compiled){s.compiled(i);return}}const n=this._createMaskCanvas(t),a=n.canvas;i.save();i.setTransform(1,0,0,1,0,0);i.drawImage(a,n.offsetX,n.offsetY);i.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,i=0,s=0,n,a){if(!this.contentVisible)return;t=this.getObject(t.data,t);const r=this.ctx;r.save();const o=getCurrentTransform(r);r.transform(e,i,s,n,0,0);const l=this._createMaskCanvas(t);r.setTransform(1,0,0,1,l.offsetX-o[4],l.offsetY-o[5]);for(let t=0,h=a.length;te?h/e:1;r=l>e?l/e:1}}this._cachedScaleForStroking[0]=a;this._cachedScaleForStroking[1]=r}return this._cachedScaleForStroking}rescaleAndStroke(t){const{ctx:e}=this,{lineWidth:i}=this.current,[s,n]=this.getScaleForStroking();e.lineWidth=i||1;if(1===s&&1===n){e.stroke();return}const a=e.getLineDash();t&&e.save();e.scale(s,n);if(a.length>0){const t=Math.max(s,n);e.setLineDash(a.map((e=>e/t)));e.lineDashOffset/=t}e.stroke();t&&e.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in X)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[X[t]]=CanvasGraphics.prototype[t]);class GlobalWorkerOptions{static#ui=null;static#pi="";static get workerPort(){return this.#ui}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#ui=t}static get workerSrc(){return this.#pi}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#pi=t}}class Metadata{#gi;#mi;constructor({parsedData:t,rawData:e}){this.#gi=t;this.#mi=e}getRaw(){return this.#mi}get(t){return this.#gi.get(t)??null}getAll(){return objectFromMap(this.#gi)}has(t){return this.#gi.has(t)}}const Tt=Symbol("INTERNAL");class OptionalContentGroup{#fi=!1;#bi=!1;#Ai=!1;#wi=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:n}){this.#fi=!!(t&r);this.#bi=!!(t&o);this.name=e;this.intent=i;this.usage=s;this.rbGroups=n}get visible(){if(this.#Ai)return this.#wi;if(!this.#wi)return!1;const{print:t,view:e}=this.usage;return this.#fi?"OFF"!==e?.viewState:!this.#bi||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==Tt&&unreachable("Internal method `_setVisible` called.");this.#Ai=i;this.#wi=e}}class OptionalContentConfig{#vi=null;#yi=new Map;#xi=null;#_i=null;constructor(t,e=r){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#_i=t.order;for(const i of t.groups)this.#yi.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#yi.values())t._setVisible(Tt,!1);for(const e of t.on)this.#yi.get(e)._setVisible(Tt,!0);for(const e of t.off)this.#yi.get(e)._setVisible(Tt,!1);this.#xi=this.getHash()}}#Ei(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;s0?objectFromMap(this.#yi):null}getGroup(t){return this.#yi.get(t)||null}getHash(){if(null!==this.#vi)return this.#vi;const t=new MurmurHash3_64;for(const[e,i]of this.#yi)t.update(`${e}:${i.visible}`);return this.#vi=t.hexdigest()}}class PDFDataTransportStream{constructor(t,{disableRange:e=!1,disableStream:i=!1}){assert(t,'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.');const{length:s,initialData:n,progressiveDone:a,contentDispositionFilename:r}=t;this._queuedChunks=[];this._progressiveDone=a;this._contentDispositionFilename=r;if(n?.length>0){const t=n instanceof Uint8Array&&n.byteLength===n.buffer.byteLength?n.buffer:new Uint8Array(n).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=t;this._isStreamingSupported=!i;this._isRangeSupported=!e;this._contentLength=s;this._fullRequestReader=null;this._rangeReaders=[];t.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));t.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));t.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));t.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));t.transportReady()}_onReceiveData({begin:t,chunk:e}){const i=e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer;if(void 0===t)this._fullRequestReader?this._fullRequestReader._enqueue(i):this._queuedChunks.push(i);else{assert(this._rangeReaders.some((function(e){if(e._begin!==t)return!1;e._enqueue(i);return!0})),"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader{constructor(t,e,i=!1,s=null){this._stream=t;this._done=i||!1;this._filename=isPdfFile(s)?s:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,i){this._stream=t;this._begin=e;this._end=i;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){try{return new URL(t).origin}catch{}return null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(a))return n;n.suggestedLength=a;if(a<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;if("identity"!==(t.get("Content-Encoding")||"identity"))return n;n.allowRangeRequests=!0;return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(e.url);if(!validateResponseStatus(e.status))throw createResponseStatusError(e.status,s);this._reader=e.body.getReader();this._headersCapability.resolve();const i=e.headers,{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:i,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=n;this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(i);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,i){this._stream=t;this._reader=null;this._loaded=0;const s=t.source;this._withCredentials=s.withCredentials||!1;this._readCapability=Promise.withResolvers();this._isStreamingSupported=!s.disableStream;this._abortController=new AbortController;const n=new Headers(t.headers);n.append("Range",`bytes=${e}-${i-1}`);const a=s.url;fetch(a,createFetchOptions(n,this._withCredentials,this._abortController)).then((e=>{const i=getResponseOrigin(e.url);if(i!==t._responseOrigin)throw new Error(`Expected range response-origin "${i}" to match "${t._responseOrigin}".`);if(!validateResponseStatus(e.status))throw createResponseStatusError(e.status,a);this._readCapability.resolve();this._reader=e.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class NetworkManager{_responseOrigin=null;constructor({url:t,httpHeaders:e,withCredentials:i}){this.url=t;this.isHttp=/^https?:/i.test(t);this.headers=createHeaders(this.isHttp,e);this.withCredentials=i||!1;this.currXhrId=0;this.pendingRequests=Object.create(null)}request(t){const e=new XMLHttpRequest,i=this.currXhrId++,s=this.pendingRequests[i]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);s.expectedStatus=206}else s.expectedStatus=200;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>{t.onError(e.status)};e.onreadystatechange=this.onStateChange.bind(this,i);e.onprogress=this.onProgress.bind(this,i);s.onHeadersReceived=t.onHeadersReceived;s.onDone=t.onDone;s.onError=t.onError;s.onProgress=t.onProgress;e.send(null);return i}onProgress(t,e){const i=this.pendingRequests[t];i&&i.onProgress?.(e)}onStateChange(t,e){const i=this.pendingRequests[t];if(!i)return;const s=i.xhr;if(s.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==s.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===s.status&&this.isHttp){i.onError(s.status);return}const n=s.status||200;if(!(200===n&&206===i.expectedStatus)&&n!==i.expectedStatus){i.onError(s.status);return}const a=function network_getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:stringToBytes(e).buffer}(s);if(206===n){const t=s.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);if(e)i.onDone({begin:parseInt(e[1],10),chunk:a});else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else a?i.onDone({begin:0,chunk:a}):i.onError(s.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t);this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const i=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);i.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;this._url=e.url;this._fullRequestId=t.request({onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._headersCapability=Promise.withResolvers();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t);this._manager._responseOrigin=getResponseOrigin(e.responseURL);const i=e.getAllResponseHeaders(),s=new Headers(i?i.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map((t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]})):[]),{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:s,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});n&&(this._isRangeSupported=!0);this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(s);this._isRangeSupported&&this._manager.abortRequest(t);this._headersCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=createResponseStatusError(t,this._url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersCapability.promise}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,i){this._manager=t;this._url=t.url;this._requestId=t.request({begin:e,end:i,onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_onHeadersReceived(){const t=getResponseOrigin(this._manager.getRequestXhr(this._requestId)?.responseURL);if(t!==this._manager._responseOrigin){this._storedError=new Error(`Expected range response-origin "${t}" to match "${this._manager._responseOrigin}".`);this._onError(0)}}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError??=createResponseStatusError(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}const Mt=/^[a-z][a-z0-9\-+.]+:/i;class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrlOrPath(t){if(Mt.test(t))return new URL(t);const e=process.getBuiltinModule("url");return new URL(e.pathToFileURL(t))}(t.url);assert("file:"===this.url.protocol,"PDFNodeStream only supports file:// URLs.");this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){assert(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNodeStreamFsFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFNodeStreamFsRangeReader(this,t,e);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNodeStreamFsFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=Promise.withResolvers();this._headersCapability=Promise.withResolvers();const i=process.getBuiltinModule("fs");i.promises.lstat(this._url).then((t=>{this._contentLength=t.size;this._setReadableStream(i.createReadStream(this._url));this._headersCapability.resolve()}),(t=>{"ENOENT"===t.code&&(t=new MissingPDFException(`Missing PDF "${this._url}".`));this._storedError=t;this._headersCapability.reject(t)}))}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class PDFNodeStreamFsRangeReader{constructor(t,e,i){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=Promise.withResolvers();const s=t.source;this._isStreamingSupported=!s.disableStream;const n=process.getBuiltinModule("fs");this._setReadableStream(n.createReadStream(this._url,{start:e,end:i-1}))}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}const Pt=30;class TextLayer{#Si=Promise.withResolvers();#pt=null;#Ci=!1;#Ti=!!globalThis.FontInspector?.enabled;#Mi=null;#Pi=null;#Di=0;#ki=0;#Ri=null;#Ii=null;#Fi=0;#Li=0;#Oi=Object.create(null);#Ni=[];#Bi=null;#Hi=[];#zi=new WeakMap;#Ui=null;static#Gi=new Map;static#$i=new Map;static#Vi=new WeakMap;static#ji=null;static#Wi=new Set;constructor({textContentSource:t,container:e,viewport:i}){if(t instanceof ReadableStream)this.#Bi=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#Bi=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#pt=this.#Ii=e;this.#Li=i.scale*(globalThis.devicePixelRatio||1);this.#Fi=i.rotation;this.#Pi={div:null,properties:null,ctx:null};const{pageWidth:s,pageHeight:n,pageX:a,pageY:r}=i.rawDims;this.#Ui=[1,0,0,-1,-a,r+n];this.#ki=s;this.#Di=n;TextLayer.#qi();setLayerDimensions(e,i);this.#Si.promise.finally((()=>{TextLayer.#Wi.delete(this);this.#Pi=null;this.#Oi=null})).catch((()=>{}))}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=util_FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){const pump=()=>{this.#Ri.read().then((({value:t,done:e})=>{if(e)this.#Si.resolve();else{this.#Mi??=t.lang;Object.assign(this.#Oi,t.styles);this.#Xi(t.items);pump()}}),this.#Si.reject)};this.#Ri=this.#Bi.getReader();TextLayer.#Wi.add(this);pump();return this.#Si.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*(globalThis.devicePixelRatio||1),s=t.rotation;if(s!==this.#Fi){e?.();this.#Fi=s;setLayerDimensions(this.#Ii,{rotation:s})}if(i!==this.#Li){e?.();this.#Li=i;const t={div:null,properties:null,ctx:TextLayer.#Ki(this.#Mi)};for(const e of this.#Hi){t.properties=this.#zi.get(e);t.div=e;this.#Yi(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#Ri?.cancel(t).catch((()=>{}));this.#Ri=null;this.#Si.reject(t)}get textDivs(){return this.#Hi}get textContentItemsStr(){return this.#Ni}#Xi(t){if(this.#Ci)return;this.#Pi.ctx??=TextLayer.#Ki(this.#Mi);const e=this.#Hi,i=this.#Ni;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#Ci=!0;return}if(void 0!==s.str){i.push(s.str);this.#Qi(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#pt;this.#pt=document.createElement("span");this.#pt.classList.add("markedContent");null!==s.id&&this.#pt.setAttribute("id",`${s.id}`);t.append(this.#pt)}else"endMarkedContent"===s.type&&(this.#pt=this.#pt.parentNode)}}#Qi(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#Hi.push(e);const s=Util.transform(this.#Ui,t.transform);let n=Math.atan2(s[1],s[0]);const a=this.#Oi[t.fontName];a.vertical&&(n+=Math.PI/2);let r=this.#Ti&&a.fontSubstitution||a.fontFamily;r=TextLayer.fontFamilyMap.get(r)||r;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#Ji(r,this.#Mi);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c="calc(var(--scale-factor)*",u=e.style;if(this.#pt===this.#Ii){u.left=`${(100*h/this.#ki).toFixed(2)}%`;u.top=`${(100*d/this.#Di).toFixed(2)}%`}else{u.left=`${c}${h.toFixed(2)}px)`;u.top=`${c}${d.toFixed(2)}px)`}u.fontSize=`${c}${(TextLayer.#ji*o).toFixed(2)}px)`;u.fontFamily=r;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#Ti&&(e.dataset.fontName=a.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let p=!1;if(t.str.length>1)p=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(p=!0)}p&&(i.canvasWidth=a.vertical?t.height:t.width);this.#zi.set(e,i);this.#Pi.div=e;this.#Pi.properties=i;this.#Yi(this.#Pi);i.hasText&&this.#pt.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#pt.append(t)}}#Yi(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;let a="";TextLayer.#ji>1&&(a=`scale(${1/TextLayer.#ji})`);if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:o}=i;TextLayer.#Zi(s,o*this.#Li,t);const{width:l}=s.measureText(e.textContent);l>0&&(a=`scaleX(${r*this.#Li/l}) ${a}`)}0!==i.angle&&(a=`rotate(${i.angle}deg) ${a}`);a.length>0&&(n.transform=a)}static cleanup(){if(!(this.#Wi.size>0)){this.#Gi.clear();for(const{canvas:t}of this.#$i.values())t.remove();this.#$i.clear()}}static#Ki(t=null){let e=this.#$i.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#$i.set(t,e);this.#Vi.set(e,{size:0,family:""})}return e}static#Zi(t,e,i){const s=this.#Vi.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#qi(){if(null!==this.#ji)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#ji=t.getBoundingClientRect().height;t.remove()}static#Ji(t,e){const i=this.#Gi.get(t);if(i)return i;const s=this.#Ki(e);s.canvas.width=s.canvas.height=Pt;this.#Zi(s,Pt,t);const n=s.measureText("");let a=n.fontBoundingBoxAscent,r=Math.abs(n.fontBoundingBoxDescent);if(a){const e=a/(a+r);this.#Gi.set(t,e);s.canvas.width=s.canvas.height=0;return e}s.strokeStyle="red";s.clearRect(0,0,Pt,Pt);s.strokeText("g",0,0);let o=s.getImageData(0,0,Pt,Pt).data;r=0;for(let t=o.length-1-3;t>=0;t-=4)if(o[t]>0){r=Math.ceil(t/4/Pt);break}s.clearRect(0,0,Pt,Pt);s.strokeText("A",0,Pt);o=s.getImageData(0,0,Pt,Pt).data;a=0;for(let t=0,e=o.length;t0){a=Pt-Math.floor(t/4/Pt);break}s.canvas.width=s.canvas.height=0;const l=a?a/(a+r):.8;this.#Gi.set(t,l);return l}}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}const Dt=65536,kt=e?class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}:class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}},Rt=e?class NodeCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){return node_utils_fetchData(t)}}:DOMCMapReaderFactory,It=e?class NodeFilterFactory extends BaseFilterFactory{}:class DOMFilterFactory extends BaseFilterFactory{#ts;#es;#is;#ss;#ns;#as;#w=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#ss=t;this.#ns=e}get#y(){return this.#es||=new Map}get#rs(){return this.#as||=new Map}get#os(){if(!this.#is){const t=this.#ns.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#ns.createElementNS(it,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#is=this.#ns.createElementNS(it,"defs");t.append(i);i.append(this.#is);this.#ns.body.append(t)}return this.#is}#ls(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),a=new Array(256),r=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;a[t]=i[t]/255;r[t]=s[t]/255}return[n.join(","),a.join(","),r.join(",")]}#hs(t){if(void 0===this.#ts){this.#ts="";const t=this.#ns.URL;t!==this.#ns.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#ts=t.split("#",1)[0])}return`url(${this.#ts}#${t})`}addFilter(t){if(!t)return"none";let e=this.#y.get(t);if(e)return e;const[i,s,n]=this.#ls(t),a=1===t.length?i:`${i}${s}${n}`;e=this.#y.get(a);if(e){this.#y.set(t,e);return e}const r=`g_${this.#ss}_transfer_map_${this.#w++}`,o=this.#hs(r);this.#y.set(t,o);this.#y.set(a,o);const l=this.#ds(r);this.#cs(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#rs.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#rs.set(s,n)}if(!t||!e)return n.url;const a=this.#us(t);t=Util.makeHexColor(...a);const r=this.#us(e);e=Util.makeHexColor(...r);this.#os.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#ss}_hcm_filter`,d=n.filter=this.#ds(h);this.#cs(l,l,l,d);this.#ps(d);const getSteps=(t,e)=>{const i=a[t]/255,s=r[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#cs(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#hs(h);return n.url}addAlphaFilter(t){let e=this.#y.get(t);if(e)return e;const[i]=this.#ls([t]),s=`alpha_${i}`;e=this.#y.get(s);if(e){this.#y.set(t,e);return e}const n=`g_${this.#ss}_alpha_map_${this.#w++}`,a=this.#hs(n);this.#y.set(t,a);this.#y.set(s,a);const r=this.#ds(n);this.#gs(i,r);return a}addLuminosityFilter(t){let e,i,s=this.#y.get(t||"luminosity");if(s)return s;if(t){[e]=this.#ls([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#y.get(i);if(s){this.#y.set(t,s);return s}const n=`g_${this.#ss}_luminosity_map_${this.#w++}`,a=this.#hs(n);this.#y.set(t,a);this.#y.set(i,a);const r=this.#ds(n);this.#ms(r);t&&this.#gs(e,r);return a}addHighlightHCMFilter(t,e,i,s,n){const a=`${e}-${i}-${s}-${n}`;let r=this.#rs.get(t);if(r?.key===a)return r.url;if(r){r.filter?.remove();r.key=a;r.url="none";r.filter=null}else{r={key:a,url:"none",filter:null};this.#rs.set(t,r)}if(!e||!i)return r.url;const[o,l]=[e,i].map(this.#us.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#us.bind(this));d{const s=new Array(256),n=(d-h)/i,a=t/255,r=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=a+t*r;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#ss}_hcm_${t}_filter`,g=r.filter=this.#ds(p);this.#ps(g);this.#cs(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);r.url=this.#hs(p);return r.url}destroy(t=!1){if(!t||!this.#as?.size){this.#is?.parentNode.parentNode.remove();this.#is=null;this.#es?.clear();this.#es=null;this.#as?.clear();this.#as=null;this.#w=0}}#ms(t){const e=this.#ns.createElementNS(it,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#ps(t){const e=this.#ns.createElementNS(it,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#ds(t){const e=this.#ns.createElementNS(it,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#os.append(e);return e}#fs(t,e,i){const s=this.#ns.createElementNS(it,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#cs(t,e,i,s){const n=this.#ns.createElementNS(it,"feComponentTransfer");s.append(n);this.#fs(n,"feFuncR",t);this.#fs(n,"feFuncG",e);this.#fs(n,"feFuncB",i)}#gs(t,e){const i=this.#ns.createElementNS(it,"feComponentTransfer");e.append(i);this.#fs(i,"feFuncA",t)}#us(t){this.#os.style.color=t;return getRGB(getComputedStyle(this.#os).getPropertyValue("color"))}},Ft=e?class NodeStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){return node_utils_fetchData(t)}}:DOMStandardFontDataFactory;function getDocument(t={}){"string"==typeof t||t instanceof URL?t={url:t}:(t instanceof ArrayBuffer||ArrayBuffer.isView(t))&&(t={data:t});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=t.url?function getUrlProp(t){if(t instanceof URL)return t.href;try{return new URL(t,window.location).href}catch{if(e&&"string"==typeof t)return t}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(t.url):null,a=t.data?function getDataProp(t){if(e&&"undefined"!=typeof Buffer&&t instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(t instanceof Uint8Array&&t.byteLength===t.buffer.byteLength)return t;if("string"==typeof t)return stringToBytes(t);if(t instanceof ArrayBuffer||ArrayBuffer.isView(t)||"object"==typeof t&&!isNaN(t?.length))return new Uint8Array(t);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(t.data):null,r=t.httpHeaders||null,o=!0===t.withCredentials,l=t.password??null,h=t.range instanceof PDFDataRangeTransport?t.range:null,d=Number.isInteger(t.rangeChunkSize)&&t.rangeChunkSize>0?t.rangeChunkSize:Dt;let c=t.worker instanceof PDFWorker?t.worker:null;const u=t.verbosity,p="string"!=typeof t.docBaseUrl||isDataScheme(t.docBaseUrl)?null:t.docBaseUrl,g="string"==typeof t.cMapUrl?t.cMapUrl:null,m=!1!==t.cMapPacked,f=t.CMapReaderFactory||Rt,b="string"==typeof t.standardFontDataUrl?t.standardFontDataUrl:null,A=t.StandardFontDataFactory||Ft,w=!0!==t.stopAtErrors,v=Number.isInteger(t.maxImageSize)&&t.maxImageSize>-1?t.maxImageSize:-1,y=!1!==t.isEvalSupported,x="boolean"==typeof t.isOffscreenCanvasSupported?t.isOffscreenCanvasSupported:!e,_="boolean"==typeof t.isImageDecoderSupported?t.isImageDecoderSupported:!e&&(util_FeatureTest.platform.isFirefox||!globalThis.chrome),E=Number.isInteger(t.canvasMaxAreaInBytes)?t.canvasMaxAreaInBytes:-1,S="boolean"==typeof t.disableFontFace?t.disableFontFace:e,C=!0===t.fontExtraProperties,T=!0===t.enableXfa,M=t.ownerDocument||globalThis.document,P=!0===t.disableRange,D=!0===t.disableStream,k=!0===t.disableAutoFetch,R=!0===t.pdfBug,I=t.CanvasFactory||kt,F=t.FilterFactory||It,L=!0===t.enableHWA,O=h?h.length:t.length??NaN,N="boolean"==typeof t.useSystemFonts?t.useSystemFonts:!e&&!S,B="boolean"==typeof t.useWorkerFetch?t.useWorkerFetch:f===DOMCMapReaderFactory&&A===DOMStandardFontDataFactory&&g&&b&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(b,document.baseURI);setVerbosityLevel(u);const H={canvasFactory:new I({ownerDocument:M,enableHWA:L}),filterFactory:new F({docId:s,ownerDocument:M}),cMapReaderFactory:B?null:new f({baseUrl:g,isCompressed:m}),standardFontDataFactory:B?null:new A({baseUrl:b})};if(!c){const t={verbosity:u,port:GlobalWorkerOptions.workerPort};c=t.port?PDFWorker.fromPort(t):new PDFWorker(t);i._worker=c}const z={docId:s,apiVersion:"4.10.38",data:a,password:l,disableAutoFetch:k,rangeChunkSize:d,length:O,docBaseUrl:p,enableXfa:T,evaluatorOptions:{maxImageSize:v,disableFontFace:S,ignoreErrors:w,isEvalSupported:y,isOffscreenCanvasSupported:x,isImageDecoderSupported:_,canvasMaxAreaInBytes:E,fontExtraProperties:C,useSystemFonts:N,cMapUrl:B?g:null,standardFontDataUrl:B?b:null}},U={disableFontFace:S,fontExtraProperties:C,ownerDocument:M,pdfBug:R,styleElement:null,loadingParams:{disableAutoFetch:k,enableXfa:T}};c.promise.then((function(){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const t=c.messageHandler.sendWithPromise("GetDocRequest",z,a?[a.buffer]:null);let l;if(h)l=new PDFDataTransportStream(h,{disableRange:P,disableStream:D});else if(!a){if(!n)throw new Error("getDocument - no `url` parameter provided.");let t;if(e)if(isValidFetchUrl(n)){if("undefined"==typeof fetch||"undefined"==typeof Response||!("body"in Response.prototype))throw new Error("getDocument - the Fetch API was disabled in Node.js, see `--no-experimental-fetch`.");t=PDFFetchStream}else t=PDFNodeStream;else t=isValidFetchUrl(n)?PDFFetchStream:PDFNetworkStream;l=new t({url:n,length:O,httpHeaders:r,withCredentials:o,rangeChunkSize:d,disableRange:P,disableStream:D})}return t.then((t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,l,U,H);i._transport=n;e.send("Ready",null)}))})).catch(i._capability.reject);return i}function isRefProxy(t){return"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0}class PDFDocumentLoadingTask{static#ss=0;constructor(){this._capability=Promise.withResolvers();this._transport=null;this._worker=null;this.docId="d"+PDFDocumentLoadingTask.#ss++;this.destroyed=!1;this.onPassword=null;this.onProgress=null}get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}}class PDFDataRangeTransport{constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s;this._rangeListeners=[];this._progressListeners=[];this._progressiveReadListeners=[];this._progressiveDoneListeners=[];this._readyCapability=Promise.withResolvers()}addRangeListener(t){this._rangeListeners.push(t)}addProgressListener(t){this._progressListeners.push(t)}addProgressiveReadListener(t){this._progressiveReadListeners.push(t)}addProgressiveDoneListener(t){this._progressiveDoneListeners.push(t)}onDataRange(t,e){for(const i of this._rangeListeners)i(t,e)}onDataProgress(t,e){this._readyCapability.promise.then((()=>{for(const i of this._progressListeners)i(t,e)}))}onDataProgressiveRead(t){this._readyCapability.promise.then((()=>{for(const e of this._progressiveReadListeners)e(t)}))}onDataProgressiveDone(){this._readyCapability.promise.then((()=>{for(const t of this._progressiveDoneListeners)t()}))}transportReady(){this._readyCapability.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#bs=null;#As=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._maybeCleanupAfterRender=!1;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=p.ENABLE,transform:n=null,background:a=null,optionalContentConfigPromise:r=null,annotationCanvasMap:l=null,pageColors:h=null,printAnnotationStorage:d=null,isEditing:c=!1}){this._stats?.time("Overall");const u=this._transport.getRenderingIntent(i,s,d,c),{renderingIntent:g,cacheKey:m}=u;this.#As=!1;this.#ws();r||=this._transport.getOptionalContentConfig(g);let f=this._intentStates.get(m);if(!f){f=Object.create(null);this._intentStates.set(m,f)}if(f.streamReaderCancelTimeout){clearTimeout(f.streamReaderCancelTimeout);f.streamReaderCancelTimeout=null}const b=!!(g&o);if(!f.displayReadyCapability){f.displayReadyCapability=Promise.withResolvers();f.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(u)}const complete=t=>{f.renderTasks.delete(A);(this._maybeCleanupAfterRender||b)&&(this.#As=!0);this.#vs(!b);if(t){A.capability.reject(t);this._abortOperatorList({intentState:f,reason:t instanceof Error?t:new Error(t)})}else A.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}},A=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:a},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:f.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!b,pdfBug:this._pdfBug,pageColors:h});(f.renderTasks||=new Set).add(A);const w=A.task;Promise.all([f.displayReadyCapability.promise,r]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&g))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");A.initializeGraphics({transparency:t,optionalContentConfig:e});A.operatorListChanged()}})).catch(complete);return w}getOperatorList({intent:t="display",annotationMode:e=p.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0);let a,r=this._intentStates.get(n.cacheKey);if(!r){r=Object.create(null);this._intentStates.set(n.cacheKey,r)}if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null),lang:null};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{n.lang??=e.lang;Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#As=!1;this.#ws();return Promise.all(t)}cleanup(t=!1){this.#As=!0;const e=this.#vs(!1);t&&e&&(this._stats&&=new StatTimer);return e}#vs(t=!1){this.#ws();if(!this.#As||this.destroyed)return!1;if(t){this.#bs=setTimeout((()=>{this.#bs=null;this.#vs(!1)}),5e3);return!1}for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#As=!1;return!0}#ws(){if(this.#bs){clearTimeout(this.#bs);this.#bs=null}}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{r.read().then((({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}}),(t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#vs(!0)}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class LoopbackPort{#ys=new Map;#xs=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#xs.then((()=>{for(const[t]of this.#ys)t.call(this,i)}))}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#ys.set(e,s)}removeEventListener(t,e){const i=this.#ys.get(e);i?.();this.#ys.delete(e)}terminate(){for(const[,t]of this.#ys)t?.();this.#ys.clear()}}class PDFWorker{static#_s=0;static#Es=!1;static#Ss;static{if(e){this.#Es=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{let i;try{i=new URL(t);if(!i.origin||"null"===i.origin)return!1}catch{return!1}const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;this._readyCapability=Promise.withResolvers();this._port=null;this._webWorker=null;this._messageHandler=null;if(e){if(PDFWorker.#Ss?.has(e))throw new Error("Cannot use more than one PDFWorker per port.");(PDFWorker.#Ss||=new WeakMap).set(e,this);this._initializeFromPort(e)}else this._initialize()}get promise(){return this._readyCapability.promise}#Cs(){this._readyCapability.resolve();this._messageHandler.send("configure",{verbosity:this.verbosity})}get port(){return this._port}get messageHandler(){return this._messageHandler}_initializeFromPort(t){this._port=t;this._messageHandler=new MessageHandler("main","worker",t);this._messageHandler.on("ready",(function(){}));this.#Cs()}_initialize(){if(PDFWorker.#Es||PDFWorker.#Ts){this._setupFakeWorker();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location.href,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this._readyCapability.reject(new Error("Worker was destroyed")):this._setupFakeWorker()},s=new AbortController;e.addEventListener("error",(()=>{this._webWorker||terminateEarly()}),{signal:s.signal});i.on("test",(t=>{s.abort();if(!this.destroyed&&t){this._messageHandler=i;this._port=e;this._webWorker=e;this.#Cs()}else terminateEarly()}));i.on("ready",(t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this._setupFakeWorker()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this._setupFakeWorker()}_setupFakeWorker(){if(!PDFWorker.#Es){warn("Setting up fake worker.");PDFWorker.#Es=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this._readyCapability.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this._port=e;const i="fake"+PDFWorker.#_s++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this._messageHandler=new MessageHandler(i,i+"_worker",e);this.#Cs()})).catch((t=>{this._readyCapability.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;this._webWorker?.terminate();this._webWorker=null;PDFWorker.#Ss?.delete(this._port);this._port=null;this._messageHandler?.destroy();this._messageHandler=null}static fromPort(t){if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");const e=this.#Ss?.get(t.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.fromPort - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#Ts(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#Ts)return this.#Ts;return(await import(this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{#Ms=new Map;#Ps=new Map;#Ds=new Map;#ks=new Map;#Rs=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=Promise.withResolvers();this.setupMessageHandler()}#Is(t,e=null){const i=this.#Ms.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#Ms.set(t,s);return s}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=p.ENABLE,i=null,s=!1,n=!1){let g=r,m=rt;switch(t){case"any":g=a;break;case"display":break;case"print":g=o;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const f=g&o&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case p.DISABLE:g+=d;break;case p.ENABLE:break;case p.ENABLE_FORMS:g+=l;break;case p.ENABLE_STORAGE:g+=h;m=f.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}s&&(g+=c);n&&(g+=u);const{ids:b,hash:A}=f.modifiedIds;return{renderingIntent:g,cacheKey:[g,m.hash,A].join("_"),annotationStorageSerializable:m,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#Rs?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#Ps.values())t.push(e._destroy());this.#Ps.clear();this.#Ds.clear();this.#ks.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#Ms.clear();this.filterFactory.destroy();TextLayer.cleanup();this._networkStream?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{assert(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(async t=>{await this._fullReader.headersReady;const{isStreamingSupported:i,isRangeSupported:s,contentLength:n}=this._fullReader;if(!i||!s){this._lastProgress&&e.onProgress?.(this._lastProgress);this._fullReader.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}return{isStreamingSupported:i,isRangeSupported:s,contentLength:n}}));t.on("GetRangeReader",((t,e)=>{assert(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(t=>{e._capability.reject(wrapReason(t))}));t.on("PasswordRequest",(t=>{this.#Rs=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#Rs.reject(t):this.#Rs.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#Rs.reject(t)}return this.#Rs.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#Ps.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":const{disableFontFace:n,fontExtraProperties:a,pdfBug:r}=this._params;if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const o=r&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,l=new FontFaceObject(s,{disableFontFace:n,fontExtraProperties:a,inspectFont:o});this.fontLoader.bind(l).catch((()=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!a&&l.data&&(l.data=null);this.commonObjs.resolve(e,l)}));break;case"CopyLocalImage":const{imageRef:h}=s;assert(h,"The imageRef must be defined.");for(const t of this.#Ps.values())for(const[,i]of t.objs)if(i?.ref===h){if(!i.dataLen)return null;this.commonObjs.resolve(e,structuredClone(i));return i.dataLen}break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}return null}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#Ps.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":n.objs.resolve(t,s);s?.dataLen>1e7&&(n._maybeCleanupAfterRender=!0);break;case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBuiltInCMap",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");if(!this.cMapReaderFactory)throw new Error("CMapReaderFactory not initialized, see the `useWorkerFetch` parameter.");return this.cMapReaderFactory.fetch(t)}));t.on("FetchStandardFontData",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");if(!this.standardFontDataFactory)throw new Error("StandardFontDataFactory not initialized, see the `useWorkerFetch` parameter.");return this.standardFontDataFactory.fetch(t)}))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#Ds.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((i=>{if(this.destroyed)throw new Error("Transport destroyed");i.refStr&&this.#ks.set(i.refStr,t);const s=new PDFPageProxy(e,i,this,this._params.pdfBug);this.#Ps.set(e,s);return s}));this.#Ds.set(e,s);return s}getPageIndex(t){return isRefProxy(t)?this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}):Promise.reject(new Error("Invalid pageIndex request."))}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#Is("GetFieldObjects")}hasJSActions(){return this.#Is("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#Is("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#Is("GetOptionalContentConfig").then((e=>new OptionalContentConfig(e,t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#Ms.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#Ms.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#Ps.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#Ms.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`;return this.#ks.get(e)??null}}const Lt=Symbol("INITIAL_DATA");class PDFObjects{#Fs=Object.create(null);#Ls(t){return this.#Fs[t]||={...Promise.withResolvers(),data:Lt}}get(t,e=null){if(e){const i=this.#Ls(t);i.promise.then((()=>e(i.data)));return null}const i=this.#Fs[t];if(!i||i.data===Lt)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#Fs[t];return!!e&&e.data!==Lt}delete(t){const e=this.#Fs[t];if(!e||e.data===Lt)return!1;delete this.#Fs[t];return!0}resolve(t,e=null){const i=this.#Ls(t);i.data=e;i.resolve()}clear(){for(const t in this.#Fs){const{data:e}=this.#Fs[t];e?.bitmap?.close()}this.#Fs=Object.create(null)}*[Symbol.iterator](){for(const t in this.#Fs){const{data:e}=this.#Fs[t];e!==Lt&&(yield[t,e])}}}class RenderTask{#Os=null;constructor(t){this.#Os=t;this.onContinue=null}get promise(){return this.#Os.capability.promise}cancel(t=0){this.#Os.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#Os.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#Os;return t.form||t.canvas&&e?.size>0}}class InternalRenderTask{#Ns=null;static#Bs=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#Bs.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#Bs.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#Ns){window.cancelAnimationFrame(this.#Ns);this.#Ns=null}InternalRenderTask.#Bs.delete(this._canvas);this.callback(t||new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e))}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#Ns=window.requestAnimationFrame((()=>{this.#Ns=null;this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#Bs.delete(this._canvas);this.callback()}}}}}const Ot="4.10.38",Nt="f9bea397f";function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}function scaleAndClamp(t){return Math.max(0,Math.min(255,255*t))}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,a=1-i;return["CMYK",s,n,a,Math.min(s,n,a)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(it,t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const a=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){a.value===i.attributes.xfaOn?t.setAttribute("checked",!0):a.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",(t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value){t.setAttribute("value",a.value);for(const t of i.children)t.attributes.value===a.value?t.attributes.selected=!0:t.attributes.hasOwnProperty("selected")&&delete t.attributes.selected}t.addEventListener("input",(t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})}))}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${s}`);for(const[e,i]of Object.entries(a))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}r&&n.addLinkAttributes(t,a.href,a.newWindow);i&&a.dataId&&this.setupStorage(t,a.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",a=document.createElement(s.name);s.attributes&&this.setAttributes({html:a,element:s,intent:n,linkService:i});const r="richText"!==n,o=t.div;o.append(a);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}r&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);a.append(t);r&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,a]];for(;h.length>0;){const[t,s,a]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);a.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);a.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);r&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const Bt=1e3,Ht=new WeakSet;function getRectDims(t){return{width:t[2]-t[0],height:t[3]-t[1]}}class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case S:return new LinkAnnotationElement(t);case E:return new TextAnnotationElement(t);case U:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case H:return new PopupAnnotationElement(t);case C:return new FreeTextAnnotationElement(t);case T:return new LineAnnotationElement(t);case M:return new SquareAnnotationElement(t);case P:return new CircleAnnotationElement(t);case k:return new PolylineAnnotationElement(t);case N:return new CaretAnnotationElement(t);case B:return new InkAnnotationElement(t);case D:return new PolygonAnnotationElement(t);case R:return new HighlightAnnotationElement(t);case I:return new UnderlineAnnotationElement(t);case F:return new SquigglyAnnotationElement(t);case L:return new StrikeOutAnnotationElement(t);case O:return new StampAnnotationElement(t);case z:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#Hs=null;#zs=!1;#Us=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;e&&(this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({titleObj:t,contentsObj:e,richText:i}){return!!(t?.str||e?.str||i?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)}updateEdited(t){if(!this.container)return;this.#Hs||={rect:this.data.rect.slice(0)};const{rect:e}=t;e&&this.#Gs(e);this.#Us?.popup.updateEdited(t)}resetEdited(){if(this.#Hs){this.#Gs(this.#Hs.rect);this.#Us?.popup.resetEdited();this.#Hs=null}}#Gs(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:a,pageX:r,pageY:o}}}}=this;i?.splice(0,4,...t);const{width:l,height:h}=getRectDims(t);e.left=100*(t[0]-r)/n+"%";e.top=100*(a-t[3]+o)/a+"%";if(0===s){e.width=100*l/n+"%";e.height=100*h/a+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||(n.tabIndex=Bt);const{style:a}=n;a.zIndex=this.parent.zIndex++;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:r,height:o}=getRectDims(e.rect);if(!t&&e.borderStyle.width>0){a.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--scale-factor)) / calc(${i}px * var(--scale-factor))`;a.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${r}px * var(--scale-factor)) / calc(${o}px * var(--scale-factor))`;a.borderRadius=t}switch(e.borderStyle.style){case G:a.borderStyle="solid";break;case $:a.borderStyle="dashed";break;case V:warn("Unimplemented border style: beveled");break;case j:warn("Unimplemented border style: inset");break;case W:a.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#zs=!0;a.borderColor=Util.makeHexColor(0|s[0],0|s[1],0|s[2])}else a.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;a.left=100*(l[0]-c)/h+"%";a.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.width=100*r/h+"%";a.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims,{width:n,height:a}=getRectDims(this.data.rect);let r,o;if(t%180==0){r=100*n/i;o=100*a/s}else{r=100*a/i;o=100*n/s}e.style.width=`${r}%`;e.style.height=`${o}%`;e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],a=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](a);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](a)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const a=i[s];if(a){a({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map((t=>Math.fround(t)));if(8===t.length){const[a,r,o,l]=t.subarray(2,6);if(s===a&&n===r&&e===o&&i===l)return}const{style:a}=this.container;let r;if(this.#zs){const{borderColor:t,borderWidth:e}=a;a.borderWidth=0;r=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#zs){r.push("')");a.backgroundImage=r.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(){const{data:t}=this,e=this.#Us=new PopupAnnotationElement({data:{color:t.color,titleObj:t.titleObj,modificationDate:t.modificationDate,contentsObj:t.contentsObj,richText:t.richText,parentRect:t.rect,borderStyle:0,id:`popup_${t.id}`,rotation:t.rotation},parent:this.parent,elements:[this]});this.parent.div.append(e.render())}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:a}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof a?a:null,r=document.querySelector(`[data-element-id="${n}"]`);!r||Ht.has(r)?i.push({id:n,exportValue:s,domElement:r}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(Ht.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",(()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e})}))}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action);s=!0}else if(t.attachment){this.#$s(i,t.attachment,t.attachmentDest);s=!0}else if(t.setOCGState){this.#Vs(i,t.setOCGState);s=!0}else if(t.dest){this._bindLink(i,t.dest);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");s&&this.container.append(i);return this.container}#js(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#js()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#js()}#$s(t,e,i=null){t.href=this.linkService.getAnchorUrl("");e.description&&(t.title=e.description);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,i);return!1};this.#js()}#Vs(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#js()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}t.onclick||(t.onclick=()=>!1);this.#js()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#js();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,a=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&a.push(i)}else for(const t of Object.values(this._fieldObjects))a.push(...t);const r=this.annotationStorage,o=[];for(const t of a){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;r.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(Ht.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));!this.data.popupRef&&this.hasPopupData&&this._createPopup();this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return util_FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(i,(t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,i,s){for(const[n,a]of i)if("Action"===a||this.data.actions?.[a]){"Focus"!==a&&"Blur"!==a||(e||={focused:!1});this._setEventListener(t,e,n,a,s);"Focus"!==a||this.data.actions?.Blur?"Blur"!==a||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:i}=this.data.defaultAppearanceData,s=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(n*s))||1);r=Math.min(s,roundToOneDecimal(e/n))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(s,roundToOneDecimal(t/n))}a.fontSize=`calc(${r}px * var(--scale-factor))`;a.color=Util.makeHexColor(i[0],i[1],i[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=i);n.setValue(a.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&n.length>a&&(n=n.slice(0,a));let r=s.formattedValue||this.data.textContent?.join("\n")||null;r&&this.data.comb&&(r=r.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:r,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=r??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type="text";i.setAttribute("value",r??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);Ht.add(i);i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=Bt;this._setRequired(i,this.data.required);a&&(i.maxLength=a);i.addEventListener("input",(s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null}));i.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",(t=>{if(o.focused)return;const{target:e}=t;o.userValue&&(e.value=o.userValue);o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)}));i.addEventListener("updatefromsandbox",(i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);t.setValue(e,{formattedValue:s})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let a=o.userValue;if(a&&!(a.length<=s)){a=a.slice(0,s);n.value=o.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)}));i.addEventListener("keydown",(t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const s=blurListener;blurListener=null;i.addEventListener("blur",(t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{value:i}=t.target;o.userValue=i;o.lastCommittedValue!==i&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)}));this.data.actions?.Keystroke&&i.addEventListener("beforeinput",(t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:a,selectionEnd:r}=s;let l=a,h=r;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(a).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":a===r&&(l-=1);break;case"deleteContentForward":a===r&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})}));this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");Ht.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=Bt;n.addEventListener("change",(s=>{const{name:n,checked:a}=s.target;for(const s of this._getElementsByName(n,i)){const i=a&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:a})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue}));if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");Ht.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=Bt;n.addEventListener("change",(e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",(e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");Ht.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=Bt;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let a=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);a=()=>{t.remove();s.removeEventListener("input",a);a=null};s.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,(t=>t.selected)).map((t=>t[e])):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",(i=>{const n={value(i){a?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,a=i.detail.remove;n[a].selected=!1;s.remove(a);if(n.length>0){-1===Array.prototype.findIndex.call(n,(t=>t.selected))&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(i){const{index:n,displayValue:a,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");h.textContent=a;h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");n.textContent=e;n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)}));s.addEventListener("input",(i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],(t=>t.target.value))}else s.addEventListener("input",(function(i){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i}=t;super(t,{isRenderable:AnnotationElement._hasPopupData(e)});this.elements=i;this.popup=null}render(){this.container.classList.add("popupAnnotation");const t=this.popup=new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open}),e=[];for(const i of this.elements){i.popup=t;i.container.ariaHasPopup="dialog";e.push(i.data.id);i.addHighlightArea()}this.container.setAttribute("aria-controls",e.map((t=>`${et}${t}`)).join(","));return this.container}}class PopupElement{#Ws=this.#qs.bind(this);#Xs=this.#Ks.bind(this);#Ys=this.#Qs.bind(this);#Js=this.#Zs.bind(this);#tn=null;#pt=null;#en=null;#in=null;#sn=null;#nn=null;#an=null;#rn=!1;#on=null;#C=null;#ln=null;#hn=null;#dn=null;#Hs=null;#cn=!1;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:a,richText:r,parent:o,rect:l,parentRect:h,open:d}){this.#pt=t;this.#dn=s;this.#en=a;this.#hn=r;this.#nn=o;this.#tn=e;this.#ln=l;this.#an=h;this.#sn=i;this.#in=PDFDateString.toDateObject(n);this.trigger=i.flatMap((t=>t.getElementsToTriggerPopup()));for(const t of this.trigger){t.addEventListener("click",this.#Js);t.addEventListener("mouseenter",this.#Ys);t.addEventListener("mouseleave",this.#Xs);t.classList.add("popupTriggerArea")}for(const t of i)t.container?.addEventListener("keydown",this.#Ws);this.#pt.hidden=!0;d&&this.#Zs()}render(){if(this.#on)return;const t=this.#on=document.createElement("div");t.className="popup";if(this.#tn){const e=t.style.outlineColor=Util.makeHexColor(...this.#tn);if(CSS.supports("background-color","color-mix(in srgb, red 30%, white)"))t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`;else{const e=.7;t.style.backgroundColor=Util.makeHexColor(...this.#tn.map((t=>Math.floor(e*(255-t)+t))))}}const e=document.createElement("span");e.className="header";const i=document.createElement("h1");e.append(i);({dir:i.dir,str:i.textContent}=this.#dn);t.append(e);if(this.#in){const t=document.createElement("span");t.classList.add("popupDate");t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#in.valueOf()}));e.append(t)}const s=this.#un;if(s){XfaLayer.render({xfaHtml:s,intent:"richText",div:t});t.lastChild.classList.add("richText","popupContent")}else{const e=this._formatContents(this.#en);t.append(e)}this.#pt.append(t)}get#un(){const t=this.#hn,e=this.#en;return!t?.str||e?.str&&e.str!==t.str?null:this.#hn.html||null}get#pn(){return this.#un?.attributes?.style?.fontSize||0}get#gn(){return this.#un?.attributes?.style?.color||null}#mn(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#gn,fontSize:this.#pn?`calc(${this.#pn}px * var(--scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}_formatContents({str:t,dir:e}){const i=document.createElement("p");i.classList.add("popupContent");i.dir=e;const s=t.split(/(?:\r\n?|\n)/);for(let t=0,e=s.length;t=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#xn.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#Sn()}));!e.popupRef&&this.hasPopupData?this._createPopup():i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#En}addHighlightArea(){this.container.classList.add("highlightArea")}#Sn(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Cn=null;#Tn=null;#Mn=new Map;#Pn=null;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:a,structTreeLayer:r}){this.div=t;this.#Cn=e;this.#Tn=i;this.#Pn=r||null;this.page=n;this.viewport=a;this.zIndex=0;this._annotationEditorUIManager=s}hasEditableAnnotations(){return this.#Mn.size>0}async#Dn(t,e){const i=t.firstChild||t,s=i.id=`${et}${e}`,n=await(this.#Pn?.getAriaAttributes(s));if(n)for(const[t,e]of n)i.setAttribute(t,e);this.div.append(t);this.#Cn?.moveElementInDOM(this.div,t,i,!1)}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n={data:null,layer:i,linkService:t.linkService,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:t.annotationStorage||new AnnotationStorage,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===H;if(e){const e=s.get(t.id);if(!e)continue;n.elements=e}else{const{width:e,height:i}=getRectDims(t.rect);if(e<=0||i<=0)continue}n.data=t;const i=AnnotationElementFactory.create(n);if(!i.isRenderable)continue;if(!e&&t.popupRef){const e=s.get(t.popupRef);e?e.push(i):s.set(t.popupRef,[i])}const a=i.render();t.hidden&&(a.style.visibility="hidden");await this.#Dn(a,t.id);if(i._isEditable){this.#Mn.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}this.#kn()}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#kn();e.hidden=!1}#kn(){if(!this.#Tn)return;const t=this.div;for(const[e,i]of this.#Tn){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i)}this.#Tn.clear()}getEditableAnnotations(){return Array.from(this.#Mn.values())}getEditableAnnotation(t){return this.#Mn.get(t)}}const zt=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#tn;#Rn="";#In=`${this.id}-editor`;#Fn=null;#pn;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=g.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.#tn=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#pn=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case m.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case m.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case m.FREETEXT_SIZE:this.#Ln(e);break;case m.FREETEXT_COLOR:this.#On(e)}}static get defaultPropertiesToUpdate(){return[[m.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[m.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[m.FREETEXT_SIZE,this.#pn],[m.FREETEXT_COLOR,this.#tn]]}#Ln(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--scale-factor))`;this.translate(0,-(t-this.#pn)*this.parentScale);this.#pn=t;this.#Nn()},e=this.#pn;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#On(t){const setColor=t=>{this.#tn=this.editorDiv.style.color=t},e=this.#tn;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#pn)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(this.isInEditMode())return;this.parent.setEditingState(!1);this.parent.updateToolbar(g.FREETEXT);super.enableEditMode();this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#Fn=new AbortController;const t=this._uiManager.combinedSignal(this.#Fn);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t})}disableEditMode(){if(this.isInEditMode()){this.parent.setEditingState(!0);super.disableEditMode();this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#In);this._isDraggable=!0;this.#Fn?.abort();this.#Fn=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing")}}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#Bn(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#Hn(i));e=i}return t.join("\n")}#Nn(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#Rn,e=this.#Rn=this.#Bn().trimEnd();if(t===e)return;const setText=t=>{this.#Rn=t;if(t){this.#zn();this._uiManager.rebuild(this);this.#Nn()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#Nn()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}dblclick(t){this.enterInEditMode()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#In);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#pn}px * var(--scale-factor))`;i.color=this.#tn;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);bindEvents(this,this.div,["dblclick","keydown"]);if(this.width){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[a,r]=this.getInitialTranslation();[a,r]=this.pageTranslationToScreen(a,r);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[r,-a];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[-a,-r];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[a,r]=[-r,a]}this.setAt(c*i,u*s,a,r)}else this.setAt(t*i,e*s,this.width*i,this.height*s);this.#zn();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#Hn(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(zt,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#Un(e.getData("text")||"").replaceAll(zt,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const a=n.getRangeAt(0);if(!s.includes("\n")){a.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:r,startOffset:o}=a,l=[],h=[];if(r.nodeType===Node.TEXT_NODE){const t=r.parentElement;h.push(r.nodeValue.slice(o).replaceAll(zt,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#Hn(i)):e=h}l.push(r.nodeValue.slice(0,o).replaceAll(zt,""))}else if(r===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#Hn(i))}}this.#Rn=`${l.join("\n")}${s}${h.join("\n")}`;this.#zn();const d=new Range;let c=l.reduce(((t,e)=>t+e.length),0);for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#zn(){this.editorDiv.replaceChildren();if(this.#Rn)for(const t of this.#Rn.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Gn(){return this.#Rn.replaceAll(" "," ")}static#Un(t){return t.replaceAll(" "," ")}get contentDiv(){return this.editorDiv}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:a,id:r,popupRef:o},textContent:l,textPosition:h,parent:{page:{pageNumber:d}}}=t;if(!l||0===l.length)return null;s=t={annotationType:g.FREETEXT,color:Array.from(i),fontSize:e,value:l.join("\n"),position:h,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,popupRef:o}}const n=await super.deserialize(t,e,i);n.#pn=t.fontSize;n.#tn=Util.makeHexColor(...t.color);n.#Rn=FreeTextEditor.#Un(t.value);n.annotationElementId=t.id||null;n._initialData=s;return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=FreeTextEditor._internalPadding*this.parentScale,i=this.getRect(e,e),s=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#tn),n={annotationType:g.FREETEXT,color:s,fontSize:this.#pn,value:this.#Gn(),pageIndex:this.pageIndex,rect:i,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t)return n;if(this.annotationElementId&&!this.#$n(n))return null;n.id=this.annotationElementId;return n}#$n(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some(((t,e)=>t!==s[e]))||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(this.deleted)return e;const{style:i}=e;i.fontSize=`calc(${this.#pn}px * var(--scale-factor))`;i.color=this.#tn;e.replaceChildren();for(const t of this.#Rn.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}const s=FreeTextEditor._internalPadding*this.parentScale;t.updateEdited({rect:this.getRect(s,s),popupContent:this.#Rn});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,a){a||=new Float32Array(t.length);for(let r=0,o=t.length;r=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#ha(i);return i.join(" ")}#oa(){const[t,e,i,s]=this.#Vn,[n,a,r,o]=this.#ra();return`M${(this.#Kn[2]-t)/i} ${(this.#Kn[3]-e)/s} L${(this.#Kn[4]-t)/i} ${(this.#Kn[5]-e)/s} L${n} ${a} L${r} ${o} L${(this.#Kn[16]-t)/i} ${(this.#Kn[17]-e)/s} L${(this.#Kn[14]-t)/i} ${(this.#Kn[15]-e)/s} Z`}#ha(t){const e=this.#jn;t.push(`L${e[4]} ${e[5]} Z`)}#la(t){const[e,i,s,n]=this.#Vn,a=this.#Kn.subarray(4,6),r=this.#Kn.subarray(16,18),[o,l,h,d]=this.#ra();t.push(`L${(a[0]-e)/s} ${(a[1]-i)/n} L${o} ${l} L${h} ${d} L${(r[0]-e)/s} ${(r[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,a){return new FreeDrawOutline(t,e,i,s,n,a)}getOutlines(){const t=this.#Xn,e=this.#jn,i=this.#Kn,[s,n,a,r]=this.#Vn,o=new Float32Array((this.#ia?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#ua(l,h);return this.newFreeDrawOutline(l,o,this.#Vn,this.#ta,this.#Wn,this.#qn)}#da(t){const e=this.#Kn,[i,s,n,a]=this.#Vn,[r,o,l,h]=this.#ra(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/a,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/a,NaN,NaN,NaN,NaN,r,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/a,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/a],0);return this.newFreeDrawOutline(d,t,this.#Vn,this.#ta,this.#Wn,this.#qn)}#ua(t,e){const i=this.#jn;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#ca(t,e){const i=this.#Kn.subarray(4,6),s=this.#Kn.subarray(16,18),[n,a,r,o]=this.#Vn,[l,h,d,c]=this.#ra();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/r,(i[1]-a)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/r,(s[1]-a)/o],e);return e+24}}class FreeDrawOutline extends Outline{#Vn;#pa=new Float32Array(4);#Wn;#qn;#ia;#ta;#ga;constructor(t,e,i,s,n,a){super();this.#ga=t;this.#ia=e;this.#Vn=i;this.#ta=s;this.#Wn=n;this.#qn=a;this.lastPoint=[NaN,NaN];this.#ma(a);const[r,o,l,h]=this.#pa;for(let e=0,i=t.length;et[0]-e[0]||t[1]-e[1]||t[2]-e[2]));const t=[];for(const e of this.#ba)if(e[3]){t.push(...this.#wa(e));this.#va(e)}else{this.#ya(e);t.push(...this.#wa(e))}return this.#xa(t)}#xa(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort(((t,e)=>t[1]-e[1]||t[0]-e[0]));for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,a,r,o,l]=t;i.delete(t);let h=e,d=a;n=[e,r];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,a,r,o,l]=t;if(h!==e){n.push(h,d,e,d===a?a:r);h=e}d=d===a?r:a}n.push(h,d)}return new HighlightOutline(s,this.#Vn,this.#fa)}#_a(t){const e=this.#Aa;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,a=e[n][0];if(a===t)return n;a=0;s--){const[i,n]=this.#Aa[s];if(i!==t)break;if(i===t&&n===e){this.#Aa.splice(s,1);return}}}#wa(t){const[e,i,s]=t,n=[[e,i,s]],a=this.#_a(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===a)return[];n.splice(t,1);t--;a--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#Vn;#Ea;constructor(t,e,i){super();this.#Ea=t;this.#Vn=e;this.lastPoint=i}toSVGPath(){const t=[];for(const e of this.#Ea){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#Xa=!0;this.#Za(t);this.#tr()}else if(this.#Ua){this.#Ha=t.anchorNode;this.#za=t.anchorOffset;this.#Va=t.focusNode;this.#ja=t.focusOffset;this.#er();this.#tr();this.rotate(this.rotation)}}get telemetryInitialData(){return{action:"added",type:this.#Xa?"free_highlight":"highlight",color:this._uiManager.highlightColorNames.get(this.color),thickness:this.#ea,methodOfCreation:this.#Ja}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.highlightColorNames.get(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#er(){const t=new HighlightOutliner(this.#Ua,.001);this.#qa=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#qa.box;const e=new HighlightOutliner(this.#Ua,.0025,.001,"ltr"===this._uiManager.direction);this.#$a=e.getOutlines();const{lastPoint:i}=this.#$a;this.#fa=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height]}#Za({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#qa=t;this.#$a=t.getNewOutline(this.#ea/2+1.5,.0025);if(e>=0){this.#w=e;this.#Ga=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#Ya=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#$a.box,path:{d:this.#$a.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#w,{bbox:HighlightEditor.#ir(this.#qa.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#Ya,{bbox:HighlightEditor.#ir(this.#$a.box,e),path:{d:this.#$a.toSVGPath()}})}const[s,n,a,r]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=a;this.height=r;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=a*e/t;this.height=r*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=a;this.height=r;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=a*e/t;this.height=r*t/e;break}}const{lastPoint:o}=this.#$a;this.#fa=[(o[0]-s)/a,(o[1]-n)/r]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case m.HIGHLIGHT_DEFAULT_COLOR:HighlightEditor._defaultColor=e;break;case m.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#fa}updateParams(t,e){switch(t){case m.HIGHLIGHT_COLOR:this.#On(e);break;case m.HIGHLIGHT_THICKNESS:this.#sr(e)}}static get defaultPropertiesToUpdate(){return[[m.HIGHLIGHT_DEFAULT_COLOR,HighlightEditor._defaultColor],[m.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[m.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[m.HIGHLIGHT_THICKNESS,this.#ea||HighlightEditor._defaultThickness],[m.HIGHLIGHT_FREE,this.#Xa]]}#On(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.#Ka=e;this.parent?.drawLayer.updateProperties(this.#w,{root:{fill:t,"fill-opacity":e}});this.#n?.updateColor(t)},e=this.color,i=this.#Ka;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.highlightColorNames.get(t)},!0)}#sr(t){const e=this.#ea,setThickness=t=>{this.#ea=t;this.#nr(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.highlightColors){this.#n=new ColorPicker({editor:this});t.addColorPicker(this.#n)}return t}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#ar())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#ar())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#rr();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#tr();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#rr();else if(t){this.#tr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#nr(t){if(!this.#Xa)return;this.#Za({highlightOutlines:this.#qa.getNewOutline(t/2)});this.fixAndSetPosition();const[e,i]=this.parentDimensions;this.setDims(this.width*e,this.height*i)}#rr(){if(null!==this.#w&&this.parent){this.parent.drawLayer.remove(this.#w);this.#w=null;this.parent.drawLayer.remove(this.#Ya);this.#Ya=null}}#tr(t=this.parent){if(null===this.#w){({id:this.#w,clipPathId:this.#Ga}=t.drawLayer.draw({bbox:this.#qa.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.#Ka},rootClass:{highlight:!0,free:this.#Xa},path:{d:this.#qa.toSVGPath()}},!1,!0));this.#Ya=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#Xa},bbox:this.#$a.box,path:{d:this.#$a.toSVGPath()}},this.#Xa);this.#Wa&&(this.#Wa.style.clipPath=this.#Ga)}}static#ir([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#Xa){t=(t-this.rotation+360)%360;i=HighlightEditor.#ir(this.#qa.box,t)}else i=HighlightEditor.#ir([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#w,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#Ya,{bbox:HighlightEditor.#ir(this.#$a.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#Qa){t.setAttribute("aria-label",this.#Qa);t.setAttribute("role","mark")}this.#Xa?t.classList.add("free"):this.div.addEventListener("keydown",this.#or.bind(this),{signal:this._uiManager._signal});const e=this.#Wa=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Ga;const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);bindEvents(this,this.#Wa,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!1}})}#or(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#lr(!0);break;case 1:case 3:this.#lr(!1)}}#lr(t){if(!this.#Ha)return;const e=window.getSelection();t?e.setPosition(this.#Ha,this.#za):e.setPosition(this.#Va,this.#ja)}select(){super.select();this.#Ya&&this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#Ya){this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{selected:!1}});this.#Xa||this.#lr(!1)}}get _mustFixPosition(){return!this.#Xa}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#w,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#Ya,{rootClass:{hidden:!t}})}}#ar(){return this.#Xa?this.rotation:0}#hr(){if(this.#Xa)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#Ua,a=new Float32Array(8*n.length);let r=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;a[r]=a[r+4]=n;a[r+1]=a[r+3]=c;a[r+2]=a[r+6]=n+h*t;a[r+5]=a[r+7]=c-d*e;r+=8}return a}#dr(t){return this.#qa.serialize(t,this.#ar())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:a,y:r,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#cr(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#ur.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[a,r,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#ur(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#cr(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:a,color:r,opacity:o,popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:g.HIGHLIGHT,color:Array.from(r),opacity:o,quadPoints:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:a,color:r,borderStyle:{rawWidth:o},popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:g.HIGHLIGHT,color:Array.from(r),thickness:o,inkLists:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}const{color:n,quadPoints:a,inkLists:r,opacity:o}=t,l=await super.deserialize(t,e,i);l.color=Util.makeHexColor(...n);l.#Ka=o||1;r&&(l.#ea=t.thickness);l.annotationElementId=t.id||null;l._initialData=s;const[h,d]=l.pageDimensions,[c,u]=l.pageTranslation;if(a){const t=l.#Ua=[];for(let e=0;et!==e[i]))}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#pr=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))this.updateProperty(e,i)}updateSVGProperty(t,e){this.#pr[t]=e}toSVGProperties(){const t=this.#pr;this.#pr=Object.create(null);return{root:t}}reset(){this.#pr=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#gr=null;#mr;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#fr=null;static#br=null;static#Ar=null;static#wr=NaN;static#vr=null;static#yr=null;static#xr=NaN;static _INNER_MARGIN=3;constructor(t){super(t);this.#mr=t.mustBeCommitted||!1;if(t.drawOutlines){this.#_r(t);this.#tr()}}#_r({drawOutlines:t,drawId:e,drawingOptions:i}){this.#gr=t;this._drawingOptions||=i;if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#Er(t,this.parent);this.#Sr(t.box)}#Er(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#fr.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=t=>{s.updateProperty(e,t);const i=this.#gr.updateProperty(e,t);i&&this.#Sr(i);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties())};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathResizingSVGProperties(this.#Cr()),{bbox:this.#Tr()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathResizedSVGProperties(this.#Cr()),{bbox:this.#Tr()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#Tr(t,e)})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathTranslatedSVGProperties(this.#Cr(),this.parentDimensions),{bbox:this.#Tr()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#mr){this.#mr=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#rr();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#tr();this.#Sr(this.#gr.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#rr()}else if(t){this._uiManager.addShouldRescale(this);this.#tr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#rr(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#tr(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#Er(this.#gr,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#Mr([t,e,i,s]){const{parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[e,1-t,i*(a/n),s*(n/a)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(a/n),s*(n/a)];default:return[t,e,i,s]}}#Cr(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[1-e,t,i*(n/a),s*(a/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/a),s*(a/n)];default:return[t,e,i,s]}}#Sr(t){[this.x,this.y,this.width,this.height]=this.#Mr(t);if(this.div){this.fixAndSetPosition();const[t,e]=this.parentDimensions;this.setDims(this.width*t,this.height*e)}this._onResized()}#Tr(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:a,parentDimensions:[r,o]}=this;switch((4*n+a)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(r/o),s*(o/r),i*(r/o)];case 5:return[1-e,t,i*(r/o),s*(o/r)];case 6:return[1-t-s*(o/r),1-e,s*(o/r),i*(r/o)];case 7:return[e-i*(r/o),1-t-s*(o/r),i*(r/o),s*(o/r)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/r),e,s*(o/r),i*(r/o)];case 13:return[1-e-i*(r/o),t-s*(o/r),i*(r/o),s*(o/r)];case 14:return[1-t,1-e-i*(r/o),s*(o/r),i*(r/o)];case 15:return[e,1-t,i*(r/o),s*(o/r)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#Tr()},this.#gr.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#Sr(this.#gr.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;const t=super.render();t.classList.add("draw");const e=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);this._uiManager.addShouldRescale(this);this.disableEditing();return t}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:a,offsetY:r,pointerId:o,pointerType:l}=s;if(DrawingEditor.#vr&&DrawingEditor.#vr!==l)return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#br=new AbortController,p=t.combinedSignal(u);DrawingEditor.#wr||=o;DrawingEditor.#vr??=l;window.addEventListener("pointerup",(t=>{DrawingEditor.#wr===t.pointerId?this._endDraw(t):DrawingEditor.#yr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointercancel",(t=>{DrawingEditor.#wr===t.pointerId?this._currentParent.endDrawingSession():DrawingEditor.#yr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointerdown",(t=>{if(DrawingEditor.#vr===t.pointerType){(DrawingEditor.#yr||=new Set).add(t.pointerId);if(DrawingEditor.#fr.isCancellable()){DrawingEditor.#fr.removeLastElement();DrawingEditor.#fr.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}}),{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",(t=>{t.timeStamp===DrawingEditor.#xr&&stopEvent(t)}),{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#fr)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.startNew(a,r,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#fr=this.createDrawerInstance(a,r,d,c,h);DrawingEditor.#Ar=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#Ar.toSVGProperties(),DrawingEditor.#fr.defaultSVGProperties),!0,!1))}}static _drawMove(t){DrawingEditor.#xr=-1;if(!DrawingEditor.#fr)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(DrawingEditor.#wr===s)if(DrawingEditor.#yr?.size>=1)this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.add(e,i));DrawingEditor.#xr=t.timeStamp;stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#fr=null;DrawingEditor.#Ar=null;DrawingEditor.#vr=null;DrawingEditor.#xr=NaN}if(DrawingEditor.#br){DrawingEditor.#br.abort();DrawingEditor.#br=null;DrawingEditor.#wr=NaN;DrawingEditor.#yr=null}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#fr,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:m.DRAW_STEP})}else this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(m.DRAW_STEP);if(!DrawingEditor.#fr.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,a=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#fr.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#Ar,mustBeCommitted:!t});this._cleanup(!0);return a}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,a){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=e.viewport,o=this.deserializeDraw(a,r,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#_r({drawOutlines:o});l.#tr();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#gr.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#Kn=new Float64Array(6);#bn;#Pr;#Fi;#ea;#ia;#Dr="";#kr=0;#Ea=new InkDrawOutline;#Rr;#Ir;constructor(t,e,i,s,n,a){this.#Rr=i;this.#Ir=s;this.#Fi=n;this.#ea=a;[t,e]=this.#Fr(t,e);const r=this.#bn=[NaN,NaN,NaN,NaN,t,e];this.#ia=[t,e];this.#Pr=[{line:r,points:this.#ia}];this.#Kn.set(r,0)}updateProperty(t,e){"stroke-width"===t&&(this.#ea=e)}#Fr(t,e){return Outline._normalizePoint(t,e,this.#Rr,this.#Ir,this.#Fi)}isEmpty(){return!this.#Pr||0===this.#Pr.length}isCancellable(){return this.#ia.length<=10}add(t,e){[t,e]=this.#Fr(t,e);const[i,s,n,a]=this.#Kn.subarray(2,6),r=t-n,o=e-a;if(Math.hypot(this.#Rr*r,this.#Ir*o)<=2)return null;this.#ia.push(t,e);if(isNaN(i)){this.#Kn.set([n,a,t,e],2);this.#bn.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#Kn[0])&&this.#bn.splice(6,6);this.#Kn.set([i,s,n,a,t,e],0);this.#bn.push(...Outline.createBezierPoints(i,s,n,a,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#ia.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#Rr=i;this.#Ir=s;this.#Fi=n;[t,e]=this.#Fr(t,e);const a=this.#bn=[NaN,NaN,NaN,NaN,t,e];this.#ia=[t,e];const r=this.#Pr.at(-1);if(r){r.line=new Float32Array(r.line);r.points=new Float32Array(r.points)}this.#Pr.push({line:a,points:this.#ia});this.#Kn.set(a,0);this.#kr=0;this.toSVGPath();return null}getLastElement(){return this.#Pr.at(-1)}setLastElement(t){if(!this.#Pr)return this.#Ea.setLastElement(t);this.#Pr.push(t);this.#bn=t.line;this.#ia=t.points;this.#kr=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#Pr)return this.#Ea.removeLastElement();this.#Pr.pop();this.#Dr="";for(let t=0,e=this.#Pr.length;tt??NaN)),d,c,u,p),points:g(r[t].map((t=>t??NaN)),d,c,u,p)});const m=new InkDrawOutline;m.build(h,i,s,1,o,l,n);return m}#Hr(t=this.#ea){const e=this.#Wn+t/2*this.#Or;return this.#Fi%180==0?[e/this.#Rr,e/this.#Ir]:[e/this.#Ir,e/this.#Rr]}#Br(){const[t,e,i,s]=this.#pa,[n,a]=this.#Hr(0);return[t+n,e+a,i-2*n,s-2*a]}#Nr(){const t=this.#pa=new Float32Array([1/0,1/0,-1/0,-1/0]);for(const{line:e}of this.#Pr){if(e.length<=12){for(let i=4,s=e.length;it!==e[i]))||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e});return null}}class StampEditor extends AnnotationEditor{#Ur=null;#Gr=null;#$r=null;#Vr=null;#jr=null;#Wr="";#qr=null;#Xr=null;#Kr=!1;#Yr=!1;static _type="stamp";static _editorType=g.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#Vr=t.bitmapUrl;this.#jr=t.bitmapFile}static initialize(t,e){AnnotationEditor.initialize(t,e)}static get supportedTypes(){return shadow(this,"supportedTypes",["apng","avif","bmp","gif","jpeg","png","svg+xml","webp","x-icon"].map((t=>`image/${t}`)))}static get supportedTypesStr(){return shadow(this,"supportedTypesStr",this.supportedTypes.join(","))}static isHandlingMimeForPasting(t){return this.supportedTypes.includes(t)}static paste(t,e){e.pasteEditor(g.STAMP,{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#Qr(t,e=!1){if(t){this.#Ur=t.bitmap;if(!e){this.#Gr=t.id;this.#Kr=t.isSvg}t.file&&(this.#Wr=t.file.name);this.#Jr()}else this.remove()}#Zr(){this.#$r=null;this._uiManager.enableWaiting(!1);if(this.#qr)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#Ur){this._editToolbar.hide();this._uiManager.editAltText(this,!0)}else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#Ur){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:a}=t||this.copyCanvas(null,null,!0).imageData,r=await i.guess({name:"altText",request:{data:s,width:n,height:a,channels:s.length/(n*a)}});if(!r)throw new Error("No response from the AI service.");if(r.error)throw new Error("Error from the AI service.");if(r.cancel)return null;if(!r.output)throw new Error("No valid response from the AI service.");const o=r.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#to(){if(this.#Gr){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#Gr).then((t=>this.#Qr(t,!0))).finally((()=>this.#Zr()));return}if(this.#Vr){const t=this.#Vr;this.#Vr=null;this._uiManager.enableWaiting(!0);this.#$r=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#Qr(t))).finally((()=>this.#Zr()));return}if(this.#jr){const t=this.#jr;this.#jr=null;this._uiManager.enableWaiting(!0);this.#$r=this._uiManager.imageManager.getFromFile(t).then((t=>this.#Qr(t))).finally((()=>this.#Zr()));return}const t=document.createElement("input");t.type="file";t.accept=StampEditor.supportedTypesStr;const e=this._uiManager._signal;this.#$r=new Promise((i=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#Qr(e)}else this.remove();i()}),{signal:e});t.addEventListener("cancel",(()=>{this.remove();i()}),{signal:e})})).finally((()=>this.#Zr()));t.click()}remove(){if(this.#Gr){this.#Ur=null;this._uiManager.imageManager.deleteId(this.#Gr);this.#qr?.remove();this.#qr=null;if(this.#Xr){clearTimeout(this.#Xr);this.#Xr=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#Gr&&null===this.#qr&&this.#to();this.isAttachedToDOM||this.parent.add(this)}}else this.#Gr&&this.#to()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#$r||this.#Ur||this.#Vr||this.#jr||this.#Gr)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.div.setAttribute("role","figure");this.addAltTextButton();this.#Ur?this.#Jr():this.#to();if(this.width&&!this.annotationElementId){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s)}this._uiManager.addShouldRescale(this);return this.div}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#Xr&&clearTimeout(this.#Xr);this.#Xr=setTimeout((()=>{this.#Xr=null;this.#eo()}),200)}#Jr(){const{div:t}=this;let{width:e,height:i}=this.#Ur;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#qr=document.createElement("canvas");l.setAttribute("role","img");this.addContainer(l);this.width=e/s;this.height=i/n;this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#eo();if(!this.#Yr){this.parent.addUndoableEditor(this);this.#Yr=!0}this._reportTelemetry({action:"inserted_image"});this.#Wr&&l.setAttribute("aria-label",this.#Wr)}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#Ur,a=new OutputScale;let r=this.#Ur,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*a.sx),i=h.height=Math.ceil(l*a.sy);this.#Kr||(r=this.#io(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(window.matchMedia?.("(prefers-color-scheme: dark)").matches){c="#8f8f9d";u="#42414d"}const p=15,g=p*a.sx,m=p*a.sy,f=new OffscreenCanvas(2*g,2*m),b=f.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*m);b.fillStyle=u;b.fillRect(0,0,g,m);b.fillRect(g,m,g,m);d.fillStyle=d.createPattern(f,"repeat");d.fillRect(0,0,t,i);d.drawImage(r,0,0,r.width,r.height,0,0,t,i)}let d=null;if(i){let e,i;if(a.symmetric&&r.widtht||n>t){const a=Math.min(t/s,t/n);e=Math.floor(s*a);i=Math.floor(n*a);this.#Kr||(r=this.#io(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(r,0,0,r.width,r.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#io(t,e){const{width:i,height:s}=this.#Ur;let n=i,a=s,r=this.#Ur;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#eo(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,a=Math.ceil(i*t*n.sx),r=Math.ceil(s*e*n.sy),o=this.#qr;if(!o||o.width===a&&o.height===r)return;o.width=a;o.height=r;const l=this.#Kr?this.#Ur:this.#io(a,r),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,a,r)}getImageForAltText(){return this.#qr}#so(t){if(t){if(this.#Kr){const t=this._uiManager.imageManager.getSvgUrl(this.#Gr);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#Ur);t.getContext("2d").drawImage(this.#Ur,0,0);return t.toDataURL()}if(this.#Kr){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#Ur,0,0,this.#Ur.width,this.#Ur.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#Ur)}static async deserialize(t,e,i){let s=null;if(t instanceof StampAnnotationElement){const{data:{rect:n,rotation:a,id:r,structParent:o,popupRef:l},container:h,parent:{page:{pageNumber:d}}}=t,c=h.querySelector("canvas"),u=i.imageManager.getFromCanvas(h.id,c);c.remove();const p=(await e._structTree.getAriaAttributes(`${et}${r}`))?.get("aria-label")||"";s=t={annotationType:g.STAMP,bitmapId:u.id,bitmap:u.bitmap,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,accessibilityData:{decorative:!1,altText:p},isSvg:!1,structParent:o,popupRef:l}}const n=await super.deserialize(t,e,i),{rect:a,bitmap:r,bitmapUrl:o,bitmapId:l,isSvg:h,accessibilityData:d}=t;if(l&&i.imageManager.isValidId(l)){n.#Gr=l;r&&(n.#Ur=r)}else n.#Vr=o;n.#Kr=h;const[c,u]=n.pageDimensions;n.width=(a[2]-a[0])/c;n.height=(a[3]-a[1])/u;n.annotationElementId=t.id||null;d&&(n.altTextData=d);n._initialData=s;n.#Yr=!!s;return n}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i={annotationType:g.STAMP,bitmapId:this.#Gr,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#Kr,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#so(!0);i.accessibilityData=this.serializeAltText(!0);return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#$n(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1}i.id=this.annotationElementId;if(null===e)return i;e.stamps||=new Map;const a=this.#Kr?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#Gr)){if(this.#Kr){const t=e.stamps.get(this.#Gr);if(a>t.area){t.area=a;t.serialized.bitmap.close();t.serialized.bitmap=this.#so(!1)}}}else{e.stamps.set(this.#Gr,{area:a,serialized:i});i.bitmap=this.#so(!1)}return i}#$n(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}}class AnnotationEditorLayer{#Cn;#no=!1;#ao=null;#ro=null;#oo=null;#lo=new Map;#ho=!1;#do=!1;#co=!1;#uo=null;#po=null;#go=null;#mo=null;#m;static _initialized=!1;static#U=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor].map((t=>[t._editorType,t])));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:a,drawLayer:r,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#U.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#m=t;this.pageIndex=e;this.div=i;this.#Cn=n;this.#ao=a;this.viewport=l;this.#go=o;this.drawLayer=r;this._structTree=s;this.#m.addLayer(this)}get isEmpty(){return 0===this.#lo.size}get isInvisible(){return this.isEmpty&&this.#m.getMode()===g.NONE}updateToolbar(t){this.#m.updateToolbar(t)}updateMode(t=this.#m.getMode()){this.#fo();switch(t){case g.NONE:this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case g.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case g.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;for(const i of AnnotationEditorLayer.#U.values())e.toggle(`${i._type}Editing`,t===i._editorType);this.div.hidden=!1}hasTextLayer(t){return t===this.#go?.div}setEditingState(t){this.#m.setEditingState(t)}addCommands(t){this.#m.addCommands(t)}cleanUndoStack(t){this.#m.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#ao?.div.classList.toggle("disabled",!t)}async enable(){this.#co=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);const t=new Set;for(const e of this.#lo.values()){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#m.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}if(!this.#ao){this.#co=!1;return}const e=this.#ao.getEditableAnnotations();for(const i of e){i.hide();if(this.#m.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#co=!1}disable(){this.#do=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);const t=new Map,e=new Map;for(const i of this.#lo.values()){i.disableEditing();if(i.annotationElementId)if(null===i.serialize()){e.set(i.annotationElementId,i);this.getEditableAnnotation(i.annotationElementId)?.show();i.remove()}else t.set(i.annotationElementId,i)}if(this.#ao){const i=this.#ao.getEditableAnnotations();for(const s of i){const{id:i}=s.data;if(this.#m.isDeletedAnnotationElement(i))continue;let n=e.get(i);if(n){n.resetAnnotationElement(s);n.show(!1);s.show()}else{n=t.get(i);if(n){this.#m.addChangedExistingAnnotation(n);n.renderAnnotationElement(s)&&n.show(!1)}s.show()}}}this.#fo();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#U.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);this.#do=!1}getEditableAnnotation(t){return this.#ao?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#m.getActive()!==t&&this.#m.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#go?.div&&!this.#mo){this.#mo=new AbortController;const t=this.#m.combinedSignal(this.#mo);this.#go.div.addEventListener("pointerdown",this.#bo.bind(this),{signal:t});this.#go.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#go?.div&&this.#mo){this.#mo.abort();this.#mo=null;this.#go.div.classList.remove("highlighting")}}#bo(t){this.#m.unselectAll();const{target:e}=t;if(e===this.#go.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent"))&&this.#go.div.contains(e)){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#m.showAllEditors("highlight",!0,!0);this.#go.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#m.direction,{target:this.#go.div,x:t.x,y:t.y});this.#go.div.addEventListener("pointerup",(()=>{this.#go.div.classList.remove("free");this.toggleDrawing(!0)}),{once:!0,signal:this.#m._signal});t.preventDefault()}}enableClick(){if(this.#ro)return;this.#ro=new AbortController;const t=this.#m.combinedSignal(this.#ro);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#ro?.abort();this.#ro=null}attach(t){this.#lo.set(t.id,t);const{annotationElementId:e}=t;e&&this.#m.isDeletedAnnotationElement(e)&&this.#m.removeDeletedAnnotationElement(t)}detach(t){this.#lo.delete(t.id);this.#Cn?.removePointerInTextLayer(t.contentDiv);!this.#do&&t.annotationElementId&&this.#m.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#m.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#m.addDeletedAnnotationElement(t.annotationElementId);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#m.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#co);this.#m.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#oo){t._focusEventsAllowed=!1;this.#oo=setTimeout((()=>{this.#oo=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this.#m._signal});e.focus()}}),0)}t._structTreeParentId=this.#Cn?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#m.getId()}get#Ao(){return AnnotationEditorLayer.#U.get(this.#m.getMode())}combinedSignal(t){return this.#m.combinedSignal(t)}#wo(t){const e=this.#Ao;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#Ao?.canCreateNewEmptyEditor()}pasteEditor(t,e){this.#m.updateToolbar(t);this.#m.updateMode(t);const{offsetX:i,offsetY:s}=this.#vo(),n=this.getNextId(),a=this.#wo({parent:this,id:n,x:i,y:s,uiManager:this.#m,isCentered:!0,...e});a&&this.add(a)}async deserialize(t){return await(AnnotationEditorLayer.#U.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#m))||null}createAndAddNewEditor(t,e,i={}){const s=this.getNextId(),n=this.#wo({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#m,isCentered:e,...i});n&&this.add(n);return n}#vo(){const{x:t,y:e,width:i,height:s}=this.div.getBoundingClientRect(),n=Math.max(0,t),a=Math.max(0,e),r=(n+Math.min(window.innerWidth,t+i))/2-t,o=(a+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[r,o]:[o,r];return{offsetX:l,offsetY:h}}addNewEditor(){this.createAndAddNewEditor(this.#vo(),!0)}setSelected(t){this.#m.setSelected(t)}toggleSelected(t){this.#m.toggleSelected(t)}unselect(t){this.#m.unselect(t)}pointerup(t){const{isMac:e}=util_FeatureTest.platform;if(!(0!==t.button||t.ctrlKey&&e)&&t.target===this.div&&this.#ho){this.#ho=!1;this.#Ao?.isDrawer&&this.#Ao.supportMultipleDrawings||(this.#no?this.#m.getMode()!==g.STAMP?this.createAndAddNewEditor(t,!1):this.#m.unselectAll():this.#no=!0)}}pointerdown(t){this.#m.getMode()===g.HIGHLIGHT&&this.enableTextSelection();if(this.#ho){this.#ho=!1;return}const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#ho=!0;if(this.#Ao?.isDrawer){this.startDrawingSession(t);return}const i=this.#m.getActive();this.#no=!i||i.isEmpty()}startDrawingSession(t){this.div.focus();if(this.#uo){this.#Ao.startDrawing(this,this.#m,!1,t);return}this.#m.setCurrentDrawingSession(this);this.#uo=new AbortController;const e=this.#m.combinedSignal(this.#uo);this.div.addEventListener("blur",(({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#po=null;this.commitOrRemove()}}),{signal:e});this.#Ao.startDrawing(this,this.#m,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#po=t)}else this.#po&&setTimeout((()=>{this.#po?.focus();this.#po=null}),0)}endDrawingSession(t=!1){if(!this.#uo)return null;this.#m.setCurrentDrawingSession(null);this.#uo.abort();this.#uo=null;this.#po=null;return this.#Ao.endDrawing(t)}findNewParent(t,e,i){const s=this.#m.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#uo){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#uo&&this.#Ao.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#m.getActive()?.parent===this){this.#m.commitOrRemove();this.#m.setActiveEditor(null)}if(this.#oo){clearTimeout(this.#oo);this.#oo=null}for(const t of this.#lo.values()){this.#Cn?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#lo.clear();this.#m.removeLayer(this)}#fo(){for(const t of this.#lo.values())t.isEmpty()&&t.remove()}render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#m.getEditors(this.pageIndex)){this.add(t);t.rebuild()}this.updateMode()}update({viewport:t}){this.#m.commitOrRemove();this.#fo();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#lo.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#m.viewParameters.realScale}}class DrawLayer{#nn=null;#w=0;#yo=new Map;#xo=new Map;constructor({pageIndex:t}){this.pageIndex=t}setParent(t){if(this.#nn){if(this.#nn!==t){if(this.#yo.size>0)for(const e of this.#yo.values()){e.remove();t.append(e)}this.#nn=t}}else this.#nn=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#_o(t,[e,i,s,n]){const{style:a}=t;a.top=100*i+"%";a.left=100*e+"%";a.width=100*s+"%";a.height=100*n+"%"}#Eo(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#nn.append(t);t.setAttribute("aria-hidden",!0);return t}#So(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#Co(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=this.#w++,n=this.#Eo(),a=DrawLayer._svgFactory.createElement("defs");n.append(a);const r=DrawLayer._svgFactory.createElement("path");a.append(r);const o=`path_p${this.pageIndex}_${s}`;r.setAttribute("id",o);r.setAttribute("vector-effect","non-scaling-stroke");e&&this.#xo.set(s,r);const l=i?this.#So(a,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#yo.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=this.#w++,s=this.#Eo(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const a=DrawLayer._svgFactory.createElement("path");n.append(a);const r=`path_p${this.pageIndex}_${i}`;a.setAttribute("id",r);a.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_p${this.pageIndex}_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${r}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${r}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#yo.set(i,s);return i}finalizeDraw(t,e){this.#xo.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:a}=e,r="number"==typeof t?this.#yo.get(t):t;if(r){i&&this.#Co(r,i);s&&DrawLayer.#_o(r,s);if(n){const{classList:t}=r;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(a){const t=r.firstChild.firstChild;this.#Co(t,a)}}}updateParent(t,e){if(e===this)return;const i=this.#yo.get(t);if(i){e.#nn.append(i);this.#yo.delete(t);e.#yo.set(t,i)}}remove(t){this.#xo.delete(t);if(null!==this.#nn){this.#yo.get(t).remove();this.#yo.delete(t)}}destroy(){this.#nn=null;for(const t of this.#yo.values())t.remove();this.#yo.clear();this.#xo.clear()}}globalThis.pdfjsTestingUtils={HighlightOutliner};var Ut=__webpack_exports__.AbortException,Gt=__webpack_exports__.AnnotationEditorLayer,$t=__webpack_exports__.AnnotationEditorParamsType,Vt=__webpack_exports__.AnnotationEditorType,jt=__webpack_exports__.AnnotationEditorUIManager,Wt=__webpack_exports__.AnnotationLayer,qt=__webpack_exports__.AnnotationMode,Xt=__webpack_exports__.ColorPicker,Kt=__webpack_exports__.DOMSVGFactory,Yt=__webpack_exports__.DrawLayer,Qt=__webpack_exports__.FeatureTest,Jt=__webpack_exports__.GlobalWorkerOptions,Zt=__webpack_exports__.ImageKind,te=__webpack_exports__.InvalidPDFException,ee=__webpack_exports__.MissingPDFException,ie=__webpack_exports__.OPS,se=__webpack_exports__.OutputScale,ne=__webpack_exports__.PDFDataRangeTransport,ae=__webpack_exports__.PDFDateString,re=__webpack_exports__.PDFWorker,oe=__webpack_exports__.PasswordResponses,le=__webpack_exports__.PermissionFlag,he=__webpack_exports__.PixelsPerInch,de=__webpack_exports__.RenderingCancelledException,ce=__webpack_exports__.TextLayer,ue=__webpack_exports__.TouchManager,pe=__webpack_exports__.UnexpectedResponseException,ge=__webpack_exports__.Util,me=__webpack_exports__.VerbosityLevel,fe=__webpack_exports__.XfaLayer,be=__webpack_exports__.build,Ae=__webpack_exports__.createValidAbsoluteUrl,we=__webpack_exports__.fetchData,ve=__webpack_exports__.getDocument,ye=__webpack_exports__.getFilenameFromUrl,xe=__webpack_exports__.getPdfFilenameFromUrl,_e=__webpack_exports__.getXfaPageViewport,Ee=__webpack_exports__.isDataScheme,Se=__webpack_exports__.isPdfFile,Ce=__webpack_exports__.noContextMenu,Te=__webpack_exports__.normalizeUnicode,Me=__webpack_exports__.setLayerDimensions,Pe=__webpack_exports__.shadow,De=__webpack_exports__.stopEvent,ke=__webpack_exports__.version;export{Ut as AbortException,Gt as AnnotationEditorLayer,$t as AnnotationEditorParamsType,Vt as AnnotationEditorType,jt as AnnotationEditorUIManager,Wt as AnnotationLayer,qt as AnnotationMode,Xt as ColorPicker,Kt as DOMSVGFactory,Yt as DrawLayer,Qt as FeatureTest,Jt as GlobalWorkerOptions,Zt as ImageKind,te as InvalidPDFException,ee as MissingPDFException,ie as OPS,se as OutputScale,ne as PDFDataRangeTransport,ae as PDFDateString,re as PDFWorker,oe as PasswordResponses,le as PermissionFlag,he as PixelsPerInch,de as RenderingCancelledException,ce as TextLayer,ue as TouchManager,pe as UnexpectedResponseException,ge as Util,me as VerbosityLevel,fe as XfaLayer,be as build,Ae as createValidAbsoluteUrl,we as fetchData,ve as getDocument,ye as getFilenameFromUrl,xe as getPdfFilenameFromUrl,_e as getXfaPageViewport,Ee as isDataScheme,Se as isPdfFile,Ce as noContextMenu,Te as normalizeUnicode,Me as setLayerDimensions,Pe as shadow,De as stopEvent,ke as version}; \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs new file mode 100644 index 00000000000..ee4038504a6 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs @@ -0,0 +1,21 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */var e={d:(t,i)=>{for(var a in i)e.o(i,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:i[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},__webpack_exports__ = globalThis.pdfjsWorker = {};e.d(__webpack_exports__,{WorkerMessageHandler:()=>WorkerMessageHandler});const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),i=[1,0,0,1,0,0],a=[.001,0,0,.001,0,0],s=1.35,r=.35,n=.25925925925925924,g=1,o=2,c=4,C=8,h=16,l=64,Q=128,E=256,u="pdfjs_internal_editor_",d=3,f=9,p=13,m=15,y={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},w=0,D=4,b=1,F=2,S=3,k=1,R=2,N=3,G=4,M=5,U=6,x=7,L=8,H=9,J=10,Y=11,v=12,K=13,T=14,q=15,O=16,W=17,j=20,X="Group",Z="R",V=1,z=2,_=4,$=16,AA=32,eA=128,tA=512,iA=1,aA=2,sA=4096,rA=8192,nA=32768,gA=65536,oA=131072,IA=1048576,cA=2097152,CA=8388608,hA=16777216,lA=1,BA=2,QA=3,EA=4,uA=5,dA={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},fA={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},pA={O:"PageOpen",C:"PageClose"},mA=1,yA=5,wA=1,DA=2,bA=3,FA=4,SA=5,kA=6,RA=7,NA=8,GA=9,MA=10,UA=11,xA=12,LA=13,HA=14,JA=15,YA=16,vA=17,KA=18,TA=19,qA=20,OA=21,PA=22,WA=23,jA=24,XA=25,ZA=26,VA=27,zA=28,_A=29,$A=30,Ae=31,ee=32,te=33,ie=34,ae=35,se=36,re=37,ne=38,ge=39,oe=40,Ie=41,ce=42,Ce=43,he=44,le=45,Be=46,Qe=47,Ee=48,ue=49,de=50,fe=51,pe=52,me=53,ye=54,we=55,De=56,be=57,Fe=58,Se=59,ke=60,Re=61,Ne=62,Ge=63,Me=64,Ue=65,xe=66,Le=67,He=68,Je=69,Ye=70,ve=71,Ke=72,Te=73,qe=74,Oe=75,Pe=76,We=77,je=80,Xe=81,Ze=83,Ve=84,ze=85,_e=86,$e=87,At=88,et=89,tt=90,it=91,at=92,st=93,rt=1,nt=2;let gt=mA;function getVerbosityLevel(){return gt}function info(e){gt>=yA&&console.log(`Info: ${e}`)}function warn(e){gt>=mA&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,i=null){if(!e)return null;try{if(i&&"string"==typeof e){if(i.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(i.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const a=t?new URL(e,t):new URL(e);if(function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(a))return a}catch{}return null}function shadow(e,t,i,a=!1){Object.defineProperty(e,t,{value:i,enumerable:!a,configurable:!0,writable:!1});return i}const ot=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends ot{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends ot{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends ot{constructor(e){super(e,"InvalidPDFException")}}class MissingPDFException extends ot{constructor(e){super(e,"MissingPDFException")}}class UnexpectedResponseException extends ot{constructor(e,t){super(e,"UnexpectedResponseException");this.status=t}}class FormatError extends ot{constructor(e){super(e,"FormatError")}}class AbortException extends ot{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,i=8192;if(t>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){return"undefined"!=typeof navigator&&"string"==typeof navigator?.platform?shadow(this,"platform",{isMac:navigator.platform.includes("Mac"),isWindows:navigator.platform.includes("Win"),isFirefox:"string"==typeof navigator?.userAgent&&navigator.userAgent.includes("Firefox")}):shadow(this,"platform",{isMac:!1,isWindows:!1,isFirefox:!1})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const It=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,i){return`#${It[e]}${It[t]}${It[i]}`}static scaleMinMax(e,t){let i;if(e[0]){if(e[0]<0){i=t[0];t[0]=t[2];t[2]=i}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){i=t[1];t[1]=t[3];t[3]=i}t[1]*=e[3];t[3]*=e[3]}else{i=t[0];t[0]=t[1];t[1]=i;i=t[2];t[2]=t[3];t[3]=i;if(e[1]<0){i=t[1];t[1]=t[3];t[3]=i}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){i=t[0];t[0]=t[2];t[2]=i}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static applyTransform(e,t){return[e[0]*t[0]+e[1]*t[2]+t[4],e[0]*t[1]+e[1]*t[3]+t[5]]}static applyInverseTransform(e,t){const i=t[0]*t[3]-t[1]*t[2];return[(e[0]*t[3]-e[1]*t[2]+t[2]*t[5]-t[4]*t[3])/i,(-e[0]*t[1]+e[1]*t[0]+t[4]*t[1]-t[5]*t[0])/i]}static getAxialAlignedBoundingBox(e,t){const i=this.applyTransform(e,t),a=this.applyTransform(e.slice(2,4),t),s=this.applyTransform([e[0],e[3]],t),r=this.applyTransform([e[2],e[1]],t);return[Math.min(i[0],a[0],s[0],r[0]),Math.min(i[1],a[1],s[1],r[1]),Math.max(i[0],a[0],s[0],r[0]),Math.max(i[1],a[1],s[1],r[1])]}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e){const t=[e[0],e[2],e[1],e[3]],i=e[0]*t[0]+e[1]*t[2],a=e[0]*t[1]+e[1]*t[3],s=e[2]*t[0]+e[3]*t[2],r=e[2]*t[1]+e[3]*t[3],n=(i+r)/2,g=Math.sqrt((i+r)**2-4*(i*r-s*a))/2,o=n+g||1,c=n-g||1;return[Math.sqrt(o),Math.sqrt(c)]}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const i=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),a=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(i>a)return null;const s=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),r=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return s>r?null:[i,s,a,r]}static#A(e,t,i,a,s,r,n,g,o,c){if(o<=0||o>=1)return;const C=1-o,h=o*o,l=h*o,Q=C*(C*(C*e+3*o*t)+3*h*i)+l*a,E=C*(C*(C*s+3*o*r)+3*h*n)+l*g;c[0]=Math.min(c[0],Q);c[1]=Math.min(c[1],E);c[2]=Math.max(c[2],Q);c[3]=Math.max(c[3],E)}static#e(e,t,i,a,s,r,n,g,o,c,C,h){if(Math.abs(o)<1e-12){Math.abs(c)>=1e-12&&this.#A(e,t,i,a,s,r,n,g,-C/c,h);return}const l=c**2-4*C*o;if(l<0)return;const Q=Math.sqrt(l),E=2*o;this.#A(e,t,i,a,s,r,n,g,(-c+Q)/E,h);this.#A(e,t,i,a,s,r,n,g,(-c-Q)/E,h)}static bezierBoundingBox(e,t,i,a,s,r,n,g,o){if(o){o[0]=Math.min(o[0],e,n);o[1]=Math.min(o[1],t,g);o[2]=Math.max(o[2],e,n);o[3]=Math.max(o[3],t,g)}else o=[Math.min(e,n),Math.min(t,g),Math.max(e,n),Math.max(t,g)];this.#e(e,i,s,n,t,a,r,g,3*(3*(i-s)-e+n),6*(e-2*i+s),3*(i-e),o);this.#e(e,i,s,n,t,a,r,g,3*(3*(a-r)-t+g),6*(t-2*a+r),3*(a-t),o);return o}}const ct=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e){if(e[0]>="ï"){let t;if("þ"===e[0]&&"ÿ"===e[1]){t="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("ÿ"===e[0]&&"þ"===e[1]){t="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"ï"===e[0]&&"»"===e[1]&&"¿"===e[2]&&(t="utf-8");if(t)try{const i=new TextDecoder(t,{fatal:!0}),a=stringToBytes(e),s=i.decode(a);return s.includes("")?s.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,""):s}catch(e){warn(`stringToPDFString: "${e}".`)}}const t=[];for(let i=0,a=e.length;iIt[e])).join("")}"function"!=typeof Promise.try&&(Promise.try=function(e,...t){return new Promise((i=>{i(e(...t))}))});const lt=Symbol("CIRCULAR_REF"),Bt=Symbol("EOF");let Qt=Object.create(null),Et=Object.create(null),ut=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return Et[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return Qt[e]||=new Cmd(e)}}const dt=function nonSerializableClosure(){return dt};class Dict{constructor(e=null){this._map=new Map;this.xref=e;this.objId=null;this.suppressEncryption=!1;this.__nonSerializable__=dt}assignXref(e){this.xref=e}get size(){return this._map.size}get(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}return a instanceof Ref&&this.xref?this.xref.fetch(a,this.suppressEncryption):a}async getAsync(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}return a instanceof Ref&&this.xref?this.xref.fetchAsync(a,this.suppressEncryption):a}getArray(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}a instanceof Ref&&this.xref&&(a=this.xref.fetch(a,this.suppressEncryption));if(Array.isArray(a)){a=a.slice();for(let e=0,t=a.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:i=!1}){const a=new Dict(e),s=new Map;for(const e of t)if(e instanceof Dict)for(const[t,a]of e._map){let e=s.get(t);if(void 0===e){e=[];s.set(t,e)}else if(!(i&&a instanceof Dict))continue;e.push(a)}for(const[t,i]of s){if(1===i.length||!(i[0]instanceof Dict)){a._map.set(t,i[0]);continue}const s=new Dict(e);for(const e of i)for(const[t,i]of e._map)s._map.has(t)||s._map.set(t,i);s.size>0&&a._map.set(t,s)}s.clear();return a.size>0?a:Dict.empty}clone(){const e=new Dict(this.xref);for(const t of this.getKeys())e.set(t,this.getRaw(t));return e}delete(e){delete this._map[e]}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=ut[e];if(t)return t;const i=/^(\d+)R(\d*)$/.exec(e);return i&&"0"!==i[1]?ut[e]=new Ref(parseInt(i[1]),i[2]?parseInt(i[2]):0):null}static get(e,t){const i=0===t?`${e}R`:`${e}R${t}`;return ut[i]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{constructor(){this._map=new Map}get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,i=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}}const ft=/^[1-9]\.\d$/,pt=2**31-1;function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends ot{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends ot{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends ot{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends ot{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let i=0;for(let a=0;a0,"The number should be a positive integer.");const i="M".repeat(e/1e3|0)+mt[e%1e3/100|0]+mt[10+(e%100/10|0)]+mt[20+e%10];return t?i.toLowerCase():i}function log2(e){return e>0?Math.ceil(Math.log2(e)):0}function readInt8(e,t){return e[t]<<24>>24}function readUint16(e,t){return e[t]<<8|e[t+1]}function readUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every((e=>"number"==typeof e)):ArrayBuffer.isView(e)&&(0===e.length||"number"==typeof e[0])&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map((e=>{const i=e.match(t);return i?{name:i[1],pos:parseInt(i[2],10)}:{name:e,pos:0}}))}function escapePDFName(e){const t=[];let i=0;for(let a=0,s=e.length;a126||35===s||40===s||41===s||60===s||62===s||91===s||93===s||123===s||125===s||47===s||37===s){i"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`))}function _collectJS(e,t,i,a){if(!e)return;let s=null;if(e instanceof Ref){if(a.has(e))return;s=e;a.put(s);e=t.fetch(e)}if(Array.isArray(e))for(const s of e)_collectJS(s,t,i,a);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let a;t instanceof BaseStream?a=t.getString():"string"==typeof t&&(a=t);a&&=stringToPDFString(a).replaceAll("\0","");a&&i.push(a)}_collectJS(e.getRaw("Next"),t,i,a)}s&&a.remove(s)}function collectActions(e,t,i){const a=Object.create(null),s=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(s)for(let t=s.length-1;t>=0;t--){const r=s[t];if(r instanceof Dict)for(const t of r.getKeys()){const s=i[t];if(!s)continue;const n=[];_collectJS(r.getRaw(t),e,n,new RefSet);n.length>0&&(a[s]=n)}}if(t.has("A")){const i=[];_collectJS(t.get("A"),e,i,new RefSet);i.length>0&&(a.Action=i)}return objectSize(a)>0?a:null}const yt={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,i=e.length;t55295&&(i<57344||i>65533)&&t++;yield i}}function encodeToXmlString(e){const t=[];let i=0;for(let a=0,s=e.length;a55295&&(s<57344||s>65533)&&a++;i=a+1}}if(0===t.length)return e;i: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:i,fontWeight:a,italicAngle:s}=e;if(!validateFontName(i,!0))return!1;const r=a?a.toString():"";e.fontWeight=t.has(r)?r:"400";const n=parseFloat(s);e.italicAngle=isNaN(n)||n<-90||n>90?"14":s.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[i,a]of e){if(!i.startsWith(u))continue;let e=t.get(a.pageIndex);if(!e){e=[];t.set(a.pageIndex,e)}e.push(a)}return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return function isAscii(e){return/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let i=0,a=e.length;i>8&255],It[255&a])}return t.join("")}function stringToUTF16String(e,t=!1){const i=[];t&&i.push("þÿ");for(let t=0,a=e.length;t>8&255),String.fromCharCode(255&a))}return i.join("")}function getRotationMatrix(e,t,i){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,i];case 270:return[0,-1,1,0,0,i];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class Stream extends BaseStream{constructor(e,t,i,a){super();this.bytes=e instanceof Uint8Array?e:new Uint8Array(e);this.start=t||0;this.pos=this.start;this.end=t+i||this.bytes.length;this.dict=a}get length(){return this.end-this.start}get isEmpty(){return 0===this.length}getByte(){return this.pos>=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,i=this.pos,a=this.end;if(!e)return t.subarray(i,a);let s=i+e;s>a&&(s=a);this.pos=s;return t.subarray(i,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,i=null){return new Stream(this.bytes.buffer,e,t,i)}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{constructor(e,t,i){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this._loadedChunks=new Set;this.numChunks=Math.ceil(e/t);this.manager=i;this.progressiveDataLength=0;this.lastSuccessfulEnsureByteChunk=-1}getMissingChunks(){const e=[];for(let t=0,i=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=i;ethis.numChunks)&&t!==this.lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this.lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const i=Math.floor(e/this.chunkSize);if(i>this.numChunks)return;const a=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let s=i;s=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,i=this.pos,a=this.end;if(!e){a>this.progressiveDataLength&&this.ensureRange(i,a);return t.subarray(i,a)}let s=i+e;s>a&&(s=a);s>this.progressiveDataLength&&this.ensureRange(i,s);this.pos=s;return t.subarray(i,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,i=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),i=Math.floor((this.end-1)/e)+1,a=[];for(let e=t;e{const readChunk=({value:r,done:n})=>{try{if(n){const t=arrayBuffersToBytes(a);a=null;e(t);return}s+=r.byteLength;i.isStreamingSupported&&this.onProgress({loaded:s});a.push(r);i.read().then(readChunk,t)}catch(e){t(e)}};i.read().then(readChunk,t)})).then((t=>{this.aborted||this.onReceiveData({chunk:t,begin:e})}))}requestAllChunks(e=!1){if(!e){const e=this.stream.getMissingChunks();this._requestChunks(e)}return this._loadedStreamCapability.promise}_requestChunks(e){const t=this.currRequestId++,i=new Set;this._chunksNeededByRequest.set(t,i);for(const t of e)this.stream.hasChunk(t)||i.add(t);if(0===i.size)return Promise.resolve();const a=Promise.withResolvers();this._promisesByRequest.set(t,a);const s=[];for(const e of i){let i=this._requestsByChunk.get(e);if(!i){i=[];this._requestsByChunk.set(e,i);s.push(e)}i.push(t)}if(s.length>0){const e=this.groupChunks(s);for(const t of e){const e=t.beginChunk*this.chunkSize,i=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,i).catch(a.reject)}}return a.promise.catch((e=>{if(!this.aborted)throw e}))}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const i=this.getBeginChunk(e),a=this.getEndChunk(t),s=[];for(let e=i;e=0&&a+1!==r){t.push({beginChunk:i,endChunk:a+1});i=r}s+1===e.length&&t.push({beginChunk:i,endChunk:r+1});a=r}return t}onProgress(e){this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize+e.loaded,total:this.length})}onReceiveData(e){const t=e.chunk,i=void 0===e.begin,a=i?this.progressiveDataLength:e.begin,s=a+t.byteLength,r=Math.floor(a/this.chunkSize),n=s0||g.push(i)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===this.stream.numChunksLoaded){const t=this.stream.numChunks-1;this.stream.hasChunk(t)||(e=t)}else e=this.stream.nextEmptyChunk(n);Number.isInteger(e)&&this._requestChunks([e])}for(const e of g){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize,total:this.length})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfNetworkStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}class ColorSpace{constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t){const i=new Uint8ClampedArray(3);this.getRgbItem(e,t,i,0);return i}getRgbItem(e,t,i,a){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,i,a,s,r,n){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,i,a,s,r,n,g,o){const c=t*i;let C=null;const h=1<h&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=n<=8?new Uint8Array(h):new Uint16Array(h);for(let e=0;e=.99554525?1:this.#B(0,1,1.055*e**(1/2.4)-.055)}#B(e,t,i){return Math.max(e,Math.min(t,i))}#Q(e){return e<0?-this.#Q(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#I}#E(e,t,i){if(0===e[0]&&0===e[1]&&0===e[2]){i[0]=t[0];i[1]=t[1];i[2]=t[2];return}const a=this.#Q(0),s=(1-a)/(1-this.#Q(e[0])),r=1-s,n=(1-a)/(1-this.#Q(e[1])),g=1-n,o=(1-a)/(1-this.#Q(e[2])),c=1-o;i[0]=t[0]*s+r;i[1]=t[1]*n+g;i[2]=t[2]*o+c}#u(e,t,i){if(1===e[0]&&1===e[2]){i[0]=t[0];i[1]=t[1];i[2]=t[2];return}const a=i;this.#c(CalRGBCS.#i,t,a);const s=CalRGBCS.#n;this.#C(e,a,s);this.#c(CalRGBCS.#a,s,i)}#d(e,t,i){const a=i;this.#c(CalRGBCS.#i,t,a);const s=CalRGBCS.#n;this.#h(e,a,s);this.#c(CalRGBCS.#a,s,i)}#t(e,t,i,a,s){const r=this.#B(0,1,e[t]*s),n=this.#B(0,1,e[t+1]*s),g=this.#B(0,1,e[t+2]*s),o=1===r?1:r**this.GR,c=1===n?1:n**this.GG,C=1===g?1:g**this.GB,h=this.MXA*o+this.MXB*c+this.MXC*C,l=this.MYA*o+this.MYB*c+this.MYC*C,Q=this.MZA*o+this.MZB*c+this.MZC*C,E=CalRGBCS.#g;E[0]=h;E[1]=l;E[2]=Q;const u=CalRGBCS.#o;this.#u(this.whitePoint,E,u);const d=CalRGBCS.#g;this.#E(this.blackPoint,u,d);const f=CalRGBCS.#o;this.#d(CalRGBCS.#r,d,f);const p=CalRGBCS.#g;this.#c(CalRGBCS.#s,f,p);i[a]=255*this.#l(p[0]);i[a+1]=255*this.#l(p[1]);i[a+2]=255*this.#l(p[2])}getRgbItem(e,t,i,a){this.#t(e,t,i,a,1)}getRgbBuffer(e,t,i,a,s,r,n){const g=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#f(e){return e>=6/29?e**3:108/841*(e-4/29)}#p(e,t,i,a){return i+e*(a-i)/t}#t(e,t,i,a,s){let r=e[t],n=e[t+1],g=e[t+2];if(!1!==i){r=this.#p(r,i,0,100);n=this.#p(n,i,this.amin,this.amax);g=this.#p(g,i,this.bmin,this.bmax)}n>this.amax?n=this.amax:nthis.bmax?g=this.bmax:g>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,i){let a=0;for(let s=i;s>=0;s--){a+=e[s]+t[s];e[s]=255&a;a>>=8}}function incHex(e,t){let i=1;for(let a=t;a>=0&&i>0;a--){i+=e[a];e[a]=255&i;i>>=8}}const wt=16;class BinaryCMapStream{constructor(e){this.buffer=e;this.pos=0;this.end=e.length;this.tmpBuf=new Uint8Array(19)}readByte(){return this.pos>=this.end?-1:this.buffer[this.pos++]}readNumber(){let e,t=0;do{const i=this.readByte();if(i<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&i);t=t<<7|127&i}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.buffer.subarray(this.pos,this.pos+t+1));this.pos+=t+1}readHexNumber(e,t){let i;const a=this.tmpBuf;let s=0;do{const e=this.readByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");i=!(128&e);a[s++]=127&e}while(!i);let r=t,n=0,g=0;for(;r>=0;){for(;g<8&&a.length>0;){n|=a[--s]<>=8;g-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const i=1&e[t]?255:0;let a=0;for(let s=0;s<=t;s++){a=(1&a)<<8|e[s];e[s]=a>>1^i}}readString(){const e=this.readNumber(),t=new Array(e);for(let i=0;i=0;){const e=l>>5;if(7===e){switch(31&l){case 0:a.readString();break;case 1:r=a.readString()}continue}const i=!!(16&l),s=15&l;if(s+1>wt)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const Q=1,E=a.readNumber();switch(e){case 0:a.readHex(n,s);a.readHexNumber(g,s);addHex(g,n,s);t.addCodespaceRange(s+1,hexToInt(n,s),hexToInt(g,s));for(let e=1;es&&(a=s)}else{for(;!this.eof;)this.readBlock(t);a=this.bufferLength}this.pos=a;return this.buffer.subarray(i,a)}async getImageData(e,t=null){if(!this.canAsyncDecodeImageFromBuffer)return this.getBytes(e,t);const i=await this.stream.asyncGetBytes();return this.decodeImage(i,t)}reset(){this.pos=0}makeSubStream(e,t,i=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const i=e+t;for(;this.bufferLength<=i&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,i)}getBaseStreams(){return this.str?this.str.getBaseStreams():null}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter((e=>e instanceof BaseStream));let i=0;for(const t of e)i+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(i);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let i;try{i=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const a=this.bufferLength,s=a+i.length;this.ensureBuffer(s).set(i,a);this.bufferLength=s}getBaseStreams(){const e=[];for(const t of this.streams){const i=t.getBaseStreams();i&&e.push(...i)}return e.length>0?e:null}}class Ascii85Stream extends DecodeStream{constructor(e,t){t&&(t*=.8);super(t);this.str=e;this.dict=e.dict;this.input=new Uint8Array(5)}readBlock(){const e=this.str;let t=e.getByte();for(;isWhiteSpace(t);)t=e.getByte();if(-1===t||126===t){this.eof=!0;return}const i=this.bufferLength;let a,s;if(122===t){a=this.ensureBuffer(i+4);for(s=0;s<4;++s)a[i+s]=0;this.bufferLength+=4}else{const r=this.input;r[0]=t;for(s=1;s<5;++s){t=e.getByte();for(;isWhiteSpace(t);)t=e.getByte();r[s]=t;if(-1===t||126===t)break}a=this.ensureBuffer(i+s-1);this.bufferLength+=s-1;if(s<5){for(;s<5;++s)r[s]=117;this.eof=!0}let n=0;for(s=0;s<5;++s)n=85*n+(r[s]-33);for(s=3;s>=0;--s){a[i+s]=255&n;n>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.str=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.str.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,i=this.ensureBuffer(this.bufferLength+t);let a=this.bufferLength,s=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(s<0)s=e;else{i[a++]=s<<4|e;s=-1}}if(s>=0&&this.eof){i[a++]=s<<4;s=-1}this.firstDigit=s;this.bufferLength=a}}const bt=-1,Ft=[[-1,-1],[-1,-1],[7,8],[7,7],[6,6],[6,6],[6,5],[6,5],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2]],St=[[-1,-1],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[12,1984],[12,2048],[12,2112],[12,2176],[12,2240],[12,2304],[11,1856],[11,1856],[11,1920],[11,1920],[12,2368],[12,2432],[12,2496],[12,2560]],kt=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[8,29],[8,29],[8,30],[8,30],[8,45],[8,45],[8,46],[8,46],[7,22],[7,22],[7,22],[7,22],[7,23],[7,23],[7,23],[7,23],[8,47],[8,47],[8,48],[8,48],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[7,20],[7,20],[7,20],[7,20],[8,33],[8,33],[8,34],[8,34],[8,35],[8,35],[8,36],[8,36],[8,37],[8,37],[8,38],[8,38],[7,19],[7,19],[7,19],[7,19],[8,31],[8,31],[8,32],[8,32],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[8,53],[8,53],[8,54],[8,54],[7,26],[7,26],[7,26],[7,26],[8,39],[8,39],[8,40],[8,40],[8,41],[8,41],[8,42],[8,42],[8,43],[8,43],[8,44],[8,44],[7,21],[7,21],[7,21],[7,21],[7,28],[7,28],[7,28],[7,28],[8,61],[8,61],[8,62],[8,62],[8,63],[8,63],[8,0],[8,0],[8,320],[8,320],[8,384],[8,384],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[7,27],[7,27],[7,27],[7,27],[8,59],[8,59],[8,60],[8,60],[9,1472],[9,1536],[9,1600],[9,1728],[7,18],[7,18],[7,18],[7,18],[7,24],[7,24],[7,24],[7,24],[8,49],[8,49],[8,50],[8,50],[8,51],[8,51],[8,52],[8,52],[7,25],[7,25],[7,25],[7,25],[8,55],[8,55],[8,56],[8,56],[8,57],[8,57],[8,58],[8,58],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[8,448],[8,448],[8,512],[8,512],[9,704],[9,768],[8,640],[8,640],[8,576],[8,576],[9,832],[9,896],[9,960],[9,1024],[9,1088],[9,1152],[9,1216],[9,1280],[9,1344],[9,1408],[7,256],[7,256],[7,256],[7,256],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7]],Rt=[[-1,-1],[-1,-1],[12,-2],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[11,1792],[11,1792],[12,1984],[12,1984],[12,2048],[12,2048],[12,2112],[12,2112],[12,2176],[12,2176],[12,2240],[12,2240],[12,2304],[12,2304],[11,1856],[11,1856],[11,1856],[11,1856],[11,1920],[11,1920],[11,1920],[11,1920],[12,2368],[12,2368],[12,2432],[12,2432],[12,2496],[12,2496],[12,2560],[12,2560],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[12,52],[12,52],[13,640],[13,704],[13,768],[13,832],[12,55],[12,55],[12,56],[12,56],[13,1280],[13,1344],[13,1408],[13,1472],[12,59],[12,59],[12,60],[12,60],[13,1536],[13,1600],[11,24],[11,24],[11,24],[11,24],[11,25],[11,25],[11,25],[11,25],[13,1664],[13,1728],[12,320],[12,320],[12,384],[12,384],[12,448],[12,448],[13,512],[13,576],[12,53],[12,53],[12,54],[12,54],[13,896],[13,960],[13,1024],[13,1088],[13,1152],[13,1216],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64]],Nt=[[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[11,23],[11,23],[12,50],[12,51],[12,44],[12,45],[12,46],[12,47],[12,57],[12,58],[12,61],[12,256],[10,16],[10,16],[10,16],[10,16],[10,17],[10,17],[10,17],[10,17],[12,48],[12,49],[12,62],[12,63],[12,30],[12,31],[12,32],[12,33],[12,40],[12,41],[11,22],[11,22],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[12,128],[12,192],[12,26],[12,27],[12,28],[12,29],[11,19],[11,19],[11,20],[11,20],[12,34],[12,35],[12,36],[12,37],[12,38],[12,39],[11,21],[11,21],[12,42],[12,43],[10,0],[10,0],[10,0],[10,0],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12]],Gt=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[6,9],[6,8],[5,7],[5,7],[4,6],[4,6],[4,6],[4,6],[4,5],[4,5],[4,5],[4,5],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2]];class CCITTFaxDecoder{constructor(e,t={}){if("function"!=typeof e?.next)throw new Error('CCITTFaxDecoder - invalid "source" parameter.');this.source=e;this.eof=!1;this.encoding=t.K||0;this.eoline=t.EndOfLine||!1;this.byteAlign=t.EncodedByteAlign||!1;this.columns=t.Columns||1728;this.rows=t.Rows||0;this.eoblock=t.EndOfBlock??!0;this.black=t.BlackIs1||!1;this.codingLine=new Uint32Array(this.columns+1);this.refLine=new Uint32Array(this.columns+2);this.codingLine[0]=this.columns;this.codingPos=0;this.row=0;this.nextLine2D=this.encoding<0;this.inputBits=0;this.inputBuf=0;this.outputBits=0;this.rowsDone=!1;let i;for(;0===(i=this._lookBits(12));)this._eatBits(1);1===i&&this._eatBits(12);if(this.encoding>0){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}}readNextChar(){if(this.eof)return-1;const e=this.refLine,t=this.codingLine,i=this.columns;let a,s,r,n,g;if(0===this.outputBits){this.rowsDone&&(this.eof=!0);if(this.eof)return-1;this.err=!1;let r,g,o;if(this.nextLine2D){for(n=0;t[n]=64);do{g+=o=this._getWhiteCode()}while(o>=64)}else{do{r+=o=this._getWhiteCode()}while(o>=64);do{g+=o=this._getBlackCode()}while(o>=64)}this._addPixels(t[this.codingPos]+r,s);t[this.codingPos]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]=64);else do{r+=o=this._getWhiteCode()}while(o>=64);this._addPixels(t[this.codingPos]+r,s);s^=1}}let c=!1;this.byteAlign&&(this.inputBits&=-8);if(this.eoblock||this.row!==this.rows-1){r=this._lookBits(12);if(this.eoline)for(;r!==bt&&1!==r;){this._eatBits(1);r=this._lookBits(12)}else for(;0===r;){this._eatBits(1);r=this._lookBits(12)}if(1===r){this._eatBits(12);c=!0}else r===bt&&(this.eof=!0)}else this.rowsDone=!0;if(!this.eof&&this.encoding>0&&!this.rowsDone){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}if(this.eoblock&&c&&this.byteAlign){r=this._lookBits(12);if(1===r){this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}if(this.encoding>=0)for(n=0;n<4;++n){r=this._lookBits(12);1!==r&&info("bad rtc code: "+r);this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}}this.eof=!0}}else if(this.err&&this.eoline){for(;;){r=this._lookBits(13);if(r===bt){this.eof=!0;return-1}if(r>>1==1)break;this._eatBits(1)}this._eatBits(12);if(this.encoding>0){this._eatBits(1);this.nextLine2D=!(1&r)}}this.outputBits=t[0]>0?t[this.codingPos=0]:t[this.codingPos=1];this.row++}if(this.outputBits>=8){g=1&this.codingPos?0:255;this.outputBits-=8;if(0===this.outputBits&&t[this.codingPos]r){g<<=r;1&this.codingPos||(g|=255>>8-r);this.outputBits-=r;r=0}else{g<<=this.outputBits;1&this.codingPos||(g|=255>>8-this.outputBits);r-=this.outputBits;this.outputBits=0;if(t[this.codingPos]0){g<<=r;r=0}}}while(r)}this.black&&(g^=255);return g}_addPixels(e,t){const i=this.codingLine;let a=this.codingPos;if(e>i[a]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&a^t&&++a;i[a]=e}this.codingPos=a}_addPixelsNeg(e,t){const i=this.codingLine;let a=this.codingPos;if(e>i[a]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&a^t&&++a;i[a]=e}else if(e0&&e=s){const t=i[e-s];if(t[0]===a){this._eatBits(a);return[!0,t[1],!0]}}}return[!1,0,!1]}_getTwoDimCode(){let e,t=0;if(this.eoblock){t=this._lookBits(7);e=Ft[t];if(e?.[0]>0){this._eatBits(e[0]);return e[1]}}else{const e=this._findTableCode(1,7,Ft);if(e[0]&&e[2])return e[1]}info("Bad two dim code");return bt}_getWhiteCode(){let e,t=0;if(this.eoblock){t=this._lookBits(12);if(t===bt)return 1;e=t>>5?kt[t>>3]:St[t];if(e[0]>0){this._eatBits(e[0]);return e[1]}}else{let e=this._findTableCode(1,9,kt);if(e[0])return e[1];e=this._findTableCode(11,12,St);if(e[0])return e[1]}info("bad white code");this._eatBits(1);return 1}_getBlackCode(){let e,t;if(this.eoblock){e=this._lookBits(13);if(e===bt)return 1;t=e>>7?!(e>>9)&&e>>7?Nt[(e>>1)-64]:Gt[e>>7]:Rt[e];if(t[0]>0){this._eatBits(t[0]);return t[1]}}else{let e=this._findTableCode(2,6,Gt);if(e[0])return e[1];e=this._findTableCode(7,12,Nt,64);if(e[0])return e[1];e=this._findTableCode(10,13,Rt);if(e[0])return e[1]}info("bad black code");this._eatBits(1);return 1}_lookBits(e){let t;for(;this.inputBits>16-e;this.inputBuf=this.inputBuf<<8|t;this.inputBits+=8}return this.inputBuf>>this.inputBits-e&65535>>16-e}_eatBits(e){(this.inputBits-=e)<0&&(this.inputBits=0)}}class CCITTFaxStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;i instanceof Dict||(i=Dict.empty);const a={next:()=>e.getByte()};this.ccittFaxDecoder=new CCITTFaxDecoder(a,{K:i.get("K"),EndOfLine:i.get("EndOfLine"),EncodedByteAlign:i.get("EncodedByteAlign"),Columns:i.get("Columns"),Rows:i.get("Rows"),EndOfBlock:i.get("EndOfBlock"),BlackIs1:i.get("BlackIs1")})}readBlock(){for(;!this.eof;){const e=this.ccittFaxDecoder.readNextChar();if(-1===e){this.eof=!0;return}this.ensureBuffer(this.bufferLength+1);this.buffer[this.bufferLength++]=e}}}const Mt=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Ut=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),xt=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),Lt=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],Ht=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5];class FlateStream extends DecodeStream{constructor(e,t){super(t);this.str=e;this.dict=e.dict;const i=e.getByte(),a=e.getByte();if(-1===i||-1===a)throw new FormatError(`Invalid header in flate stream: ${i}, ${a}`);if(8!=(15&i))throw new FormatError(`Unknown compression method in flate stream: ${i}, ${a}`);if(((i<<8)+a)%31!=0)throw new FormatError(`Bad FCHECK in flate stream: ${i}, ${a}`);if(32&a)throw new FormatError(`FDICT bit set in flate stream: ${i}, ${a}`);this.codeSize=0;this.codeBuf=0}async getImageData(e,t){const i=await this.asyncGetBytes();return i?.subarray(0,e)||this.getBytes(e)}async asyncGetBytes(){this.str.reset();const e=this.str.getBytes();try{const{readable:t,writable:i}=new DecompressionStream("deflate"),a=i.getWriter();await a.ready;a.write(e).then((async()=>{await a.ready;await a.close()})).catch((()=>{}));const s=[];let r=0;for await(const e of t){s.push(e);r+=e.byteLength}const n=new Uint8Array(r);let g=0;for(const e of s){n.set(e,g);g+=e.byteLength}return n}catch{this.str=new Stream(e,2,e.length,this.str.dict);this.reset();return null}}get isAsync(){return!0}getBits(e){const t=this.str;let i,a=this.codeSize,s=this.codeBuf;for(;a>e;this.codeSize=a-=e;return i}getCode(e){const t=this.str,i=e[0],a=e[1];let s,r=this.codeSize,n=this.codeBuf;for(;r>16,c=65535&g;if(o<1||r>o;this.codeSize=r-o;return c}generateHuffmanTable(e){const t=e.length;let i,a=0;for(i=0;ia&&(a=e[i]);const s=1<>=1}for(i=e;i>=1;if(0===t){let t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}let i=t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}i|=t<<8;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}let s=t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}s|=t<<8;if(s!==(65535&~i)&&(0!==i||0!==s))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const r=this.bufferLength,n=r+i;e=this.ensureBuffer(n);this.bufferLength=n;if(0===i)-1===a.peekByte()&&(this.eof=!0);else{const t=a.getBytes(i);e.set(t,r);t.length0;)C[g++]=Q}s=this.generateHuffmanTable(C.subarray(0,e));r=this.generateHuffmanTable(C.subarray(e,c))}}e=this.buffer;let n=e?e.length:0,g=this.bufferLength;for(;;){let t=this.getCode(s);if(t<256){if(g+1>=n){e=this.ensureBuffer(g+1);n=e.length}e[g++]=t;continue}if(256===t){this.bufferLength=g;return}t-=257;t=Ut[t];let a=t>>16;a>0&&(a=this.getBits(a));i=(65535&t)+a;t=this.getCode(r);t=xt[t];a=t>>16;a>0&&(a=this.getBits(a));const o=(65535&t)+a;if(g+i>=n){e=this.ensureBuffer(g+i);n=e.length}for(let t=0;t>9&127;this.clow=this.clow<<7&65535;this.ct-=7;this.a=32768}byteIn(){const e=this.data;let t=this.bp;if(255===e[t])if(e[t+1]>143){this.clow+=65280;this.ct=8}else{t++;this.clow+=e[t]<<9;this.ct=7;this.bp=t}else{t++;this.clow+=t65535){this.chigh+=this.clow>>16;this.clow&=65535}}readBit(e,t){let i=e[t]>>1,a=1&e[t];const s=Jt[i],r=s.qe;let n,g=this.a-r;if(this.chigh>15&1;this.clow=this.clow<<1&65535;this.ct--}while(!(32768&g));this.a=g;e[t]=i<<1|a;return n}}class Jbig2Error extends ot{constructor(e){super(e,"Jbig2Error")}}class ContextCache{getContexts(e){return e in this?this[e]:this[e]=new Int8Array(65536)}}class DecodingContext{constructor(e,t,i){this.data=e;this.start=t;this.end=i}get decoder(){return shadow(this,"decoder",new ArithmeticDecoder(this.data,this.start,this.end))}get contextCache(){return shadow(this,"contextCache",new ContextCache)}}function decodeInteger(e,t,i){const a=e.getContexts(t);let s=1;function readBits(e){let t=0;for(let r=0;r>>0}const r=readBits(1),n=readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(32)+4436:readBits(12)+340:readBits(8)+84:readBits(6)+20:readBits(4)+4:readBits(2);let g;0===r?g=n:n>0&&(g=-n);return g>=-2147483648&&g<=pt?g:null}function decodeIAID(e,t,i){const a=e.getContexts("IAID");let s=1;for(let e=0;e=F&&x=S){K=K<<1&d;for(u=0;u=0&&H=0){J=G[L][H];J&&(K|=J<=e?c<<=1:c=c<<1|w[g][o]}for(Q=0;Q=m||o<0||o>=p?c<<=1:c=c<<1|a[g][o]}const E=D.readBit(b,c);t[n]=E}}return w}function decodeTextRegion(e,t,i,a,s,r,n,g,o,c,C,h,l,Q,E,u,d,f,p){if(e&&t)throw new Jbig2Error("refinement with Huffman is not supported");const m=[];let y,w;for(y=0;y1&&(s=e?p.readBits(f):decodeInteger(b,"IAIT",D));const r=n*F+s,S=e?Q.symbolIDTable.decode(p):decodeIAID(b,D,o),k=t&&(e?p.readBit():decodeInteger(b,"IARI",D));let R=g[S],N=R[0].length,G=R.length;if(k){const e=decodeInteger(b,"IARDW",D),t=decodeInteger(b,"IARDH",D);N+=e;G+=t;R=decodeRefinement(N,G,E,R,(e>>1)+decodeInteger(b,"IARDX",D),(t>>1)+decodeInteger(b,"IARDY",D),!1,u,d)}let M=0;c?1&h?M=G-1:a+=G-1:h>1?a+=N-1:M=N-1;const U=r-(1&h?0:G-1),x=a-(2&h?N-1:0);let L,H,J;if(c)for(L=0;L>5&7;const o=[31&n];let c=t+6;if(7===n){g=536870911&readUint32(e,c-1);c+=3;let t=g+7>>3;o[0]=e[c++];for(;--t>0;)o.push(e[c++])}else if(5===n||6===n)throw new Jbig2Error("invalid referred-to flags");i.retainBits=o;let C=4;i.number<=256?C=1:i.number<=65536&&(C=2);const h=[];let l,Q;for(l=0;l>>24&255;r[3]=t.height>>16&255;r[4]=t.height>>8&255;r[5]=255&t.height;for(l=c,Q=e.length;l>2&3;e.huffmanDWSelector=t>>4&3;e.bitmapSizeSelector=t>>6&1;e.aggregationInstancesSelector=t>>7&1;e.bitmapCodingContextUsed=!!(256&t);e.bitmapCodingContextRetained=!!(512&t);e.template=t>>10&3;e.refinementTemplate=t>>12&1;c+=2;if(!e.huffman){o=0===e.template?4:1;n=[];for(g=0;g>2&3;C.stripSize=1<>4&3;C.transposed=!!(64&h);C.combinationOperator=h>>7&3;C.defaultPixelValue=h>>9&1;C.dsOffset=h<<17>>27;C.refinementTemplate=h>>15&1;if(C.huffman){const e=readUint16(a,c);c+=2;C.huffmanFS=3&e;C.huffmanDS=e>>2&3;C.huffmanDT=e>>4&3;C.huffmanRefinementDW=e>>6&3;C.huffmanRefinementDH=e>>8&3;C.huffmanRefinementDX=e>>10&3;C.huffmanRefinementDY=e>>12&3;C.huffmanRefinementSizeSelector=!!(16384&e)}if(C.refinement&&!C.refinementTemplate){n=[];for(g=0;g<2;g++){n.push({x:readInt8(a,c),y:readInt8(a,c+1)});c+=2}C.refinementAt=n}C.numberOfSymbolInstances=readUint32(a,c);c+=4;r=[C,i.referredTo,a,c,s];break;case 16:const l={},Q=a[c++];l.mmr=!!(1&Q);l.template=Q>>1&3;l.patternWidth=a[c++];l.patternHeight=a[c++];l.maxPatternIndex=readUint32(a,c);c+=4;r=[l,i.number,a,c,s];break;case 22:case 23:const E={};E.info=readRegionSegmentInformation(a,c);c+=Ot;const u=a[c++];E.mmr=!!(1&u);E.template=u>>1&3;E.enableSkip=!!(8&u);E.combinationOperator=u>>4&7;E.defaultPixelValue=u>>7&1;E.gridWidth=readUint32(a,c);c+=4;E.gridHeight=readUint32(a,c);c+=4;E.gridOffsetX=4294967295&readUint32(a,c);c+=4;E.gridOffsetY=4294967295&readUint32(a,c);c+=4;E.gridVectorX=readUint16(a,c);c+=2;E.gridVectorY=readUint16(a,c);c+=2;r=[E,i.referredTo,a,c,s];break;case 38:case 39:const d={};d.info=readRegionSegmentInformation(a,c);c+=Ot;const f=a[c++];d.mmr=!!(1&f);d.template=f>>1&3;d.prediction=!!(8&f);if(!d.mmr){o=0===d.template?4:1;n=[];for(g=0;g>2&1;p.combinationOperator=m>>3&3;p.requiresBuffer=!!(32&m);p.combinationOperatorOverride=!!(64&m);r=[p];break;case 49:case 50:case 51:case 62:break;case 53:r=[i.number,a,c,s];break;default:throw new Jbig2Error(`segment type ${i.typeName}(${i.type}) is not implemented`)}const C="on"+i.typeName;C in t&&t[C].apply(t,r)}function processSegments(e,t){for(let i=0,a=e.length;i>3,i=new Uint8ClampedArray(t*e.height);e.defaultPixelValue&&i.fill(255);this.buffer=i}drawBitmap(e,t){const i=this.currentPageInfo,a=e.width,s=e.height,r=i.width+7>>3,n=i.combinationOperatorOverride?e.combinationOperator:i.combinationOperator,g=this.buffer,o=128>>(7&e.x);let c,C,h,l,Q=e.y*r+(e.x>>3);switch(n){case 0:for(c=0;c>=1;if(!h){h=128;l++}}Q+=r}break;case 2:for(c=0;c>=1;if(!h){h=128;l++}}Q+=r}break;default:throw new Jbig2Error(`operator ${n} is not supported`)}}onImmediateGenericRegion(e,t,i,a){const s=e.info,r=new DecodingContext(t,i,a),n=decodeBitmap(e.mmr,s.width,s.height,e.template,e.prediction,null,e.at,r);this.drawBitmap(s,n)}onImmediateLosslessGenericRegion(){this.onImmediateGenericRegion(...arguments)}onSymbolDictionary(e,t,i,a,s,r){let n,g;if(e.huffman){n=function getSymbolDictionaryHuffmanTables(e,t,i){let a,s,r,n,g=0;switch(e.huffmanDHSelector){case 0:case 1:a=getStandardTable(e.huffmanDHSelector+4);break;case 3:a=getCustomHuffmanTable(g,t,i);g++;break;default:throw new Jbig2Error("invalid Huffman DH selector")}switch(e.huffmanDWSelector){case 0:case 1:s=getStandardTable(e.huffmanDWSelector+2);break;case 3:s=getCustomHuffmanTable(g,t,i);g++;break;default:throw new Jbig2Error("invalid Huffman DW selector")}if(e.bitmapSizeSelector){r=getCustomHuffmanTable(g,t,i);g++}else r=getStandardTable(1);n=e.aggregationInstancesSelector?getCustomHuffmanTable(g,t,i):getStandardTable(1);return{tableDeltaHeight:a,tableDeltaWidth:s,tableBitmapSize:r,tableAggregateInstances:n}}(e,i,this.customTables);g=new Reader(a,s,r)}let o=this.symbols;o||(this.symbols=o={});const c=[];for(const e of i){const t=o[e];t&&c.push(...t)}const C=new DecodingContext(a,s,r);o[t]=function decodeSymbolDictionary(e,t,i,a,s,r,n,g,o,c,C,h){if(e&&t)throw new Jbig2Error("symbol refinement with Huffman is not supported");const l=[];let Q=0,E=log2(i.length+a);const u=C.decoder,d=C.contextCache;let f,p;if(e){f=getStandardTable(1);p=[];E=Math.max(E,1)}for(;l.length1)m=decodeTextRegion(e,t,a,Q,0,s,1,i.concat(l),E,0,0,1,0,r,o,c,C,0,h);else{const e=decodeIAID(d,u,E),t=decodeInteger(d,"IARDX",u),s=decodeInteger(d,"IARDY",u);m=decodeRefinement(a,Q,o,e=32){let i,a,n;switch(t){case 32:if(0===e)throw new Jbig2Error("no previous value in symbol ID table");a=s.readBits(2)+3;i=r[e-1].prefixLength;break;case 33:a=s.readBits(3)+3;i=0;break;case 34:a=s.readBits(7)+11;i=0;break;default:throw new Jbig2Error("invalid code length in symbol ID table")}for(n=0;n=0;d--){R=e?decodeMMRBitmap(k,o,c,!0):decodeBitmap(!1,o,c,i,!1,null,F,E);S[d]=R}for(N=0;N=0;f--){M^=S[f][N][G];U|=M<>8;H=h+N*l-G*Q>>8;if(L>=0&&L+w<=a&&H>=0&&H+D<=s)for(d=0;d=s)){Y=u[t];J=x[d];for(f=0;f=0&&e>1&7),o=1+(a>>4&7),c=[];let C,h,l=s;do{C=n.readBits(g);h=n.readBits(o);c.push(new HuffmanLine([l,C,h,0]));l+=1<>t&1;if(t<=0)this.children[i]=new HuffmanTreeNode(e);else{let a=this.children[i];a||(this.children[i]=a=new HuffmanTreeNode(null));a.buildTree(e,t-1)}}decodeNode(e){if(this.isLeaf){if(this.isOOB)return null;const t=e.readBits(this.rangeLength);return this.rangeLow+(this.isLowerRange?-t:t)}const t=this.children[e.readBit()];if(!t)throw new Jbig2Error("invalid Huffman data");return t.decodeNode(e)}}class HuffmanTable{constructor(e,t){t||this.assignPrefixCodes(e);this.rootNode=new HuffmanTreeNode(null);for(let t=0,i=e.length;t0&&this.rootNode.buildTree(i,i.prefixLength-1)}}decode(e){return this.rootNode.decodeNode(e)}assignPrefixCodes(e){const t=e.length;let i=0;for(let a=0;a=this.end)throw new Jbig2Error("end of data while reading bit");this.currentByte=this.data[this.position++];this.shift=7}const e=this.currentByte>>this.shift&1;this.shift--;return e}readBits(e){let t,i=0;for(t=e-1;t>=0;t--)i|=this.readBit()<=this.end?-1:this.data[this.position++]}}function getCustomHuffmanTable(e,t,i){let a=0;for(let s=0,r=t.length;s>i&1;i--}}if(a&&!g){const e=5;for(let t=0;t>2,c=new Uint32Array(e.buffer,t,o);if(FeatureTest.isLittleEndian){for(;n>>24|t<<8|4278190080;i[a+2]=t>>>16|s<<16|4278190080;i[a+3]=s>>>8|4278190080}for(let s=4*n,r=t+g;s>>8|255;i[a+2]=t<<16|s>>>16|255;i[a+3]=s<<8|255}for(let s=4*n,r=t+g;s>3,h=7&a,l=e.length;i=new Uint32Array(i.buffer);let Q=0;for(let a=0;a0&&!e[r-1];)r--;const n=[{children:[],index:0}];let g,o=n[0];for(i=0;i0;)o=n.pop();o.index++;n.push(o);for(;n.length<=i;){n.push(g={children:[],index:0});o.children[o.index]=g.children;o=g}s++}if(i+10){E--;return Q>>E&1}Q=e[t++];if(255===Q){const a=e[t++];if(a){if(220===a&&c){const a=readUint16(e,t+=2);t+=2;if(a>0&&a!==i.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",a)}else if(217===a){if(c){const e=p*(8===i.precision?8:0);if(e>0&&Math.round(i.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(Q<<8|a).toString(16)}`)}}E=7;return Q>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){u--;return}let i=r;const a=n;for(;i<=a;){const a=decodeHuffman(e.huffmanTableAC),s=15&a,r=a>>4;if(0===s){if(r<15){u=receive(r)+(1<>4;if(0===s)if(c<15){u=receive(c)+(1<>4;if(0===a){if(r<15)break;s+=16;continue}s+=r;const n=Wt[s];e.blockData[t+n]=receiveAndExtend(a);s++}};let k,R=0;const N=1===m?a[0].blocksPerLine*a[0].blocksPerColumn:C*i.mcusPerColumn;let G,M;for(;R<=N;){const i=s?Math.min(N-R,s):N;if(i>0){for(w=0;w0?"unexpected":"excessive"} MCU data, current marker is: ${k.invalid}`);t=k.offset}if(!(k.marker>=65488&&k.marker<=65495))break;t+=2}return t-l}function quantizeAndInverse(e,t,i){const a=e.quantizationTable,s=e.blockData;let r,n,g,o,c,C,h,l,Q,E,u,d,f,p,m,y,w;if(!a)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){Q=s[t+e];E=s[t+e+1];u=s[t+e+2];d=s[t+e+3];f=s[t+e+4];p=s[t+e+5];m=s[t+e+6];y=s[t+e+7];Q*=a[e];if(E|u|d|f|p|m|y){E*=a[e+1];u*=a[e+2];d*=a[e+3];f*=a[e+4];p*=a[e+5];m*=a[e+6];y*=a[e+7];r=$t*Q+128>>8;n=$t*f+128>>8;g=u;o=m;c=Ai*(E-y)+128>>8;l=Ai*(E+y)+128>>8;C=d<<4;h=p<<4;r=r+n+1>>1;n=r-n;w=g*_t+o*zt+128>>8;g=g*zt-o*_t+128>>8;o=w;c=c+h+1>>1;h=c-h;l=l+C+1>>1;C=l-C;r=r+o+1>>1;o=r-o;n=n+g+1>>1;g=n-g;w=c*Vt+l*Zt+2048>>12;c=c*Zt-l*Vt+2048>>12;l=w;w=C*Xt+h*jt+2048>>12;C=C*jt-h*Xt+2048>>12;h=w;i[e]=r+l;i[e+7]=r-l;i[e+1]=n+h;i[e+6]=n-h;i[e+2]=g+C;i[e+5]=g-C;i[e+3]=o+c;i[e+4]=o-c}else{w=$t*Q+512>>10;i[e]=w;i[e+1]=w;i[e+2]=w;i[e+3]=w;i[e+4]=w;i[e+5]=w;i[e+6]=w;i[e+7]=w}}for(let e=0;e<8;++e){Q=i[e];E=i[e+8];u=i[e+16];d=i[e+24];f=i[e+32];p=i[e+40];m=i[e+48];y=i[e+56];if(E|u|d|f|p|m|y){r=$t*Q+2048>>12;n=$t*f+2048>>12;g=u;o=m;c=Ai*(E-y)+2048>>12;l=Ai*(E+y)+2048>>12;C=d;h=p;r=4112+(r+n+1>>1);n=r-n;w=g*_t+o*zt+2048>>12;g=g*zt-o*_t+2048>>12;o=w;c=c+h+1>>1;h=c-h;l=l+C+1>>1;C=l-C;r=r+o+1>>1;o=r-o;n=n+g+1>>1;g=n-g;w=c*Vt+l*Zt+2048>>12;c=c*Zt-l*Vt+2048>>12;l=w;w=C*Xt+h*jt+2048>>12;C=C*jt-h*Xt+2048>>12;h=w;Q=r+l;y=r-l;E=n+h;m=n-h;u=g+C;p=g-C;d=o+c;f=o-c;Q<16?Q=0:Q>=4080?Q=255:Q>>=4;E<16?E=0:E>=4080?E=255:E>>=4;u<16?u=0:u>=4080?u=255:u>>=4;d<16?d=0:d>=4080?d=255:d>>=4;f<16?f=0:f>=4080?f=255:f>>=4;p<16?p=0:p>=4080?p=255:p>>=4;m<16?m=0:m>=4080?m=255:m>>=4;y<16?y=0:y>=4080?y=255:y>>=4;s[t+e]=Q;s[t+e+8]=E;s[t+e+16]=u;s[t+e+24]=d;s[t+e+32]=f;s[t+e+40]=p;s[t+e+48]=m;s[t+e+56]=y}else{w=$t*Q+8192>>14;w=w<-2040?0:w>=2024?255:w+2056>>4;s[t+e]=w;s[t+e+8]=w;s[t+e+16]=w;s[t+e+24]=w;s[t+e+32]=w;s[t+e+40]=w;s[t+e+48]=w;s[t+e+56]=w}}}function buildComponentData(e,t){const i=t.blocksPerLine,a=t.blocksPerColumn,s=new Int16Array(64);for(let e=0;e=a)return null;const r=readUint16(e,t);if(r>=65472&&r<=65534)return{invalid:null,marker:r,offset:t};let n=readUint16(e,s);for(;!(n>=65472&&n<=65534);){if(++s>=a)return null;n=readUint16(e,s)}return{invalid:r.toString(16),marker:n,offset:s}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),i=Math.ceil(e.scanLines/8/e.maxV);for(const a of e.components){const s=Math.ceil(Math.ceil(e.samplesPerLine/8)*a.h/e.maxH),r=Math.ceil(Math.ceil(e.scanLines/8)*a.v/e.maxV),n=t*a.h,g=64*(i*a.v)*(n+1);a.blockData=new Int16Array(g);a.blocksPerLine=s;a.blocksPerColumn=r}e.mcusPerLine=t;e.mcusPerColumn=i}function readDataBlock(e,t){const i=readUint16(e,t);let a=(t+=2)+i-2;const s=findNextFileMarker(e,a,t);if(s?.invalid){warn("readDataBlock - incorrect length, current marker is: "+s.invalid);a=s.offset}const r=e.subarray(t,a);return{appData:r,newOffset:t+=r.length}}function skipData(e,t){const i=readUint16(e,t),a=(t+=2)+i-2,s=findNextFileMarker(e,a,t);return s?.invalid?s.offset:a}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){let i=0,a=null,s=readUint16(e,i);i+=2;if(65496!==s)throw new JpegError("SOI not found");s=readUint16(e,i);i+=2;A:for(;65497!==s;){switch(s){case 65472:case 65473:case 65474:a=e[i+7];break A;case 65535:255!==e[i]&&i--}i=skipData(e,i);s=readUint16(e,i);i+=2}return 4!==a&&(3!==a||0!==t)}parse(e,{dnlScanLines:t=null}={}){let i,a,s=0,r=null,n=null,g=0;const o=[],c=[],C=[];let h=readUint16(e,s);s+=2;if(65496!==h)throw new JpegError("SOI not found");h=readUint16(e,s);s+=2;A:for(;65497!==h;){let l,Q,E;switch(h){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:u,newOffset:d}=readDataBlock(e,s);s=d;65504===h&&74===u[0]&&70===u[1]&&73===u[2]&&70===u[3]&&0===u[4]&&(r={version:{major:u[5],minor:u[6]},densityUnits:u[7],xDensity:u[8]<<8|u[9],yDensity:u[10]<<8|u[11],thumbWidth:u[12],thumbHeight:u[13],thumbData:u.subarray(14,14+3*u[12]*u[13])});65518===h&&65===u[0]&&100===u[1]&&111===u[2]&&98===u[3]&&101===u[4]&&(n={version:u[5]<<8|u[6],flags0:u[7]<<8|u[8],flags1:u[9]<<8|u[10],transformCode:u[11]});break;case 65499:const f=readUint16(e,s);s+=2;const p=f+s-2;let m;for(;s>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(Q=0;Q<64;Q++){m=Wt[Q];i[m]=readUint16(e,s);s+=2}}else for(Q=0;Q<64;Q++){m=Wt[Q];i[m]=e[s++]}o[15&t]=i}break;case 65472:case 65473:case 65474:if(i)throw new JpegError("Only single frame JPEGs supported");s+=2;i={};i.extended=65473===h;i.progressive=65474===h;i.precision=e[s++];const y=readUint16(e,s);s+=2;i.scanLines=t||y;i.samplesPerLine=readUint16(e,s);s+=2;i.components=[];i.componentIds={};const w=e[s++];let D=0,b=0;for(l=0;l>4,r=15&e[s+1];D>4?c:C)[15&t]=buildHuffmanTable(i,r)}break;case 65501:s+=2;a=readUint16(e,s);s+=2;break;case 65498:const S=1==++g&&!t;s+=2;const k=e[s++],R=[];for(l=0;l>4];r.huffmanTableAC=c[15&n];R.push(r)}const N=e[s++],G=e[s++],M=e[s++];try{s+=decodeScan(e,s,i,R,a,N,G,M>>4,15&M,S)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break A}throw t}break;case 65500:s+=4;break;case 65535:255!==e[s]&&s--;break;default:const U=findNextFileMarker(e,s-2,s-3);if(U?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+U.invalid);s=U.offset;break}if(!U||s>=e.length-1){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break A}throw new JpegError("JpegImage.parse - unknown marker: "+h.toString(16))}h=readUint16(e,s);s+=2}if(!i)throw new JpegError("JpegImage.parse - no frame data found.");this.width=i.samplesPerLine;this.height=i.scanLines;this.jfif=r;this.adobe=n;this.components=[];for(const e of i.components){const t=o[e.quantizationId];t&&(e.quantizationTable=t);this.components.push({index:e.index,output:buildComponentData(0,e),scaleX:e.h/i.maxH,scaleY:e.v/i.maxV,blocksPerLine:e.blocksPerLine,blocksPerColumn:e.blocksPerColumn})}this.numComponents=this.components.length}_getLinearizedBlockData(e,t,i=!1){const a=this.width/e,s=this.height/t;let r,n,g,o,c,C,h,l,Q,E,u,d=0;const f=this.components.length,p=e*t*f,m=new Uint8ClampedArray(p),y=new Uint32Array(e),w=4294967288;let D;for(h=0;h>8)+b[Q+1];return m}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,i,a;for(let s=0,r=e.length;s4)throw new JpegError("Unsupported color mode");const r=this._getLinearizedBlockData(e,t,s);if(1===this.numComponents&&(i||a)){const e=r.length*(i?4:3),t=new Uint8ClampedArray(e);let a=0;if(i)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let i=0,a=e.length;i0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#w(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const i=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=i;this.bufferLength=i.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const i=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!i)return null;const a=this.#w(i);if(!JpegImage.canUseImageDecoder(a,e.colorTransform))return null;t=new ImageDecoder({data:a,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}}var ei,ti=(ei="undefined"!=typeof document?document.currentScript?.src:void 0,function(e={}){var t,i,a=e;new Promise(((e,a)=>{t=e;i=a}));a.decode=function(e,{numComponents:t=4,isIndexedColormap:i=!1,smaskInData:s=!1}){const r=e.length,n=a._malloc(r);a.HEAPU8.set(e,n);const g=a._jp2_decode(n,r,t>0?t:0,!!i,!!s);a._free(n);if(g){const{errorMessages:e}=a;if(e){delete a.errorMessages;return e}return"Unknown error"}const{imageData:o}=a;a.imageData=null;return o};var s=Object.assign({},a),r="./this.program",quit_=(e,t)=>{throw t},n="";"undefined"!=typeof document&&document.currentScript&&(n=document.currentScript.src);ei&&(n=ei);n=n.startsWith("blob:")?"":n.substr(0,n.replace(/[?#].*/,"").lastIndexOf("/")+1);var g=a.print||console.log.bind(console),o=a.printErr||console.error.bind(console);Object.assign(a,s);s=null;a.arguments&&a.arguments;a.thisProgram&&(r=a.thisProgram);var c,C=a.wasmBinary;function tryParseAsDataURI(e){if(isDataURI(e))return function intArrayFromBase64(e){for(var t=atob(e),i=new Uint8Array(t.length),a=0;ae.startsWith(b);function instantiateSync(e,t){var i,a=function getBinarySync(e){if(e==d&&C)return new Uint8Array(C);var t=tryParseAsDataURI(e);if(t)return t;throw'sync fetching of the wasm failed: you can preload it to Module["wasmBinary"] manually, or emcc.py will do that for you when generating HTML (but not JS)'}(e);i=new WebAssembly.Module(a);return[new WebAssembly.Instance(i,t),i]}class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var F,callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(a)},S=a.noExitRuntime||!0,k=0,R={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return h;quit_(0,e)},keepRuntimeAlive=()=>S||k>0,_proc_exit=e=>{h=e;if(!keepRuntimeAlive()){a.onExit?.(e);u=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{h=e;_proc_exit(e)},callUserCallback=e=>{if(!u)try{e();(()=>{if(!keepRuntimeAlive())try{_exit(h)}catch(e){handleException(e)}})()}catch(e){handleException(e)}},growMemory=e=>{var t=(e-c.buffer.byteLength+65535)/65536|0;try{c.grow(t);updateMemoryViews();return 1}catch(e){}},N={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:r||"./this.program"};for(var t in N)void 0===N[t]?delete e[t]:e[t]=N[t];var i=[];for(var t in e)i.push(`${t}=${e[t]}`);getEnvStrings.strings=i}return getEnvStrings.strings},G=[null,[],[]],M="undefined"!=typeof TextDecoder?new TextDecoder:void 0,UTF8ArrayToString=(e,t=0,i=NaN)=>{for(var a=t+i,s=t;e[s]&&!(s>=a);)++s;if(s-t>16&&e.buffer&&M)return M.decode(e.subarray(t,s));for(var r="";t>10,56320|1023&c)}}else r+=String.fromCharCode((31&n)<<6|g)}else r+=String.fromCharCode(n)}return r},printChar=(e,t)=>{var i=G[e];if(0===t||10===t){(1===e?g:o)(UTF8ArrayToString(i));i.length=0}else i.push(t)},UTF8ToString=(e,t)=>e?UTF8ArrayToString(Q,e,t):"",U={m:()=>function abort(e){a.onAbort?.(e);o(e="Aborted("+e+")");u=!0;e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);i(t);throw t}(""),c:(e,t,i)=>Q.copyWithin(e,t,t+i),l:()=>{S=!1;k=0},n:(e,t)=>{if(R[e]){clearTimeout(R[e].id);delete R[e]}if(!t)return 0;var i=setTimeout((()=>{delete R[e];callUserCallback((()=>L(e,performance.now())))}),t);R[e]={id:i,timeout_ms:t};return 0},g:function _copy_pixels_1(e,t){e>>=2;const i=a.imageData=new Uint8ClampedArray(t),s=a.HEAP32.subarray(e,e+t);i.set(s)},f:function _copy_pixels_3(e,t,i,s){e>>=2;t>>=2;i>>=2;const r=a.imageData=new Uint8ClampedArray(3*s),n=a.HEAP32.subarray(e,e+s),g=a.HEAP32.subarray(t,t+s),o=a.HEAP32.subarray(i,i+s);for(let e=0;e>=2;t>>=2;i>>=2;s>>=2;const n=a.imageData=new Uint8ClampedArray(4*r),g=a.HEAP32.subarray(e,e+r),o=a.HEAP32.subarray(t,t+r),c=a.HEAP32.subarray(i,i+r),C=a.HEAP32.subarray(s,s+r);for(let e=0;e{var t,i,a=Q.length,s=2147483648;if((e>>>=0)>s)return!1;for(var r=1;r<=4;r*=2){var n=a*(1+.2/r);n=Math.min(n,e+100663296);var g=Math.min(s,(t=Math.max(e,n),i=65536,Math.ceil(t/i)*i));if(growMemory(g))return!0}return!1},p:(e,t)=>{var i=0;getEnvStrings().forEach(((a,s)=>{var r=t+i;E[e+4*s>>2]=r;((e,t)=>{for(var i=0;i{var i=getEnvStrings();E[e>>2]=i.length;var a=0;i.forEach((e=>a+=e.length+1));E[t>>2]=a;return 0},r:e=>52,j:function _fd_seek(e,t,i,a,s){return 70},b:(e,t,i,a)=>{for(var s=0,r=0;r>2],g=E[t+4>>2];t+=8;for(var o=0;o>2]=s;return 0},s:function _gray_to_rgba(e,t){e>>=2;const i=a.imageData=new Uint8ClampedArray(4*t),s=a.HEAP32.subarray(e,e+t);for(let e=0;e>=2;t>>=2;const s=a.imageData=new Uint8ClampedArray(4*i),r=a.HEAP32.subarray(e,e+i),n=a.HEAP32.subarray(t,t+i);for(let e=0;e>=2;t>>=2;i>>=2;const r=a.imageData=new Uint8ClampedArray(4*s),n=a.HEAP32.subarray(e,e+s),g=a.HEAP32.subarray(t,t+s),o=a.HEAP32.subarray(i,i+s);for(let e=0;e0)){!function preRun(){if(a.preRun){"function"==typeof a.preRun&&(a.preRun=[a.preRun]);for(;a.preRun.length;)e=a.preRun.shift(),f.unshift(e)}var e;callRuntimeCallbacks(f)}();if(!(y>0))if(a.setStatus){a.setStatus("Running...");setTimeout((()=>{setTimeout((()=>a.setStatus("")),1);doRun()}),1)}else doRun()}function doRun(){if(!F){F=!0;a.calledRun=!0;if(!u){!function initRuntime(){callRuntimeCallbacks(p)}();t(a);a.onRuntimeInitialized?.();!function postRun(){if(a.postRun){"function"==typeof a.postRun&&(a.postRun=[a.postRun]);for(;a.postRun.length;)e=a.postRun.shift(),m.unshift(e)}var e;callRuntimeCallbacks(m)}()}}}}if(a.preInit){"function"==typeof a.preInit&&(a.preInit=[a.preInit]);for(;a.preInit.length>0;)a.preInit.pop()()}run();return a});const ii=ti;class JpxError extends ot{constructor(e){super(e,"JpxError")}}class JpxImage{static#D=null;static decode(e,t){t||={};this.#D||=ii({warn});const i=this.#D.decode(e,t);if("string"==typeof i)throw new JpxError(i);return i}static cleanup(){this.#D=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const i=t;t=e.getByte();if(65361===(i<<8|t)){e.skip(4);const t=e.getInt32()>>>0,i=e.getInt32()>>>0,a=e.getInt32()>>>0,s=e.getInt32()>>>0;e.skip(16);return{width:t-a,height:i-s,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}class JpxStream extends DecodeStream{constructor(e,t,i){super(t);this.stream=e;this.dict=e.dict;this.maybeLength=t;this.params=i}get bytes(){return shadow(this,"bytes",this.stream.getBytes(this.maybeLength))}ensureBuffer(e){}readBlock(e){this.decodeImage(null,e)}decodeImage(e,t){if(this.eof)return this.buffer;e||=this.bytes;this.buffer=JpxImage.decode(e,t);this.bufferLength=this.buffer.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}}class LZWStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;this.cachedData=0;this.bitsCached=0;const a=4096,s={earlyChange:i,codeLength:9,nextCode:258,dictionaryValues:new Uint8Array(a),dictionaryLengths:new Uint16Array(a),dictionaryPrevCodes:new Uint16Array(a),currentSequence:new Uint8Array(a),currentSequenceLength:0};for(let e=0;e<256;++e){s.dictionaryValues[e]=e;s.dictionaryLengths[e]=1}this.lzwState=s}readBits(e){let t=this.bitsCached,i=this.cachedData;for(;t>>t&(1<0;if(e<256){l[0]=e;Q=1}else{if(!(e>=258)){if(256===e){C=9;n=258;Q=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){l[t]=g[i];i=c[i]}}else l[Q++]=l[0]}if(s){c[n]=h;o[n]=o[h]+1;g[n]=l[0];n++;C=n+r&n+r-1?C:0|Math.min(Math.log(n+r)/.6931471805599453+1,12)}h=e;E+=Q;if(a15))throw new FormatError(`Unsupported predictor: ${a}`);this.readBlock=2===a?this.readBlockTiff:this.readBlockPng;this.str=e;this.dict=e.dict;const s=this.colors=i.get("Colors")||1,r=this.bits=i.get("BPC","BitsPerComponent")||8,n=this.columns=i.get("Columns")||1;this.pixBytes=s*r+7>>3;this.rowBytes=n*s*r+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,i=this.ensureBuffer(t+e),a=this.bits,s=this.colors,r=this.str.getBytes(e);this.eof=!r.length;if(this.eof)return;let n,g=0,o=0,c=0,C=0,h=t;if(1===a&&1===s)for(n=0;n>1;e^=e>>2;e^=e>>4;g=(1&e)<<7;i[h++]=e}else if(8===a){for(n=0;n>8&255;i[h++]=255&e}}else{const e=new Uint8Array(s+1),h=(1<>c-a)&h;c-=a;o=o<=8){i[Q++]=o>>C-8&255;C-=8}}C>0&&(i[Q++]=(o<<8-C)+(g&(1<<8-C)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,i=this.str.getByte(),a=this.str.getBytes(e);this.eof=!a.length;if(this.eof)return;const s=this.bufferLength,r=this.ensureBuffer(s+e);let n=r.subarray(s-e,s);0===n.length&&(n=new Uint8Array(e));let g,o,c,C=s;switch(i){case 0:for(g=0;g>1)+a[g];for(;g>1)+a[g]&255;C++}break;case 4:for(g=0;g0){const e=this.str.getBytes(a);t.set(e,i);i+=a}}else{a=257-a;const s=e[1];t=this.ensureBuffer(i+a+1);for(let e=0;e>")&&this.buf1!==Bt;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===Bt)break;a.set(t,this.getObj(e))}if(this.buf1===Bt){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(a,e):a;this.shift();return a;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,i=e.pos;let a,s,r=0;for(;-1!==(a=e.getByte());)if(0===r)r=69===a?1:0;else if(1===r)r=73===a?2:0;else if(32===a||10===a||13===a){s=e.pos;const i=e.peekBytes(15),n=i.length;if(0===n)break;for(let e=0;e127))){r=0;break}}if(2!==r)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const g=new Lexer(new Stream(i.slice()),t);g._hexStringWarn=()=>{};let o=0;for(;;){const e=g.getObj();if(e===Bt){r=0;break}if(e instanceof Cmd){const i=t[e.cmd];if(!i){r=0;break}if(i.variableArgs?o<=i.numArgs:o===i.numArgs)break;o=0}else o++}if(2===r)break}else r=0;if(-1===a){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(s){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-s))}}let n=4;e.skip(-n);a=e.peekByte();e.skip(n);isWhiteSpace(a)||n--;return e.pos-n-i}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let i,a,s=!1;for(;-1!==(i=e.getByte());)if(255===i){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:s=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:a=e.getUint16();a>2?e.skip(a-2):e.skip(-2)}if(s)break}const r=e.pos-t;if(-1===i){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let i;for(;-1!==(i=e.getByte());)if(126===i){const t=e.pos;i=e.peekByte();for(;isWhiteSpace(i);){e.skip();i=e.peekByte()}if(62===i){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const a=e.pos-t;if(-1===i){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-a);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return a}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let i;for(;-1!==(i=e.getByte())&&62!==i;);const a=e.pos-t;if(-1===i){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-a);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return a}inlineStreamSkipEI(e){let t,i=0;for(;-1!==(t=e.getByte());)if(0===i)i=69===t?1:0;else if(1===i)i=73===t?2:0;else if(2===i)break}makeInlineImage(e){const t=this.lexer,i=t.stream,a=Object.create(null);let s;for(;!isCmd(this.buf1,"ID")&&this.buf1!==Bt;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===Bt)break;a[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(s=i.pos-t.beginInlineImagePos);const r=this.xref.fetchIfRef(a.F||a.Filter);let n;if(r instanceof Name)n=r.name;else if(Array.isArray(r)){const e=this.xref.fetchIfRef(r[0]);e instanceof Name&&(n=e.name)}const g=i.pos;let o,c;switch(n){case"DCT":case"DCTDecode":o=this.findDCTDecodeInlineStreamEnd(i);break;case"A85":case"ASCII85Decode":o=this.findASCII85DecodeInlineStreamEnd(i);break;case"AHx":case"ASCIIHexDecode":o=this.findASCIIHexDecodeInlineStreamEnd(i);break;default:o=this.findDefaultInlineStreamEnd(i)}if(o<1e3&&s>0){const e=i.pos;i.pos=t.beginInlineImagePos;c=function getInlineImageCacheKey(e){const t=[],i=e.length;let a=0;for(;a=a){let a=!1;for(const e of s){const t=e.length;let s=0;for(;s=r){a=!0;break}if(s>=t){if(isWhiteSpace(n[o+g+s])){info(`Found "${bytesToString([...i,...e])}" when searching for endstream command.`);a=!0}break}}if(a){t.pos+=o;return t.pos-e}}o++}t.pos+=g}return-1}makeStream(e,t){const i=this.lexer;let a=i.stream;i.skipToNextLine();const s=a.pos-1;let r=e.get("Length");if(!Number.isInteger(r)){info(`Bad length "${r&&r.toString()}" in stream.`);r=0}a.pos=s+r;i.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{r=this.#b(s);if(r<0)throw new FormatError("Missing endstream command.");i.nextChar();this.shift();this.shift()}this.shift();a=a.makeSubStream(s,r,e);t&&(a=t.createStream(a,r));a=this.filter(a,e,r);a.dict=e;return a}filter(e,t,i){let a=t.get("F","Filter"),s=t.get("DP","DecodeParms");if(a instanceof Name){Array.isArray(s)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,a.name,i,s)}let r=i;if(Array.isArray(a)){const t=a,i=s;for(let n=0,g=t.length;n=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=!1,i=0,a=1;if(45===e){a=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){i=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let s=e-48,r=0,n=1;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){const a=e-48;if(t)r=10*r+a;else{0!==i&&(i*=10);s=10*s+a}}else if(46===e){if(0!==i)break;i=1}else if(45===e)warn("Badly formatted number: minus sign in the middle");else{if(69!==e&&101!==e)break;e=this.peekChar();if(43===e||45===e){n=45===e?-1:1;this.nextChar()}else if(e<48||e>57)break;t=!0}0!==i&&(s/=i);t&&(s*=10**(n*r));return a*s}getString(){let e=1,t=!1;const i=this.strBuf;i.length=0;let a=this.nextChar();for(;;){let s=!1;switch(0|a){case-1:warn("Unterminated string");t=!0;break;case 40:++e;i.push("(");break;case 41:if(0==--e){this.nextChar();t=!0}else i.push(")");break;case 92:a=this.nextChar();switch(a){case-1:warn("Unterminated string");t=!0;break;case 110:i.push("\n");break;case 114:i.push("\r");break;case 116:i.push("\t");break;case 98:i.push("\b");break;case 102:i.push("\f");break;case 92:case 40:case 41:i.push(String.fromCharCode(a));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&a;a=this.nextChar();s=!0;if(a>=48&&a<=55){e=(e<<3)+(15&a);a=this.nextChar();if(a>=48&&a<=55){s=!1;e=(e<<3)+(15&a)}}i.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:i.push(String.fromCharCode(a))}break;default:i.push(String.fromCharCode(a))}if(t)break;s||(a=this.nextChar())}return i.join("")}getName(){let e,t;const i=this.strBuf;i.length=0;for(;(e=this.nextChar())>=0&&!ai[e];)if(35===e){e=this.nextChar();if(ai[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");i.push("#");break}const a=toHexDigit(e);if(-1!==a){t=e;e=this.nextChar();const s=toHexDigit(e);if(-1===s){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);i.push("#",String.fromCharCode(t));if(ai[e])break;i.push(String.fromCharCode(e));continue}i.push(String.fromCharCode(a<<4|s))}else i.push("#",String.fromCharCode(e))}else i.push(String.fromCharCode(e));i.length>127&&warn(`Name token is longer than allowed by the spec: ${i.length}`);return Name.get(i.join(""))}_hexStringWarn(e){5!=this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,i=-1,a=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==ai[t]){a=toHexDigit(t);if(-1===a)this._hexStringWarn(t);else if(-1===i)i=a;else{e.push(String.fromCharCode(i<<4|a));i=-1}t=this.nextChar()}else t=this.nextChar()}-1!==i&&e.push(String.fromCharCode(i<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return Bt;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==ai[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let i=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(i)}}const a=this.knownCommands;let s=void 0!==a?.[i];for(;(t=this.nextChar())>=0&&!ai[t];){const e=i+String.fromCharCode(t);if(s&&void 0===a[e])break;if(128===i.length)throw new FormatError(`Command token too long: ${i.length}`);i=e;s=void 0!==a?.[i]}if("true"===i)return!0;if("false"===i)return!1;if("null"===i)return null;"BI"===i&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(i)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,i=!1){const a=e.get(t);if(Number.isInteger(a)&&(i?a>=0:a>0))return a;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),i=t.getObj(),a=t.getObj(),s=t.getObj(),r=t.getObj();let n,g;if(!(Number.isInteger(i)&&Number.isInteger(a)&&isCmd(s,"obj")&&r instanceof Dict&&"number"==typeof(n=r.get("Linearized"))&&n>0))return null;if((g=getInt(r,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:g,hints:function getHints(e){const t=e.get("H");let i;if(Array.isArray(t)&&(2===(i=t.length)||4===i)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(r),objectNumberFirst:getInt(r,"O"),endFirst:getInt(r,"E"),numPages:getInt(r,"N"),mainXRefEntriesOffset:getInt(r,"T"),pageFirst:r.has("P")?getInt(r,"P",!0):0}}}const si=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],ri=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,i){this.codespaceRanges[e-1].push(t,i);this.numCodespaceRanges++}mapCidRange(e,t,i){if(t-e>ri)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=i++}mapBfRange(e,t,i){if(t-e>ri)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const a=i.length-1;for(;e<=t;){this._map[e++]=i;const t=i.charCodeAt(a)+1;t>255?i=i.substring(0,a-1)+String.fromCharCode(i.charCodeAt(a-1)+1)+"\0":i=i.substring(0,a)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,i){if(t-e>ri)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const a=i.length;let s=0;for(;e<=t&&s>>0;const n=s[r];for(let e=0,t=n.length;e=t&&a<=s){i.charcode=a;i.length=r+1;return}}}i.charcode=0;i.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let i=0,a=t.length;i=s&&e<=r)return i+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,i){unreachable("should not call mapCidRange")}mapBfRange(e,t,i){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,i){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let i=0;i>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endbfchar"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=i;e.mapOne(a,s)}}function parseBfRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endbfrange"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=strToInt(i);i=t.getObj();if(Number.isInteger(i)||"string"==typeof i){const t=Number.isInteger(i)?String.fromCharCode(i):i;e.mapBfRange(a,s,t)}else{if(!isCmd(i,"["))break;{i=t.getObj();const r=[];for(;!isCmd(i,"]")&&i!==Bt;){r.push(i);i=t.getObj()}e.mapBfRangeToArray(a,s,r)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcidchar"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectInt(i);const s=i;e.mapOne(a,s)}}function parseCidRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcidrange"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=strToInt(i);i=t.getObj();expectInt(i);const r=i;e.mapCidRange(a,s,r)}}function parseCodespaceRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcodespacerange"))return;if("string"!=typeof i)break;const a=strToInt(i);i=t.getObj();if("string"!=typeof i)break;const s=strToInt(i);e.addCodespaceRange(i.length,a,s)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const i=t.getObj();Number.isInteger(i)&&(e.vertical=!!i)}function parseCMapName(e,t){const i=t.getObj();i instanceof Name&&(e.name=i.name)}async function parseCMap(e,t,i,a){let s,r;A:for(;;)try{const i=t.getObj();if(i===Bt)break;if(i instanceof Name){"WMode"===i.name?parseWMode(e,t):"CMapName"===i.name&&parseCMapName(e,t);s=i}else if(i instanceof Cmd)switch(i.cmd){case"endcmap":break A;case"usecmap":s instanceof Name&&(r=s.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!a&&r&&(a=r);return a?extendCMap(e,i,a):e}async function extendCMap(e,t,i){e.useCMap=await createBuiltInCMap(i,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let i=0;iextendCMap(s,t,e)));const r=new Lexer(new Stream(i));return parseCMap(s,r,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:i}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){const a=await parseCMap(new CMap,new Lexer(e),t,i);return a.isIdentityCMap?createBuiltInCMap(a.name,t):a}throw new Error("Encoding required.")}}const ni=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],gi=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],oi=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],Ii=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","","asuperior","bsuperior","centsuperior","dsuperior","esuperior","","","","isuperior","","","lsuperior","msuperior","nsuperior","osuperior","","","rsuperior","ssuperior","tsuperior","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdownsmall","centoldstyle","Lslashsmall","","","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","","Dotaccentsmall","","","Macronsmall","","","figuredash","hypheninferior","","","Ogoneksmall","Ringsmall","Cedillasmall","","","","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],ci=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","centoldstyle","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","","threequartersemdash","","questionsmall","","","","","Ethsmall","","","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","","","","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hypheninferior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","asuperior","centsuperior","","","","","Aacutesmall","Agravesmall","Acircumflexsmall","Adieresissmall","Atildesmall","Aringsmall","Ccedillasmall","Eacutesmall","Egravesmall","Ecircumflexsmall","Edieresissmall","Iacutesmall","Igravesmall","Icircumflexsmall","Idieresissmall","Ntildesmall","Oacutesmall","Ogravesmall","Ocircumflexsmall","Odieresissmall","Otildesmall","Uacutesmall","Ugravesmall","Ucircumflexsmall","Udieresissmall","","eightsuperior","fourinferior","threeinferior","sixinferior","eightinferior","seveninferior","Scaronsmall","","centinferior","twoinferior","","Dieresissmall","","Caronsmall","osuperior","fiveinferior","","commainferior","periodinferior","Yacutesmall","","dollarinferior","","","Thornsmall","","nineinferior","zeroinferior","Zcaronsmall","AEsmall","Oslashsmall","questiondownsmall","oneinferior","Lslashsmall","","","","","","","Cedillasmall","","","","","","OEsmall","figuredash","hyphensuperior","","","","","exclamdownsmall","","Ydieresissmall","","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","ninesuperior","zerosuperior","","esuperior","rsuperior","tsuperior","","","isuperior","ssuperior","dsuperior","","","","","","lsuperior","Ogoneksmall","Brevesmall","Macronsmall","bsuperior","nsuperior","msuperior","commasuperior","periodsuperior","Dotaccentsmall","Ringsmall","","","",""],Ci=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","space","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron"],hi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","","endash","dagger","daggerdbl","periodcentered","","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","","questiondown","","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","","ring","cedilla","","hungarumlaut","ogonek","caron","emdash","","","","","","","","","","","","","","","","","AE","","ordfeminine","","","","","Lslash","Oslash","OE","ordmasculine","","","","","","ae","","","","dotlessi","","","lslash","oslash","oe","germandbls","","","",""],li=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","bullet","Euro","bullet","quotesinglbase","florin","quotedblbase","ellipsis","dagger","daggerdbl","circumflex","perthousand","Scaron","guilsinglleft","OE","bullet","Zcaron","bullet","bullet","quoteleft","quoteright","quotedblleft","quotedblright","bullet","endash","emdash","tilde","trademark","scaron","guilsinglright","oe","bullet","zcaron","Ydieresis","space","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis","copyright","ordfeminine","guillemotleft","logicalnot","hyphen","registered","macron","degree","plusminus","twosuperior","threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright","onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth","Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae","ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis","yacute","thorn","ydieresis"],Bi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","universal","numbersign","existential","percent","ampersand","suchthat","parenleft","parenright","asteriskmath","plus","comma","minus","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","congruent","Alpha","Beta","Chi","Delta","Epsilon","Phi","Gamma","Eta","Iota","theta1","Kappa","Lambda","Mu","Nu","Omicron","Pi","Theta","Rho","Sigma","Tau","Upsilon","sigma1","Omega","Xi","Psi","Zeta","bracketleft","therefore","bracketright","perpendicular","underscore","radicalex","alpha","beta","chi","delta","epsilon","phi","gamma","eta","iota","phi1","kappa","lambda","mu","nu","omicron","pi","theta","rho","sigma","tau","upsilon","omega1","omega","xi","psi","zeta","braceleft","bar","braceright","similar","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Euro","Upsilon1","minute","lessequal","fraction","infinity","florin","club","diamond","heart","spade","arrowboth","arrowleft","arrowup","arrowright","arrowdown","degree","plusminus","second","greaterequal","multiply","proportional","partialdiff","bullet","divide","notequal","equivalence","approxequal","ellipsis","arrowvertex","arrowhorizex","carriagereturn","aleph","Ifraktur","Rfraktur","weierstrass","circlemultiply","circleplus","emptyset","intersection","union","propersuperset","reflexsuperset","notsubset","propersubset","reflexsubset","element","notelement","angle","gradient","registerserif","copyrightserif","trademarkserif","product","radical","dotmath","logicalnot","logicaland","logicalor","arrowdblboth","arrowdblleft","arrowdblup","arrowdblright","arrowdbldown","lozenge","angleleft","registersans","copyrightsans","trademarksans","summation","parenlefttp","parenleftex","parenleftbt","bracketlefttp","bracketleftex","bracketleftbt","bracelefttp","braceleftmid","braceleftbt","braceex","","angleright","integral","integraltp","integralex","integralbt","parenrighttp","parenrightex","parenrightbt","bracketrighttp","bracketrightex","bracketrightbt","bracerighttp","bracerightmid","bracerightbt",""],Qi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","a1","a2","a202","a3","a4","a5","a119","a118","a117","a11","a12","a13","a14","a15","a16","a105","a17","a18","a19","a20","a21","a22","a23","a24","a25","a26","a27","a28","a6","a7","a8","a9","a10","a29","a30","a31","a32","a33","a34","a35","a36","a37","a38","a39","a40","a41","a42","a43","a44","a45","a46","a47","a48","a49","a50","a51","a52","a53","a54","a55","a56","a57","a58","a59","a60","a61","a62","a63","a64","a65","a66","a67","a68","a69","a70","a71","a72","a73","a74","a203","a75","a204","a76","a77","a78","a79","a81","a82","a83","a84","a97","a98","a99","a100","","a89","a90","a93","a94","a91","a92","a205","a85","a206","a86","a87","a88","a95","a96","","","","","","","","","","","","","","","","","","","","a101","a102","a103","a104","a106","a107","a108","a112","a111","a110","a109","a120","a121","a122","a123","a124","a125","a126","a127","a128","a129","a130","a131","a132","a133","a134","a135","a136","a137","a138","a139","a140","a141","a142","a143","a144","a145","a146","a147","a148","a149","a150","a151","a152","a153","a154","a155","a156","a157","a158","a159","a160","a161","a163","a164","a196","a165","a192","a166","a167","a168","a169","a170","a171","a172","a173","a162","a174","a175","a176","a177","a178","a179","a193","a180","a199","a181","a200","a182","","a201","a183","a184","a197","a185","a194","a198","a186","a195","a187","a188","a189","a190","a191",""];function getEncoding(e){switch(e){case"WinAnsiEncoding":return li;case"StandardEncoding":return hi;case"MacRomanEncoding":return Ci;case"SymbolSetEncoding":return Bi;case"ZapfDingbatsEncoding":return Qi;case"ExpertEncoding":return Ii;case"MacExpertEncoding":return ci;default:return null}}const Ei=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],ui=391,di=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1,undefStack:!0},{id:"return",min:0,undefStack:!0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1,undefStack:!0},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],fi=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,i){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!i}parse(){const e=this.properties,t=new CFF;this.cff=t;const i=this.parseHeader(),a=this.parseIndex(i.endPos),s=this.parseIndex(a.endPos),r=this.parseIndex(s.endPos),n=this.parseIndex(r.endPos),g=this.parseDict(s.obj.get(0)),o=this.createDict(CFFTopDict,g,t.strings);t.header=i.obj;t.names=this.parseNameIndex(a.obj);t.strings=this.parseStringIndex(r.obj);t.topDict=o;t.globalSubrIndex=n.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=o.hasName("ROS");const c=o.getByName("CharStrings"),C=this.parseIndex(c).obj,h=o.getByName("FontMatrix");h&&(e.fontMatrix=h);const l=o.getByName("FontBBox");if(l){e.ascent=Math.max(l[3],l[1]);e.descent=Math.min(l[1],l[3]);e.ascentScaled=!0}let Q,E;if(t.isCIDFont){const e=this.parseIndex(o.getByName("FDArray")).obj;for(let i=0,a=e.count;i=t)throw new FormatError("Invalid CFF header");if(0!==i){info("cff data is shifted");e=e.subarray(i);this.bytes=e}const a=e[0],s=e[1],r=e[2],n=e[3];return{obj:new CFFHeader(a,s,r,n),endPos:r}}parseDict(e){let t=0;function parseOperand(){let i=e[t++];if(30===i)return function parseFloatOperand(){let i="";const a=15,s=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],r=e.length;for(;t>4,g=15&r;if(n===a)break;i+=s[n];if(g===a)break;i+=s[g]}return parseFloat(i)}();if(28===i){i=e[t++];i=(i<<24|e[t++]<<16)>>16;return i}if(29===i){i=e[t++];i=i<<8|e[t++];i=i<<8|e[t++];i=i<<8|e[t++];return i}if(i>=32&&i<=246)return i-139;if(i>=247&&i<=250)return 256*(i-247)+e[t++]+108;if(i>=251&&i<=254)return-256*(i-251)-e[t++]-108;warn('CFFParser_parseDict: "'+i+'" is a reserved command.');return NaN}let i=[];const a=[];t=0;const s=e.length;for(;t10)return!1;let s=e.stackSize;const r=e.stack;let n=t.length;for(let g=0;g>16;g+=2;s++}else if(14===o){if(s>=4){s-=4;if(this.seacAnalysisEnabled){e.seac=r.slice(s,s+4);return!1}}c=di[o]}else if(o>=32&&o<=246){r[s]=o-139;s++}else if(o>=247&&o<=254){r[s]=o<251?(o-247<<8)+t[g]+108:-(o-251<<8)-t[g]-108;g++;s++}else if(255===o){r[s]=(t[g]<<24|t[g+1]<<16|t[g+2]<<8|t[g+3])/65536;g+=4;s++}else if(19===o||20===o){e.hints+=s>>1;if(0===e.hints){t.copyWithin(g-1,g,-1);g-=1;n-=1;continue}g+=e.hints+7>>3;s%=2;c=di[o]}else{if(10===o||29===o){const t=10===o?i:a;if(!t){c=di[o];warn("Missing subrsIndex for "+c.id);return!1}let n=32768;t.count<1240?n=107:t.count<33900&&(n=1131);const g=r[--s]+n;if(g<0||g>=t.count||isNaN(g)){c=di[o];warn("Out of bounds subrIndex for "+c.id);return!1}e.stackSize=s;e.callDepth++;if(!this.parseCharString(e,t.get(g),i,a))return!1;e.callDepth--;s=e.stackSize;continue}if(11===o){e.stackSize=s;return!0}if(0===o&&g===t.length){t[g-1]=14;c=di[14]}else{if(9===o){t.copyWithin(g-1,g,-1);g-=1;n-=1;continue}c=di[o]}}if(c){if(c.stem){e.hints+=s>>1;if(3===o||23===o)e.hasVStems=!0;else if(e.hasVStems&&(1===o||18===o)){warn("CFF stem hints are in wrong order");t[g-1]=1===o?3:23}}if("min"in c&&!e.undefStack&&s=2&&c.stem?s%=2:s>1&&warn("Found too many parameters for stack-clearing command");s>0&&(e.width=r[s-1])}if("stackDelta"in c){"stackFn"in c&&c.stackFn(r,s);s+=c.stackDelta}else if(c.stackClearing)s=0;else if(c.resetStack){s=0;e.undefStack=!1}else if(c.undefStack){s=0;e.undefStack=!0;e.firstStackClearing=!1}}}n=s.length){warn("Invalid fd index for glyph index.");h=!1}if(h){Q=s[e].privateDict;l=Q.subrsIndex}}else t&&(l=t);h&&(h=this.parseCharString(C,o,l,i));if(null!==C.width){const e=Q.getByName("nominalWidthX");g[c]=e+C.width}else{const e=Q.getByName("defaultWidthX");g[c]=e}null!==C.seac&&(n[c]=C.seac);h||e.set(c,new Uint8Array([14]))}return{charStrings:e,seacs:n,widths:g}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const i=t[0],a=t[1];if(0===i||a>=this.bytes.length){this.emptyPrivateDictionary(e);return}const s=a+i,r=this.bytes.subarray(a,s),n=this.parseDict(r),g=this.createDict(CFFPrivateDict,n,e.strings);e.privateDict=g;0===g.getByName("ExpansionFactor")&&g.setByName("ExpansionFactor",.06);if(!g.getByName("Subrs"))return;const o=g.getByName("Subrs"),c=a+o;if(0===o||c>=this.bytes.length){this.emptyPrivateDictionary(e);return}const C=this.parseIndex(c);g.subrsIndex=C.obj}parseCharsets(e,t,i,a){if(0===e)return new CFFCharset(!0,yi.ISO_ADOBE,ni);if(1===e)return new CFFCharset(!0,yi.EXPERT,gi);if(2===e)return new CFFCharset(!0,yi.EXPERT_SUBSET,oi);const s=this.bytes,r=e,n=s[e++],g=[a?0:".notdef"];let o,c,C;t-=1;switch(n){case 0:for(C=0;C=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,i,a){this.major=e;this.minor=t;this.hdrSize=i;this.offSize=a}}class CFFStrings{constructor(){this.strings=[]}get(e){return e>=0&&e<=390?Ei[e]:e-ui<=this.strings.length?this.strings[e-ui]:Ei[0]}getSID(e){let t=Ei.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+ui:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{constructor(){this.objects=[];this.length=0}add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const i of t)if(isNaN(i)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const i=this.types[e];"num"!==i&&"sid"!==i&&"offset"!==i||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const i of e){const e=Array.isArray(i[0])?(i[0][0]<<8)+i[0][1]:i[0];t.keyToNameMap[e]=i[1];t.nameToKeyMap[i[1]]=e;t.types[e]=i[2];t.defaults[e]=i[3];t.opcodes[e]=Array.isArray(i[0])?i[0]:[i[0]];t.order.push(e)}return t}}const pi=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(pi))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const mi=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(mi))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const yi={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,i,a){this.predefined=e;this.format=t;this.charset=i;this.raw=a}}class CFFEncoding{constructor(e,t,i,a){this.predefined=e;this.format=t;this.encoding=i;this.raw=a}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{constructor(){this.offsets=Object.create(null)}isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,i){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const a=i.data,s=this.offsets[e];for(let e=0,i=t.length;e>24&255;a[n]=c>>16&255;a[g]=c>>8&255;a[o]=255&c}}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t={data:[],length:0,add(e){try{this.data.push(...e)}catch{this.data=this.data.concat(e)}this.length=this.data.length}},i=this.compileHeader(e.header);t.add(i);const a=this.compileNameIndex(e.names);t.add(a);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const i of e.fdArray){let e=t.slice(0);i.hasName("FontMatrix")&&(e=Util.transform(e,i.getByName("FontMatrix")));i.setByName("FontMatrix",e)}}const s=e.topDict.getByName("XUID");s?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let r=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(r.output);const n=r.trackers[0],g=this.compileStringIndex(e.strings.strings);t.add(g);const o=this.compileIndex(e.globalSubrIndex);t.add(o);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)n.setEntryLocation("Encoding",[e.encoding.format],t);else{const i=this.compileEncoding(e.encoding);n.setEntryLocation("Encoding",[t.length],t);t.add(i)}const c=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);n.setEntryLocation("charset",[t.length],t);t.add(c);const C=this.compileCharStrings(e.charStrings);n.setEntryLocation("CharStrings",[t.length],t);t.add(C);if(e.isCIDFont){n.setEntryLocation("FDSelect",[t.length],t);const i=this.compileFDSelect(e.fdSelect);t.add(i);r=this.compileTopDicts(e.fdArray,t.length,!0);n.setEntryLocation("FDArray",[t.length],t);t.add(r.output);const a=r.trackers;this.compilePrivateDicts(e.fdArray,a,t)}this.compilePrivateDicts([e.topDict],[n],t);t.add([0]);return t.data}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const i=CFFCompiler.EncodeFloatRegExp.exec(t);if(i){const a=parseFloat("1e"+((i[2]?+i[2]:0)+i[1].length));t=(Math.round(e*a)/a).toString()}let a,s,r="";for(a=0,s=t.length;a=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const i of e){const e=Math.min(i.length,127);let a=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");a[t]=e}a=a.join("");""===a&&(a="Bad_Font_Name");t.add(stringToBytes(a))}return this.compileIndex(t)}compileTopDicts(e,t,i){const a=[];let s=new CFFIndex;for(const r of e){if(i){r.removeByName("CIDFontVersion");r.removeByName("CIDFontRevision");r.removeByName("CIDFontType");r.removeByName("CIDCount");r.removeByName("UIDBase")}const e=new CFFOffsetTracker,n=this.compileDict(r,e);a.push(e);s.add(n);e.offset(t)}s=this.compileIndex(s,a);return{trackers:a,output:s}}compilePrivateDicts(e,t,i){for(let a=0,s=e.length;a>8&255,255&r]);else{s=new Uint8Array(1+2*r);s[0]=0;let t=0;const a=e.charset.length;let n=!1;for(let r=1;r>8&255;s[r+1]=255&g}}return this.compileTypedArray(s)}compileEncoding(e){return this.compileTypedArray(e.raw)}compileFDSelect(e){const t=e.format;let i,a;switch(t){case 0:i=new Uint8Array(1+e.fdSelect.length);i[0]=t;for(a=0;a>8&255,255&s,r];for(a=1;a>8&255,255&a,t);r=t}}const g=(n.length-3)/3;n[1]=g>>8&255;n[2]=255&g;n.push(a>>8&255,255&a);i=new Uint8Array(n)}return this.compileTypedArray(i)}compileTypedArray(e){return Array.from(e)}compileIndex(e,t=[]){const i=e.objects,a=i.length;if(0===a)return[0,0];const s=[a>>8&255,255&a];let r,n,g=1;for(r=0;r>8&255,255&o):3===n?s.push(o>>16&255,o>>8&255,255&o):s.push(o>>>24&255,o>>16&255,o>>8&255,255&o);i[r]&&(o+=i[r].length)}for(r=0;r=5&&t<=7))return-1;a=e.substring(1)}if(a===a.toUpperCase()){i=parseInt(a,16);if(i>=0)return i}}return-1}const Fi=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const i=Fi[t];for(let a=0,s=i.length;a=i[a]&&e<=i[a+1])return t}for(let t=0,i=Fi.length;t=i[a]&&e<=i[a+1])return t}return-1}const Si=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),ki=new Map;const Ri=!0,Ni=1,Gi=2,Mi=4,xi=32,Hi=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const i=getUnicodeForGlyph(e,t);if(-1!==i)for(const e in t)if(t[e]===i)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,i){const a=Object.create(null);let s,r,n;const g=!!(e.flags&Mi);if(e.isInternalFont){n=t;for(r=0;r=0?s:0}}else if(e.baseEncodingName){n=getEncoding(e.baseEncodingName);for(r=0;r=0?s:0}}else if(g)for(r in t)a[r]=t[r];else{n=hi;for(r=0;r=0?s:0}}const o=e.differences;let c;if(o)for(r in o){const e=o[r];s=i.indexOf(e);if(-1===s){c||(c=wi());const t=recoverGlyphName(e,c);t!==e&&(s=i.indexOf(t))}a[r]=s>=0?s:0}return a}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const Ji=getLookupTableFactory((e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080})),Yi=getLookupTableFactory((function(e){e["Times-Roman"]="Times-Roman";e.Helvetica="Helvetica";e.Courier="Courier";e.Symbol="Symbol";e["Times-Bold"]="Times-Bold";e["Helvetica-Bold"]="Helvetica-Bold";e["Courier-Bold"]="Courier-Bold";e.ZapfDingbats="ZapfDingbats";e["Times-Italic"]="Times-Italic";e["Helvetica-Oblique"]="Helvetica-Oblique";e["Courier-Oblique"]="Courier-Oblique";e["Times-BoldItalic"]="Times-BoldItalic";e["Helvetica-BoldOblique"]="Helvetica-BoldOblique";e["Courier-BoldOblique"]="Courier-BoldOblique";e.ArialNarrow="Helvetica";e["ArialNarrow-Bold"]="Helvetica-Bold";e["ArialNarrow-BoldItalic"]="Helvetica-BoldOblique";e["ArialNarrow-Italic"]="Helvetica-Oblique";e.ArialBlack="Helvetica";e["ArialBlack-Bold"]="Helvetica-Bold";e["ArialBlack-BoldItalic"]="Helvetica-BoldOblique";e["ArialBlack-Italic"]="Helvetica-Oblique";e["Arial-Black"]="Helvetica";e["Arial-Black-Bold"]="Helvetica-Bold";e["Arial-Black-BoldItalic"]="Helvetica-BoldOblique";e["Arial-Black-Italic"]="Helvetica-Oblique";e.Arial="Helvetica";e["Arial-Bold"]="Helvetica-Bold";e["Arial-BoldItalic"]="Helvetica-BoldOblique";e["Arial-Italic"]="Helvetica-Oblique";e.ArialMT="Helvetica";e["Arial-BoldItalicMT"]="Helvetica-BoldOblique";e["Arial-BoldMT"]="Helvetica-Bold";e["Arial-ItalicMT"]="Helvetica-Oblique";e["Arial-BoldItalicMT-BoldItalic"]="Helvetica-BoldOblique";e["Arial-BoldMT-Bold"]="Helvetica-Bold";e["Arial-ItalicMT-Italic"]="Helvetica-Oblique";e.ArialUnicodeMS="Helvetica";e["ArialUnicodeMS-Bold"]="Helvetica-Bold";e["ArialUnicodeMS-BoldItalic"]="Helvetica-BoldOblique";e["ArialUnicodeMS-Italic"]="Helvetica-Oblique";e["Courier-BoldItalic"]="Courier-BoldOblique";e["Courier-Italic"]="Courier-Oblique";e.CourierNew="Courier";e["CourierNew-Bold"]="Courier-Bold";e["CourierNew-BoldItalic"]="Courier-BoldOblique";e["CourierNew-Italic"]="Courier-Oblique";e["CourierNewPS-BoldItalicMT"]="Courier-BoldOblique";e["CourierNewPS-BoldMT"]="Courier-Bold";e["CourierNewPS-ItalicMT"]="Courier-Oblique";e.CourierNewPSMT="Courier";e["Helvetica-BoldItalic"]="Helvetica-BoldOblique";e["Helvetica-Italic"]="Helvetica-Oblique";e["HelveticaLTStd-Bold"]="Helvetica-Bold";e["Symbol-Bold"]="Symbol";e["Symbol-BoldItalic"]="Symbol";e["Symbol-Italic"]="Symbol";e.TimesNewRoman="Times-Roman";e["TimesNewRoman-Bold"]="Times-Bold";e["TimesNewRoman-BoldItalic"]="Times-BoldItalic";e["TimesNewRoman-Italic"]="Times-Italic";e.TimesNewRomanPS="Times-Roman";e["TimesNewRomanPS-Bold"]="Times-Bold";e["TimesNewRomanPS-BoldItalic"]="Times-BoldItalic";e["TimesNewRomanPS-BoldItalicMT"]="Times-BoldItalic";e["TimesNewRomanPS-BoldMT"]="Times-Bold";e["TimesNewRomanPS-Italic"]="Times-Italic";e["TimesNewRomanPS-ItalicMT"]="Times-Italic";e.TimesNewRomanPSMT="Times-Roman";e["TimesNewRomanPSMT-Bold"]="Times-Bold";e["TimesNewRomanPSMT-BoldItalic"]="Times-BoldItalic";e["TimesNewRomanPSMT-Italic"]="Times-Italic"})),vi=getLookupTableFactory((function(e){e.Courier="FoxitFixed.pfb";e["Courier-Bold"]="FoxitFixedBold.pfb";e["Courier-BoldOblique"]="FoxitFixedBoldItalic.pfb";e["Courier-Oblique"]="FoxitFixedItalic.pfb";e.Helvetica="LiberationSans-Regular.ttf";e["Helvetica-Bold"]="LiberationSans-Bold.ttf";e["Helvetica-BoldOblique"]="LiberationSans-BoldItalic.ttf";e["Helvetica-Oblique"]="LiberationSans-Italic.ttf";e["Times-Roman"]="FoxitSerif.pfb";e["Times-Bold"]="FoxitSerifBold.pfb";e["Times-BoldItalic"]="FoxitSerifBoldItalic.pfb";e["Times-Italic"]="FoxitSerifItalic.pfb";e.Symbol="FoxitSymbol.pfb";e.ZapfDingbats="FoxitDingbats.pfb";e["LiberationSans-Regular"]="LiberationSans-Regular.ttf";e["LiberationSans-Bold"]="LiberationSans-Bold.ttf";e["LiberationSans-Italic"]="LiberationSans-Italic.ttf";e["LiberationSans-BoldItalic"]="LiberationSans-BoldItalic.ttf"})),Ki=getLookupTableFactory((function(e){e.Calibri="Helvetica";e["Calibri-Bold"]="Helvetica-Bold";e["Calibri-BoldItalic"]="Helvetica-BoldOblique";e["Calibri-Italic"]="Helvetica-Oblique";e.CenturyGothic="Helvetica";e["CenturyGothic-Bold"]="Helvetica-Bold";e["CenturyGothic-BoldItalic"]="Helvetica-BoldOblique";e["CenturyGothic-Italic"]="Helvetica-Oblique";e.ComicSansMS="Comic Sans MS";e["ComicSansMS-Bold"]="Comic Sans MS-Bold";e["ComicSansMS-BoldItalic"]="Comic Sans MS-BoldItalic";e["ComicSansMS-Italic"]="Comic Sans MS-Italic";e.GillSansMT="Helvetica";e["GillSansMT-Bold"]="Helvetica-Bold";e["GillSansMT-BoldItalic"]="Helvetica-BoldOblique";e["GillSansMT-Italic"]="Helvetica-Oblique";e.Impact="Helvetica";e["ItcSymbol-Bold"]="Helvetica-Bold";e["ItcSymbol-BoldItalic"]="Helvetica-BoldOblique";e["ItcSymbol-Book"]="Helvetica";e["ItcSymbol-BookItalic"]="Helvetica-Oblique";e["ItcSymbol-Medium"]="Helvetica";e["ItcSymbol-MediumItalic"]="Helvetica-Oblique";e.LucidaConsole="Courier";e["LucidaConsole-Bold"]="Courier-Bold";e["LucidaConsole-BoldItalic"]="Courier-BoldOblique";e["LucidaConsole-Italic"]="Courier-Oblique";e["LucidaSans-Demi"]="Helvetica-Bold";e["MS-Gothic"]="MS Gothic";e["MS-Gothic-Bold"]="MS Gothic-Bold";e["MS-Gothic-BoldItalic"]="MS Gothic-BoldItalic";e["MS-Gothic-Italic"]="MS Gothic-Italic";e["MS-Mincho"]="MS Mincho";e["MS-Mincho-Bold"]="MS Mincho-Bold";e["MS-Mincho-BoldItalic"]="MS Mincho-BoldItalic";e["MS-Mincho-Italic"]="MS Mincho-Italic";e["MS-PGothic"]="MS PGothic";e["MS-PGothic-Bold"]="MS PGothic-Bold";e["MS-PGothic-BoldItalic"]="MS PGothic-BoldItalic";e["MS-PGothic-Italic"]="MS PGothic-Italic";e["MS-PMincho"]="MS PMincho";e["MS-PMincho-Bold"]="MS PMincho-Bold";e["MS-PMincho-BoldItalic"]="MS PMincho-BoldItalic";e["MS-PMincho-Italic"]="MS PMincho-Italic";e.NuptialScript="Times-Italic";e.SegoeUISymbol="Helvetica"})),Ti=getLookupTableFactory((function(e){e["Adobe Jenson"]=!0;e["Adobe Text"]=!0;e.Albertus=!0;e.Aldus=!0;e.Alexandria=!0;e.Algerian=!0;e["American Typewriter"]=!0;e.Antiqua=!0;e.Apex=!0;e.Arno=!0;e.Aster=!0;e.Aurora=!0;e.Baskerville=!0;e.Bell=!0;e.Bembo=!0;e["Bembo Schoolbook"]=!0;e.Benguiat=!0;e["Berkeley Old Style"]=!0;e["Bernhard Modern"]=!0;e["Berthold City"]=!0;e.Bodoni=!0;e["Bauer Bodoni"]=!0;e["Book Antiqua"]=!0;e.Bookman=!0;e["Bordeaux Roman"]=!0;e["Californian FB"]=!0;e.Calisto=!0;e.Calvert=!0;e.Capitals=!0;e.Cambria=!0;e.Cartier=!0;e.Caslon=!0;e.Catull=!0;e.Centaur=!0;e["Century Old Style"]=!0;e["Century Schoolbook"]=!0;e.Chaparral=!0;e["Charis SIL"]=!0;e.Cheltenham=!0;e["Cholla Slab"]=!0;e.Clarendon=!0;e.Clearface=!0;e.Cochin=!0;e.Colonna=!0;e["Computer Modern"]=!0;e["Concrete Roman"]=!0;e.Constantia=!0;e["Cooper Black"]=!0;e.Corona=!0;e.Ecotype=!0;e.Egyptienne=!0;e.Elephant=!0;e.Excelsior=!0;e.Fairfield=!0;e["FF Scala"]=!0;e.Folkard=!0;e.Footlight=!0;e.FreeSerif=!0;e["Friz Quadrata"]=!0;e.Garamond=!0;e.Gentium=!0;e.Georgia=!0;e.Gloucester=!0;e["Goudy Old Style"]=!0;e["Goudy Schoolbook"]=!0;e["Goudy Pro Font"]=!0;e.Granjon=!0;e["Guardian Egyptian"]=!0;e.Heather=!0;e.Hercules=!0;e["High Tower Text"]=!0;e.Hiroshige=!0;e["Hoefler Text"]=!0;e["Humana Serif"]=!0;e.Imprint=!0;e["Ionic No. 5"]=!0;e.Janson=!0;e.Joanna=!0;e.Korinna=!0;e.Lexicon=!0;e.LiberationSerif=!0;e["Liberation Serif"]=!0;e["Linux Libertine"]=!0;e.Literaturnaya=!0;e.Lucida=!0;e["Lucida Bright"]=!0;e.Melior=!0;e.Memphis=!0;e.Miller=!0;e.Minion=!0;e.Modern=!0;e["Mona Lisa"]=!0;e["Mrs Eaves"]=!0;e["MS Serif"]=!0;e["Museo Slab"]=!0;e["New York"]=!0;e["Nimbus Roman"]=!0;e["NPS Rawlinson Roadway"]=!0;e.NuptialScript=!0;e.Palatino=!0;e.Perpetua=!0;e.Plantin=!0;e["Plantin Schoolbook"]=!0;e.Playbill=!0;e["Poor Richard"]=!0;e["Rawlinson Roadway"]=!0;e.Renault=!0;e.Requiem=!0;e.Rockwell=!0;e.Roman=!0;e["Rotis Serif"]=!0;e.Sabon=!0;e.Scala=!0;e.Seagull=!0;e.Sistina=!0;e.Souvenir=!0;e.STIX=!0;e["Stone Informal"]=!0;e["Stone Serif"]=!0;e.Sylfaen=!0;e.Times=!0;e.Trajan=!0;e["Trinité"]=!0;e["Trump Mediaeval"]=!0;e.Utopia=!0;e["Vale Type"]=!0;e["Bitstream Vera"]=!0;e["Vera Serif"]=!0;e.Versailles=!0;e.Wanted=!0;e.Weiss=!0;e["Wide Latin"]=!0;e.Windsor=!0;e.XITS=!0})),qi=getLookupTableFactory((function(e){e.Dingbats=!0;e.Symbol=!0;e.ZapfDingbats=!0;e.Wingdings=!0;e["Wingdings-Bold"]=!0;e["Wingdings-Regular"]=!0})),Oi=getLookupTableFactory((function(e){e[2]=10;e[3]=32;e[4]=33;e[5]=34;e[6]=35;e[7]=36;e[8]=37;e[9]=38;e[10]=39;e[11]=40;e[12]=41;e[13]=42;e[14]=43;e[15]=44;e[16]=45;e[17]=46;e[18]=47;e[19]=48;e[20]=49;e[21]=50;e[22]=51;e[23]=52;e[24]=53;e[25]=54;e[26]=55;e[27]=56;e[28]=57;e[29]=58;e[30]=894;e[31]=60;e[32]=61;e[33]=62;e[34]=63;e[35]=64;e[36]=65;e[37]=66;e[38]=67;e[39]=68;e[40]=69;e[41]=70;e[42]=71;e[43]=72;e[44]=73;e[45]=74;e[46]=75;e[47]=76;e[48]=77;e[49]=78;e[50]=79;e[51]=80;e[52]=81;e[53]=82;e[54]=83;e[55]=84;e[56]=85;e[57]=86;e[58]=87;e[59]=88;e[60]=89;e[61]=90;e[62]=91;e[63]=92;e[64]=93;e[65]=94;e[66]=95;e[67]=96;e[68]=97;e[69]=98;e[70]=99;e[71]=100;e[72]=101;e[73]=102;e[74]=103;e[75]=104;e[76]=105;e[77]=106;e[78]=107;e[79]=108;e[80]=109;e[81]=110;e[82]=111;e[83]=112;e[84]=113;e[85]=114;e[86]=115;e[87]=116;e[88]=117;e[89]=118;e[90]=119;e[91]=120;e[92]=121;e[93]=122;e[94]=123;e[95]=124;e[96]=125;e[97]=126;e[98]=196;e[99]=197;e[100]=199;e[101]=201;e[102]=209;e[103]=214;e[104]=220;e[105]=225;e[106]=224;e[107]=226;e[108]=228;e[109]=227;e[110]=229;e[111]=231;e[112]=233;e[113]=232;e[114]=234;e[115]=235;e[116]=237;e[117]=236;e[118]=238;e[119]=239;e[120]=241;e[121]=243;e[122]=242;e[123]=244;e[124]=246;e[125]=245;e[126]=250;e[127]=249;e[128]=251;e[129]=252;e[130]=8224;e[131]=176;e[132]=162;e[133]=163;e[134]=167;e[135]=8226;e[136]=182;e[137]=223;e[138]=174;e[139]=169;e[140]=8482;e[141]=180;e[142]=168;e[143]=8800;e[144]=198;e[145]=216;e[146]=8734;e[147]=177;e[148]=8804;e[149]=8805;e[150]=165;e[151]=181;e[152]=8706;e[153]=8721;e[154]=8719;e[156]=8747;e[157]=170;e[158]=186;e[159]=8486;e[160]=230;e[161]=248;e[162]=191;e[163]=161;e[164]=172;e[165]=8730;e[166]=402;e[167]=8776;e[168]=8710;e[169]=171;e[170]=187;e[171]=8230;e[179]=8220;e[180]=8221;e[181]=8216;e[182]=8217;e[200]=193;e[203]=205;e[207]=211;e[210]=218;e[223]=711;e[224]=321;e[225]=322;e[226]=352;e[227]=353;e[228]=381;e[229]=382;e[233]=221;e[234]=253;e[252]=263;e[253]=268;e[254]=269;e[258]=258;e[260]=260;e[261]=261;e[265]=280;e[266]=281;e[267]=282;e[268]=283;e[269]=313;e[275]=323;e[276]=324;e[278]=328;e[283]=344;e[284]=345;e[285]=346;e[286]=347;e[292]=367;e[295]=377;e[296]=378;e[298]=380;e[305]=963;e[306]=964;e[307]=966;e[308]=8215;e[309]=8252;e[310]=8319;e[311]=8359;e[312]=8592;e[313]=8593;e[337]=9552;e[493]=1039;e[494]=1040;e[672]=1488;e[673]=1489;e[674]=1490;e[675]=1491;e[676]=1492;e[677]=1493;e[678]=1494;e[679]=1495;e[680]=1496;e[681]=1497;e[682]=1498;e[683]=1499;e[684]=1500;e[685]=1501;e[686]=1502;e[687]=1503;e[688]=1504;e[689]=1505;e[690]=1506;e[691]=1507;e[692]=1508;e[693]=1509;e[694]=1510;e[695]=1511;e[696]=1512;e[697]=1513;e[698]=1514;e[705]=1524;e[706]=8362;e[710]=64288;e[711]=64298;e[759]=1617;e[761]=1776;e[763]=1778;e[775]=1652;e[777]=1764;e[778]=1780;e[779]=1781;e[780]=1782;e[782]=771;e[783]=64726;e[786]=8363;e[788]=8532;e[790]=768;e[791]=769;e[792]=768;e[795]=803;e[797]=64336;e[798]=64337;e[799]=64342;e[800]=64343;e[801]=64344;e[802]=64345;e[803]=64362;e[804]=64363;e[805]=64364;e[2424]=7821;e[2425]=7822;e[2426]=7823;e[2427]=7824;e[2428]=7825;e[2429]=7826;e[2430]=7827;e[2433]=7682;e[2678]=8045;e[2679]=8046;e[2830]=1552;e[2838]=686;e[2840]=751;e[2842]=753;e[2843]=754;e[2844]=755;e[2846]=757;e[2856]=767;e[2857]=848;e[2858]=849;e[2862]=853;e[2863]=854;e[2864]=855;e[2865]=861;e[2866]=862;e[2906]=7460;e[2908]=7462;e[2909]=7463;e[2910]=7464;e[2912]=7466;e[2913]=7467;e[2914]=7468;e[2916]=7470;e[2917]=7471;e[2918]=7472;e[2920]=7474;e[2921]=7475;e[2922]=7476;e[2924]=7478;e[2925]=7479;e[2926]=7480;e[2928]=7482;e[2929]=7483;e[2930]=7484;e[2932]=7486;e[2933]=7487;e[2934]=7488;e[2936]=7490;e[2937]=7491;e[2938]=7492;e[2940]=7494;e[2941]=7495;e[2942]=7496;e[2944]=7498;e[2946]=7500;e[2948]=7502;e[2950]=7504;e[2951]=7505;e[2952]=7506;e[2954]=7508;e[2955]=7509;e[2956]=7510;e[2958]=7512;e[2959]=7513;e[2960]=7514;e[2962]=7516;e[2963]=7517;e[2964]=7518;e[2966]=7520;e[2967]=7521;e[2968]=7522;e[2970]=7524;e[2971]=7525;e[2972]=7526;e[2974]=7528;e[2975]=7529;e[2976]=7530;e[2978]=1537;e[2979]=1538;e[2980]=1539;e[2982]=1549;e[2983]=1551;e[2984]=1552;e[2986]=1554;e[2987]=1555;e[2988]=1556;e[2990]=1623;e[2991]=1624;e[2995]=1775;e[2999]=1791;e[3002]=64290;e[3003]=64291;e[3004]=64292;e[3006]=64294;e[3007]=64295;e[3008]=64296;e[3011]=1900;e[3014]=8223;e[3015]=8244;e[3017]=7532;e[3018]=7533;e[3019]=7534;e[3075]=7590;e[3076]=7591;e[3079]=7594;e[3080]=7595;e[3083]=7598;e[3084]=7599;e[3087]=7602;e[3088]=7603;e[3091]=7606;e[3092]=7607;e[3095]=7610;e[3096]=7611;e[3099]=7614;e[3100]=7615;e[3103]=7618;e[3104]=7619;e[3107]=8337;e[3108]=8338;e[3116]=1884;e[3119]=1885;e[3120]=1885;e[3123]=1886;e[3124]=1886;e[3127]=1887;e[3128]=1887;e[3131]=1888;e[3132]=1888;e[3135]=1889;e[3136]=1889;e[3139]=1890;e[3140]=1890;e[3143]=1891;e[3144]=1891;e[3147]=1892;e[3148]=1892;e[3153]=580;e[3154]=581;e[3157]=584;e[3158]=585;e[3161]=588;e[3162]=589;e[3165]=891;e[3166]=892;e[3169]=1274;e[3170]=1275;e[3173]=1278;e[3174]=1279;e[3181]=7622;e[3182]=7623;e[3282]=11799;e[3316]=578;e[3379]=42785;e[3393]=1159;e[3416]=8377})),Pi=getLookupTableFactory((function(e){e[227]=322;e[264]=261;e[291]=346})),Wi=getLookupTableFactory((function(e){e[1]=32;e[4]=65;e[5]=192;e[6]=193;e[9]=196;e[17]=66;e[18]=67;e[21]=268;e[24]=68;e[28]=69;e[29]=200;e[30]=201;e[32]=282;e[38]=70;e[39]=71;e[44]=72;e[47]=73;e[48]=204;e[49]=205;e[58]=74;e[60]=75;e[62]=76;e[68]=77;e[69]=78;e[75]=79;e[76]=210;e[80]=214;e[87]=80;e[89]=81;e[90]=82;e[92]=344;e[94]=83;e[97]=352;e[100]=84;e[104]=85;e[109]=220;e[115]=86;e[116]=87;e[121]=88;e[122]=89;e[124]=221;e[127]=90;e[129]=381;e[258]=97;e[259]=224;e[260]=225;e[263]=228;e[268]=261;e[271]=98;e[272]=99;e[273]=263;e[275]=269;e[282]=100;e[286]=101;e[287]=232;e[288]=233;e[290]=283;e[295]=281;e[296]=102;e[336]=103;e[346]=104;e[349]=105;e[350]=236;e[351]=237;e[361]=106;e[364]=107;e[367]=108;e[371]=322;e[373]=109;e[374]=110;e[381]=111;e[382]=242;e[383]=243;e[386]=246;e[393]=112;e[395]=113;e[396]=114;e[398]=345;e[400]=115;e[401]=347;e[403]=353;e[410]=116;e[437]=117;e[442]=252;e[448]=118;e[449]=119;e[454]=120;e[455]=121;e[457]=253;e[460]=122;e[462]=382;e[463]=380;e[853]=44;e[855]=58;e[856]=46;e[876]=47;e[878]=45;e[882]=45;e[894]=40;e[895]=41;e[896]=91;e[897]=93;e[923]=64;e[1004]=48;e[1005]=49;e[1006]=50;e[1007]=51;e[1008]=52;e[1009]=53;e[1010]=54;e[1011]=55;e[1012]=56;e[1013]=57;e[1081]=37;e[1085]=43;e[1086]=45}));function getStandardFontName(e){const t=normalizeFontName(e);return Yi()[t]}function isKnownFontName(e){const t=normalizeFontName(e);return!!(Yi()[t]||Ki()[t]||Ti()[t]||qi()[t])}class ToUnicodeMap{constructor(e=[]){this._map=e}get length(){return this._map.length}forEach(e){for(const t in this._map)e(t,this._map[t].codePointAt(0))}has(e){return void 0!==this._map[e]}get(e){return this._map[e]}charCodeOf(e){const t=this._map;if(t.length<=65536)return t.indexOf(e);for(const i in t)if(t[i]===e)return 0|i;return-1}amend(e){for(const t in e)this._map[t]=e[t]}}class IdentityToUnicodeMap{constructor(e,t){this.firstChar=e;this.lastChar=t}get length(){return this.lastChar+1-this.firstChar}forEach(e){for(let t=this.firstChar,i=this.lastChar;t<=i;t++)e(t,t)}has(e){return this.firstChar<=e&&e<=this.lastChar}get(e){if(this.firstChar<=e&&e<=this.lastChar)return String.fromCharCode(e)}charCodeOf(e){return Number.isInteger(e)&&e>=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const i=new CFFParser(e,t,Ri);this.cff=i.parse();this.cff.duplicateFirstGlyph();const a=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=a.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:i,cMap:a}=t,s=e.charset.charset;let r,n;if(t.composite){let t,g;if(i?.length>0){t=Object.create(null);for(let e=0,a=i.length;e=0){const a=i[t];a&&(s[e]=a)}}s.length>0&&(this.properties.builtInEncoding=s)}}function getUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function getUint16(e,t){return e[t]<<8|e[t+1]}function getInt16(e,t){return(e[t]<<24|e[t+1]<<16)>>16}function getInt8(e,t){return e[t]<<24>>24}function getFloat214(e,t){return getInt16(e,t)/16384}function getSubroutineBias(e){const t=e.length;let i=32768;t<1240?i=107:t<33900&&(i=1131);return i}function parseCmap(e,t,i){const a=1===getUint16(e,t+2)?getUint32(e,t+8):getUint32(e,t+16),s=getUint16(e,t+a);let r,n,g;if(4===s){getUint16(e,t+a+2);const i=getUint16(e,t+a+6)>>1;n=t+a+14;r=[];for(g=0;g>1;i0;)C.push({flags:r})}for(i=0;i>1;p=!0;break;case 4:n+=s.pop();moveTo(r,n);p=!0;break;case 5:for(;s.length>0;){r+=s.shift();n+=s.shift();lineTo(r,n)}break;case 6:for(;s.length>0;){r+=s.shift();lineTo(r,n);if(0===s.length)break;n+=s.shift();lineTo(r,n)}break;case 7:for(;s.length>0;){n+=s.shift();lineTo(r,n);if(0===s.length)break;r+=s.shift();lineTo(r,n)}break;case 8:for(;s.length>0;){c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}break;case 10:d=s.pop();f=null;if(i.isCFFCIDFont){const e=i.fdSelect.getFDIndex(a);if(e>=0&&eMath.abs(n-t)?r+=s.shift():n+=s.shift();bezierCurveTo(c,h,C,l,r,n);break;default:throw new FormatError(`unknown operator: 12 ${m}`)}break;case 14:if(s.length>=4){const e=s.pop(),a=s.pop();n=s.pop();r=s.pop();t.save();t.translate(r,n);let g=lookupCmap(i.cmap,String.fromCharCode(i.glyphNameMap[hi[e]]));compileCharString(i.glyphs[g.glyphId],t,i,g.glyphId);t.restore();g=lookupCmap(i.cmap,String.fromCharCode(i.glyphNameMap[hi[a]]));compileCharString(i.glyphs[g.glyphId],t,i,g.glyphId)}return;case 19:case 20:g+=s.length>>1;o+=g+7>>3;p=!0;break;case 21:n+=s.pop();r+=s.pop();moveTo(r,n);p=!0;break;case 22:r+=s.pop();moveTo(r,n);p=!0;break;case 24:for(;s.length>2;){c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}r+=s.shift();n+=s.shift();lineTo(r,n);break;case 25:for(;s.length>6;){r+=s.shift();n+=s.shift();lineTo(r,n)}c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n);break;case 26:s.length%2&&(r+=s.shift());for(;s.length>0;){c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C;n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}break;case 27:s.length%2&&(n+=s.shift());for(;s.length>0;){c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l;bezierCurveTo(c,h,C,l,r,n)}break;case 28:s.push((e[o]<<24|e[o+1]<<16)>>16);o+=2;break;case 29:d=s.pop()+i.gsubrsBias;f=i.gsubrs[d];f&&parse(f);break;case 30:for(;s.length>0;){c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n);if(0===s.length)break;c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();n=l+s.shift();r=C+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n)}break;case 31:for(;s.length>0;){c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();n=l+s.shift();r=C+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n);if(0===s.length)break;c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n)}break;default:if(m<32)throw new FormatError(`unknown operator: ${m}`);if(m<247)s.push(m-139);else if(m<251)s.push(256*(m-247)+e[o++]+108);else if(m<255)s.push(256*-(m-251)-e[o++]-108);else{s.push((e[o]<<24|e[o+1]<<16|e[o+2]<<8|e[o+3])/65536);o+=4}}p&&(s.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const[i,a,s,r,n,g]=this.currentTransform;for(let e=0,o=t.length;e=0&&e2*getUint16(e,t)}const r=[];let n=s(t,0);for(let i=a;ie+(t.getSize()+3&-4)),0)}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),i=e>131070,a=i?4:2,s=new DataView(new ArrayBuffer((this.glyphs.length+1)*a));i?s.setUint32(0,0):s.setUint16(0,0);let r=0,n=0;for(const e of this.glyphs){r+=e.write(r,t);r=r+3&-4;n+=a;i?s.setUint32(n,r):s.setUint16(n,r>>1)}return{isLocationLong:i,loca:new Uint8Array(s.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,i=this.glyphs.length;te+t.getSize()),0);return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const i=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const i of this.composites)e+=i.write(e,t);return e-i}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const i of this.composites)i.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:i,xMax:a,yMax:s}){this.numberOfContours=e;this.xMin=t;this.yMin=i;this.xMax=a;this.yMax=s}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:i}){this.xCoordinates=t;this.yCoordinates=i;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,i){const a=[];for(let s=0;s255?e+=2:g>0&&(e+=1);t=r;g=Math.abs(n-i);g>255?e+=2:g>0&&(e+=1);i=n}}return e}write(e,t){const i=e,a=[],s=[],r=[];let n=0,g=0;for(const i of this.contours){for(let e=0,t=i.xCoordinates.length;e=0?18:2;a.push(e)}else a.push(c)}n=o;const C=i.yCoordinates[e];c=C-g;if(0===c){t|=32;s.push(0)}else{const e=Math.abs(c);if(e<=255){t|=c>=0?36:4;s.push(e)}else s.push(c)}g=C;r.push(t)}t.setUint16(e,a.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const i of r)t.setUint8(e++,i);for(let i=0,s=a.length;i=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const i=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-i}scale(e,t){}}function writeInt16(e,t,i){e[t]=i>>8&255;e[t+1]=255&i}function writeInt32(e,t,i){e[t]=i>>24&255;e[t+1]=i>>16&255;e[t+2]=i>>8&255;e[t+3]=255&i}function writeData(e,t,i){if(i instanceof Uint8Array)e.set(i,t);else if("string"==typeof i)for(let a=0,s=i.length;ai;){i<<=1;a++}const s=i*t;return{range:s,entry:a,rangeShift:t*e-s}}toArray(){let e=this.sfnt;const t=this.tables,i=Object.keys(t);i.sort();const a=i.length;let s,r,n,g,o,c=12+16*a;const C=[c];for(s=0;s>>0;C.push(c)}const h=new Uint8Array(c);for(s=0;s>>0}writeInt32(h,c+4,e);writeInt32(h,c+8,C[s]);writeInt32(h,c+12,t[o].length);c+=16}return h}addTable(e,t){if(e in this.tables)throw new Error("Table "+e+" already exists");this.tables[e]=t}}const Zi=[4],Vi=[5],zi=[6],_i=[7],$i=[8],Aa=[12,35],ea=[14],ta=[21],ia=[22],aa=[30],sa=[31];class Type1CharString{constructor(){this.width=0;this.lsb=0;this.flexing=!1;this.output=[];this.stack=[]}convert(e,t,i){const a=e.length;let s,r,n,g=!1;for(let o=0;oa)return!0;const s=a-e;for(let e=s;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);i?this.stack.splice(s,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,i){if(i>=e.length)return new Uint8Array(0);let a,s,r=0|t;for(a=0;a>8;r=52845*(t+r)+22719&65535}return g}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,i){if(t){const t=e.getBytes(),i=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(i?decrypt(t,55665,4):function decryptAscii(e,t,i){let a=0|t;const s=e.length,r=new Uint8Array(s>>>1);let n,g;for(n=0,g=0;n>8;a=52845*(e+a)+22719&65535}}return r.slice(i,g)}(t,55665,4))}this.seacAnalysisEnabled=!!i;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let i="";do{i+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return i}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,i=[],a=[],s=Object.create(null);s.lenIV=4;const r={subrs:[],charstrings:[],properties:{privateData:s}};let n,g,o,c;for(;null!==(n=this.getToken());)if("/"===n){n=this.getToken();switch(n){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){n=this.getToken();if(null===n||"end"===n)break;if("/"!==n)continue;const e=this.getToken();g=this.readInt();this.getToken();o=g>0?t.getBytes(g):new Uint8Array(0);c=r.properties.privateData.lenIV;const i=this.readCharStrings(o,c);this.nextChar();n=this.getToken();"noaccess"===n?this.getToken():"/"===n&&this.prevChar();a.push({glyph:e,encoded:i})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();g=this.readInt();this.getToken();o=g>0?t.getBytes(g):new Uint8Array(0);c=r.properties.privateData.lenIV;const a=this.readCharStrings(o,c);this.nextChar();n=this.getToken();"noaccess"===n&&this.getToken();i[e]=a}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":r.properties.privateData[n]=this.readNumberArray();break;case"StdHW":case"StdVW":r.properties.privateData[n]=this.readNumberArray()[0];break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":r.properties.privateData[n]=this.readNumber();break;case"ExpansionFactor":r.properties.privateData[n]=this.readNumber()||.06;break;case"ForceBold":r.properties.privateData[n]=this.readBoolean()}}for(const{encoded:t,glyph:s}of a){const a=new Type1CharString,n=a.convert(t,i,this.seacAnalysisEnabled);let g=a.output;n&&(g=[14]);const o={glyphName:s,charstring:g,width:a.width,lsb:a.lsb,seac:a.seac};".notdef"===s?r.charstrings.unshift(o):r.charstrings.push(o);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(s);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=a.width)}}return r}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const i=this.readNumberArray();e.fontMatrix=i;break;case"Encoding":const a=this.getToken();let s;if(/^\d+$/.test(a)){s=[];const e=0|parseInt(a,10);this.getToken();for(let i=0;i=s){n+=i;for(;n=0&&(a[e]=s)}}return type1FontGlyphMapping(e,a,i)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let i=0,a=e.length;i0;e--)t[e]-=t[e-1];Q.setByName(e,t)}r.topDict.privateDict=Q;const u=new CFFIndex;for(C=0,h=a.length;C0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,i,a,s,r,n,g,o){this.originalCharCode=e;this.fontChar=t;this.unicode=i;this.accent=a;this.width=s;this.vmetric=r;this.operatorListId=n;this.isSpace=g;this.isInFont=o}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=ki.get(e);if(t)return t;const i=e.match(Si),a={isWhitespace:!!i?.[1],isZeroWidthDiacritic:!!i?.[2],isInvisibleFormatMark:!!i?.[3]};ki.set(e,a);return a}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,i){e[t+1]=i;e[t]=i>>>8}function signedInt16(e,t){const i=(e<<8)+t;return 32768&i?i-65536:i}function string16(e){return String.fromCharCode(e>>8&255,255&e)}function safeString16(e){e>32767?e=32767:e<-32768&&(e=-32768);return String.fromCharCode(e>>8&255,255&e)}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:i,composite:a}){let s,r;if(function isTrueTypeFile(e){const t=e.peekBytes(4);return 65536===readUint32(t,0)||"true"===bytesToString(t)}(e)||isTrueTypeCollectionFile(e))s=a?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))s=a?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))s=a?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(a){s="CIDFontType0";r="CIDFontType0C"}else{s="MMType1"===t?"MMType1":"Type1";r="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");s=t;r=i}return[s,r]}function applyStandardFontGlyphMap(e,t){for(const i in t)e[+i]=t[i]}function buildToFontChar(e,t,i){const a=[];let s;for(let i=0,r=e.length;iC){o++;if(o>=ra.length){warn("Ran out of space in font private use area.");break}c=ra[o][0];C=ra[o][1]}const E=c++;0===Q&&(Q=i);let u=a.get(l);"string"==typeof u&&(u=u.codePointAt(0));if(u&&!(h=u,ra[0][0]<=h&&h<=ra[0][1]||ra[1][0]<=h&&h<=ra[1][1])&&!g.has(Q)){r.set(u,Q);g.add(Q)}s[E]=Q;n[l]=E}var h;return{toFontChar:n,charCodeToGlyphId:s,toUnicodeExtraMap:r,nextAvailableFontCharCode:c}}function createCmapTable(e,t,i){const a=function getRanges(e,t,i){const a=[];for(const t in e)e[t]>=i||a.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,s]of t)s>=i||a.push({fontCharCode:e,glyphId:s});0===a.length&&a.push({fontCharCode:0,glyphId:0});a.sort((function fontGetRangesSort(e,t){return e.fontCharCode-t.fontCharCode}));const s=[],r=a.length;for(let e=0;e65535?2:1;let r,n,g,o,c="\0\0"+string16(s)+"\0\0"+string32(4+8*s);for(r=a.length-1;r>=0&&!(a[r][0]<=65535);--r);const C=r+1;a[r][0]<65535&&65535===a[r][1]&&(a[r][1]=65534);const h=a[r][1]<65535?1:0,l=C+h,Q=OpenTypeFileBuilder.getSearchParams(l,2);let E,u,d,f,p="",m="",y="",w="",D="",b=0;for(r=0,n=C;r0){m+="ÿÿ";p+="ÿÿ";y+="\0";w+="\0\0"}const F="\0\0"+string16(2*l)+string16(Q.range)+string16(Q.entry)+string16(Q.rangeShift)+m+"\0\0"+p+y+w+D;let S="",k="";if(s>1){c+="\0\0\n"+string32(4+8*s+4+F.length);S="";for(r=0,n=a.length;re||!g)&&(g=e);o 123 are reserved for internal usage");n|=1<65535&&(o=65535)}else{g=0;o=255}const C=e.bbox||[0,0,0,0],h=i.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),l=e.ascentScaled?1:h/na,Q=i.ascent||Math.round(l*(e.ascent||C[3]));let E=i.descent||Math.round(l*(e.descent||C[1]));E>0&&e.descent>0&&C[1]<0&&(E=-E);const u=i.yMax||Q,d=-i.yMin||-E;return"\0$ô\0\0\0Š»\0\0\0ŒŠ»\0\0ß\x001\0\0\0\0"+String.fromCharCode(e.fixedPitch?9:0)+"\0\0\0\0\0\0"+string32(a)+string32(s)+string32(r)+string32(n)+"*21*"+string16(e.italicAngle?1:0)+string16(g||e.firstChar)+string16(o||e.lastChar)+string16(Q)+string16(E)+"\0d"+string16(u)+string16(d)+"\0\0\0\0\0\0\0\0"+string16(e.xHeight)+string16(e.capHeight)+string16(0)+string16(g||e.firstChar)+"\0"}function createPostTable(e){return"\0\0\0"+string32(Math.floor(65536*e.italicAngle))+"\0\0\0\0"+string32(e.fixedPitch?1:0)+"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||(t=[[],[]]);const i=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],a=[];let s,r,n,g,o;for(s=0,r=i.length;s0;if((n||g)&&"CIDFontType2"===i&&this.cidEncoding.startsWith("Identity-")){const i=e.cidToGidMap,a=[];applyStandardFontGlyphMap(a,Oi());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(a,Pi()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(a,Wi());if(i){for(const e in a){const t=a[e];void 0!==i[t]&&(a[+e]=i[t])}i.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach((function(e,t){const s=a[e];void 0===i[s]&&(a[+e]=t)}))}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(e,t){a[+e]=t}));this.toFontChar=a;this.toUnicode=new ToUnicodeMap(a)}else if(/Symbol/i.test(a))this.toFontChar=buildToFontChar(Bi,wi(),this.differences);else if(/Dingbats/i.test(a))this.toFontChar=buildToFontChar(Qi,Di(),this.differences);else if(n||g){const e=buildToFontChar(this.defaultEncoding,wi(),this.differences);"CIDFontType2"!==i||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(t,i){e[+t]=i}));this.toFontChar=e}else{const e=wi(),i=[];this.toUnicode.forEach(((t,a)=>{if(!this.composite){const i=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==i&&(a=i)}i[+t]=a}));this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(i,Oi());this.toFontChar=i}amendFallbackToUnicode(e);this.loadedName=a.split("-",1)[0]}checkAndRepair(e,t,i){const a=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const i=Object.create(null);i["OS/2"]=null;i.cmap=null;i.head=null;i.hhea=null;i.hmtx=null;i.maxp=null;i.name=null;i.post=null;for(let s=0;s>>0,a=e.getInt32()>>>0,s=e.getInt32()>>>0,r=e.pos;e.pos=e.start||0;e.skip(a);const n=e.getBytes(s);e.pos=r;if("head"===t){n[8]=n[9]=n[10]=n[11]=0;n[17]|=32}return{tag:t,checksum:i,length:s,offset:a,data:n}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,i,a,s,r){const n={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||i>e.length||i-t<=12)return n;const g=e.subarray(t,i),o=signedInt16(g[2],g[3]),c=signedInt16(g[4],g[5]),C=signedInt16(g[6],g[7]),h=signedInt16(g[8],g[9]);if(o>C){writeSignedInt16(g,2,C);writeSignedInt16(g,6,o)}if(c>h){writeSignedInt16(g,4,h);writeSignedInt16(g,8,c)}const l=signedInt16(g[0],g[1]);if(l<0){if(l<-1)return n;a.set(g,s);n.length=g.length;return n}let Q,E=10,u=0;for(Q=0;Qg.length)return n;if(!r&&f>0){a.set(g.subarray(0,d),s);a.set([0,0],s+d);a.set(g.subarray(p,y),s+d+2);y-=f;g.length-y>3&&(y=y+3&-4);n.length=y;return n}if(g.length-y>3){y=y+3&-4;a.set(g.subarray(0,y),s);n.length=y;return n}a.set(g,s);n.length=g.length;return n}function readNameTable(e){const i=(t.start||0)+e.offset;t.pos=i;const a=[[],[]],s=[],r=e.length,n=i+r;if(0!==t.getUint16()||r<6)return[a,s];const g=t.getUint16(),o=t.getUint16();let c,C;for(c=0;cn)continue;t.pos=r;const g=e.name;if(e.encoding){let i="";for(let a=0,s=e.length;a0&&(c+=e-1)}}else{if(d||p){warn("TT: nested FDEFs not allowed");u=!0}d=!0;h=c;n=l.pop();t.functionsDefined[n]={data:o,i:c}}else if(!d&&!p){n=l.at(-1);if(isNaN(n))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[n]=!0;if(n in t.functionsStackDeltas){const e=l.length+t.functionsStackDeltas[n];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}l.length=e}else if(n in t.functionsDefined&&!E.includes(n)){Q.push({data:o,i:c,stackTop:l.length-1});E.push(n);g=t.functionsDefined[n];if(!g){warn("TT: CALL non-existent function");t.hintsValid=!1;return}o=g.data;c=g.i}}}if(!d&&!p){let t=0;e<=142?t=s[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){a=l.pop();isNaN(a)||(t=2*-a)}for(;t<0&&l.length>0;){l.pop();t++}for(;t>0;){l.push(NaN);t--}}}t.tooComplexToFollowFunctions=u;const m=[o];c>o.length&&m.push(new Uint8Array(c-o.length));if(h>C){warn("TT: complementing a missing function tail");m.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let i,a,s=0;for(i=0,a=t.length;i>>0,r=[];for(let t=0;t>>0);const n={ttcTag:t,majorVersion:i,minorVersion:a,numFonts:s,offsetTable:r};switch(i){case 1:return n;case 2:n.dsigTag=e.getInt32()>>>0;n.dsigLength=e.getInt32()>>>0;n.dsigOffset=e.getInt32()>>>0;return n}throw new FormatError(`Invalid TrueType Collection majorVersion: ${i}.`)}(e),s=t.split("+");let r;for(let n=0;n0||!(i.cMap instanceof IdentityCMap));if("OTTO"===r.version&&!t||!n.head||!n.hhea||!n.maxp||!n.post){o=new Stream(n["CFF "].data);g=new CFFFont(o,i);adjustWidths(i);return this.convert(e,g,i)}delete n.glyf;delete n.loca;delete n.fpgm;delete n.prep;delete n["cvt "];this.isOpenType=!0}if(!n.maxp)throw new FormatError('Required "maxp" table is not found');t.pos=(t.start||0)+n.maxp.offset;let C=t.getInt32();const h=t.getUint16();if(65536!==C&&20480!==C){if(6===n.maxp.length)C=20480;else{if(!(n.maxp.length>=32))throw new FormatError('"maxp" table has a wrong version number');C=65536}!function writeUint32(e,t,i){e[t+3]=255&i;e[t+2]=i>>>8;e[t+1]=i>>>16;e[t]=i>>>24}(n.maxp.data,0,C)}if(i.scaleFactors?.length===h&&c){const{scaleFactors:e}=i,t=int16(n.head.data[50],n.head.data[51]),a=new GlyfTable({glyfTable:n.glyf.data,isGlyphLocationsLong:t,locaTable:n.loca.data,numGlyphs:h});a.scale(e);const{glyf:s,loca:r,isLocationLong:g}=a.write();n.glyf.data=s;n.loca.data=r;if(g!==!!t){n.head.data[50]=0;n.head.data[51]=g?1:0}const o=n.hmtx.data;for(let t=0;t>8&255;o[i+1]=255&a;writeSignedInt16(o,i+2,Math.round(e[t]*signedInt16(o[i+2],o[i+3])))}}let l=h+1,Q=!0;if(l>65535){Q=!1;l=h;warn("Not enough space in glyfs to duplicate first glyph.")}let E=0,u=0;if(C>=65536&&n.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){n.maxp.data[14]=0;n.maxp.data[15]=2}t.pos+=4;E=t.getUint16();t.pos+=4;u=t.getUint16()}n.maxp.data[4]=l>>8;n.maxp.data[5]=255&l;const d=function sanitizeTTPrograms(e,t,i,a){const s={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,s);t&&sanitizeTTProgram(t,s);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let i=0,a=e.functionsUsed.length;it){warn("TT: invalid function id: "+i);e.hintsValid=!1;return}if(e.functionsUsed[i]&&!e.functionsDefined[i]){warn("TT: undefined function: "+i);e.hintsValid=!1;return}}}(s,a);if(i&&1&i.length){const e=new Uint8Array(i.length+1);e.set(i.data);i.data=e}return s.hintsValid}(n.fpgm,n.prep,n["cvt "],E);if(!d){delete n.fpgm;delete n.prep;delete n["cvt "]}!function sanitizeMetrics(e,t,i,a,s,r){if(!t){i&&(i.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const n=e.getUint16();e.pos+=8;e.pos+=2;let g=e.getUint16();if(0!==n){if(!(2&int16(a.data[44],a.data[45]))){t.data[22]=0;t.data[23]=0}}if(g>s){info(`The numOfMetrics (${g}) should not be greater than the numGlyphs (${s}).`);g=s;t.data[34]=(65280&g)>>8;t.data[35]=255&g}const o=s-g-(i.length-4*g>>1);if(o>0){const e=new Uint8Array(i.length+2*o);e.set(i.data);if(r){e[i.length]=i.data[2];e[i.length+1]=i.data[3]}i.data=e}}(t,n.hhea,n.hmtx,n.head,l,Q);if(!n.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,i){const a=e.data,s=function int32(e,t,i,a){return(e<<24)+(t<<16)+(i<<8)+a}(a[0],a[1],a[2],a[3]);if(s>>16!=1){info("Attempting to fix invalid version in head table: "+s);a[0]=0;a[1]=1;a[2]=0;a[3]=0}const r=int16(a[50],a[51]);if(r<0||r>1){info("Attempting to fix invalid indexToLocFormat in head table: "+r);const e=t+1;if(i===e<<1){a[50]=0;a[51]=0}else{if(i!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+r);a[50]=0;a[51]=1}}}(n.head,h,c?n.loca.length:0);let f=Object.create(null);if(c){const e=int16(n.head.data[50],n.head.data[51]),t=function sanitizeGlyphLocations(e,t,i,a,s,r,n){let g,o,c;if(a){g=4;o=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};c=function fontItemEncodeLong(e,t,i){e[t]=i>>>24&255;e[t+1]=i>>16&255;e[t+2]=i>>8&255;e[t+3]=255&i}}else{g=2;o=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};c=function fontItemEncode(e,t,i){e[t]=i>>9&255;e[t+1]=i>>1&255}}const C=r?i+1:i,h=g*(1+C),l=new Uint8Array(h);l.set(e.data.subarray(0,h));e.data=l;const Q=t.data,E=Q.length,u=new Uint8Array(E);let d,f;const p=[];for(d=0,f=0;dE&&(e=E);p.push({index:d,offset:e,endOffset:0})}p.sort(((e,t)=>e.offset-t.offset));for(d=0;de.index-t.index));for(d=0;dn&&(n=e.sizeOfInstructions);w+=t;c(l,f,w)}if(0===w){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(d=0,f=g;di+w)t.data=u.subarray(0,i+w);else{t.data=new Uint8Array(i+w);t.data.set(u.subarray(0,w))}t.data.set(u.subarray(0,i),w);c(e.data,l.length-g,w+i)}else t.data=u.subarray(0,w);return{missingGlyphs:y,maxSizeOfInstructions:n}}(n.loca,n.glyf,h,e,d,Q,u);f=t.missingGlyphs;if(C>=65536&&n.maxp.length>=32){n.maxp.data[26]=t.maxSizeOfInstructions>>8;n.maxp.data[27]=255&t.maxSizeOfInstructions}}if(!n.hhea)throw new FormatError('Required "hhea" table is not found');if(0===n.hhea.data[10]&&0===n.hhea.data[11]){n.hhea.data[10]=255;n.hhea.data[11]=255}const p={unitsPerEm:int16(n.head.data[18],n.head.data[19]),yMax:signedInt16(n.head.data[42],n.head.data[43]),yMin:signedInt16(n.head.data[38],n.head.data[39]),ascent:signedInt16(n.hhea.data[4],n.hhea.data[5]),descent:signedInt16(n.hhea.data[6],n.hhea.data[7]),lineGap:signedInt16(n.hhea.data[8],n.hhea.data[9])};this.ascent=p.ascent/p.unitsPerEm;this.descent=p.descent/p.unitsPerEm;this.lineGap=p.lineGap/p.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;n.post&&function readPostScriptTable(e,i,a){const s=(t.start||0)+e.offset;t.pos=s;const r=s+e.length,n=t.getInt32();t.skip(28);let g,o,c=!0;switch(n){case 65536:g=Hi;break;case 131072:const e=t.getUint16();if(e!==a){c=!1;break}const s=[];for(o=0;o=32768){c=!1;break}s.push(e)}if(!c)break;const C=[],h=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let s=-1;t?s=a:void 0!==e[a]&&(s=e[a]);s>=0&&s>>0;let C=!1;if(g?.platformId!==s||g?.encodingId!==r){if(0!==s||0!==r&&1!==r&&3!==r)if(1===s&&0===r)C=!0;else if(3!==s||1!==r||!a&&g){if(i&&3===s&&0===r){C=!0;let i=!0;if(e>3;e.push(a);i=Math.max(a,i)}const a=[];for(let e=0;e<=i;e++)a.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let i=0;i<256;i++)if(0===e[i]){t.pos=a[0].idRangePos+2*i;Q=t.getUint16();h.push({charCode:i,glyphId:Q})}else{const s=a[e[i]];for(l=0;l>1;t.skip(6);const i=[];let a;for(a=0;a>1)-(e-a);s.offsetIndex=n;g=Math.max(g,n+s.end-s.start+1)}else s.offsetIndex=-1}const o=[];for(l=0;l>>0;for(l=0;l>>0,i=t.getInt32()>>>0;let a=t.getInt32()>>>0;for(let t=e;t<=i;t++)h.push({charCode:t,glyphId:a++})}}}h.sort((function(e,t){return e.charCode-t.charCode}));for(let e=1;e=61440&&t<=61695&&(t&=255);m[t]=e.glyphId}else for(const e of r)m[e.charCode]=e.glyphId;if(i.glyphNames&&(g.length||this.differences.length))for(let e=0;e<256;++e){if(!o&&void 0!==m[e])continue;const t=this.differences[e]||g[e];if(!t)continue;const a=i.glyphNames.indexOf(t);a>0&&hasGlyph(a)&&(m[e]=a)}}0===m.length&&(m[0]=0);let y=l-1;Q||(y=0);if(!i.cssFontInfo){const e=adjustMapping(m,hasGlyph,y,this.toUnicode);this.toFontChar=e.toFontChar;n.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,l)};n["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const i=t.getUint16();t.skip(60);const a=t.getUint16();if(i<4&&768&a)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(n["OS/2"],t)||(n["OS/2"]={tag:"OS/2",data:createOS2Table(i,e.charCodeToGlyphId,p)})}if(!c)try{o=new Stream(n["CFF "].data);g=new CFFParser(o,i,Ri).parse();g.duplicateFirstGlyph();const e=new CFFCompiler(g);n["CFF "].data=e.compile()}catch{warn("Failed to compile font "+i.loadedName)}if(n.name){const[t,a]=readNameTable(n.name);n.name.data=createNameTable(e,t);this.psName=t[0][6]||null;i.composite||function adjustTrueTypeToUnicode(e,t,i){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===i.length)return;if(e.defaultEncoding===li)return;for(const e of i)if(!isWinNameRecord(e))return;const a=li,s=[],r=wi();for(const e in a){const t=a[e];if(""===t)continue;const i=r[t];void 0!==i&&(s[e]=String.fromCharCode(i))}s.length>0&&e.toUnicode.amend(s)}(i,this.isSymbolicFont,a)}else n.name={tag:"name",data:createNameTable(this.name)};const w=new OpenTypeFileBuilder(r.version);for(const e in n)w.addTable(e,n[e].data);return w.toArray()}convert(e,t,i){i.fixedPitch=!1;i.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const i=[],a=wi();for(const s in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[s]))continue;const r=getUnicodeForGlyph(t[s],a);-1!==r&&(i[s]=String.fromCharCode(r))}i.length>0&&e.toUnicode.amend(i)}(i,i.builtInEncoding);let s=1;t instanceof CFFFont&&(s=t.numGlyphs-1);const r=t.getGlyphMapping(i);let n=null,g=r,o=null;if(!i.cssFontInfo){n=adjustMapping(r,t.hasGlyphId.bind(t),s,this.toUnicode);this.toFontChar=n.toFontChar;g=n.charCodeToGlyphId;o=n.toUnicodeExtraMap}const c=t.numGlyphs;function getCharCodes(e,t){let i=null;for(const a in e)t===e[a]&&(i||=[]).push(0|a);return i}function createCharCode(e,t){for(const i in e)if(t===e[i])return 0|i;n.charCodeToGlyphId[n.nextAvailableFontCharCode]=t;return n.nextAvailableFontCharCode++}const C=t.seacs;if(n&&C?.length){const e=i.fontMatrix||a,s=t.getCharset(),g=Object.create(null);for(let t in C){t|=0;const i=C[t],a=hi[i[2]],o=hi[i[3]],c=s.indexOf(a),h=s.indexOf(o);if(c<0||h<0)continue;const l={x:i[0]*e[0]+i[1]*e[2]+e[4],y:i[0]*e[1]+i[1]*e[3]+e[5]},Q=getCharCodes(r,t);if(Q)for(const e of Q){const t=n.charCodeToGlyphId,i=createCharCode(t,c),a=createCharCode(t,h);g[e]={baseFontCharCode:i,accentFontCharCode:a,accentOffset:l}}}i.seacMap=g}const h=i.fontMatrix?1/Math.max(...i.fontMatrix.slice(0,4).map(Math.abs)):1e3,l=new OpenTypeFileBuilder("OTTO");l.addTable("CFF ",t.data);l.addTable("OS/2",createOS2Table(i,g));l.addTable("cmap",createCmapTable(g,o,c));l.addTable("head","\0\0\0\0\0\0\0\0\0\0_<õ\0\0"+safeString16(h)+"\0\0\0\0ž\v~'\0\0\0\0ž\v~'\0\0"+safeString16(i.descent)+"ÿ"+safeString16(i.ascent)+string16(i.italicAngle?2:0)+"\0\0\0\0\0\0\0");l.addTable("hhea","\0\0\0"+safeString16(i.ascent)+safeString16(i.descent)+"\0\0ÿÿ\0\0\0\0\0\0"+safeString16(i.capHeight)+safeString16(Math.tan(i.italicAngle)*i.xHeight)+"\0\0\0\0\0\0\0\0\0\0\0\0"+string16(c));l.addTable("hmtx",function fontFieldsHmtx(){const e=t.charstrings,i=t.cff?t.cff.widths:null;let a="\0\0\0\0";for(let t=1,s=c;t=65520&&e<=65535?0:e>=62976&&e<=63743?bi()[e]||e:173===e?45:e}(i)}this.isType3Font&&(s=i);let C=null;if(this.seacMap?.[e]){c=!0;const t=this.seacMap[e];i=t.baseFontCharCode;C={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let h="";"number"==typeof i&&(i<=1114111?h=String.fromCodePoint(i):warn(`charToGlyph - invalid fontCharCode: ${i}`));if(this.missingFile&&this.vertical&&1===h.length){const e=Ji()[h.charCodeAt(0)];e&&(h=o=String.fromCharCode(e))}r=new fonts_Glyph(e,h,o,C,a,g,s,t,c);return this._glyphCache[e]=r}charsToGlyphs(e){let t=this._charsCache[e];if(t)return t;t=[];if(this.cMap){const i=Object.create(null),a=e.length;let s=0;for(;st.length%2==1,a=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let s=0,r=e.length;s55295&&(r<57344||r>65533)&&s++;if(this.toUnicode){const e=a(r);if(-1!==e){if(hasCurrentBufErrors()){t.push(i.join(""));i.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)i.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(i.join(""));i.length=0}i.push(String.fromCodePoint(r))}t.push(i.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(e=!1){return{error:this.error}}}const Ia=2,ca=3,Ca=4,ha=5,la=6,Ba=7;class Pattern{constructor(){unreachable("Cannot initialize Pattern.")}static parseShading(e,t,i,a,s){const r=e instanceof BaseStream?e.dict:e,n=r.get("ShadingType");try{switch(n){case Ia:case ca:return new RadialAxialShading(r,t,i,a,s);case Ca:case ha:case la:case Ba:return new MeshShading(e,t,i,a,s);default:throw new FormatError("Unsupported ShadingType: "+n)}}catch(e){if(e instanceof MissingDataException)throw e;warn(e);return new DummyShading}}}class BaseShading{static SMALL_NUMBER=1e-6;getIR(){unreachable("Abstract method `getIR` called.")}}class RadialAxialShading extends BaseShading{constructor(e,t,i,a,s){super();this.shadingType=e.get("ShadingType");let r=0;this.shadingType===Ia?r=4:this.shadingType===ca&&(r=6);this.coordsArr=e.getArray("Coords");if(!isNumberArray(this.coordsArr,r))throw new FormatError("RadialAxialShading: Invalid /Coords array.");const n=ColorSpace.parse({cs:e.getRaw("CS")||e.getRaw("ColorSpace"),xref:t,resources:i,pdfFunctionFactory:a,localColorSpaceCache:s});this.bbox=lookupNormalRect(e.getArray("BBox"),null);let g=0,o=1;const c=e.getArray("Domain");isNumberArray(c,2)&&([g,o]=c);let C=!1,h=!1;const l=e.getArray("Extend");(function isBooleanArray(e,t){return Array.isArray(e)&&(null===t||e.length===t)&&e.every((e=>"boolean"==typeof e))})(l,2)&&([C,h]=l);if(!(this.shadingType!==ca||C&&h)){const[e,t,i,a,s,r]=this.coordsArr,n=Math.hypot(e-a,t-s);i<=r+n&&r<=i+n&&warn("Unsupported radial gradient.")}this.extendStart=C;this.extendEnd=h;const Q=e.getRaw("Function"),E=a.createFromArray(Q),u=(o-g)/840,d=this.colorStops=[];if(g>=o||u<=0){info("Bad shading domain.");return}const f=new Float32Array(n.numComps),p=new Float32Array(1);let m,y=0;p[0]=g;E(p,0,f,0);let w=n.getRgb(f,0);const D=Util.makeHexColor(w[0],w[1],w[2]);d.push([0,D]);let b=1;p[0]=g+u;E(p,0,f,0);let F=n.getRgb(f,0),S=F[0]-w[0]+1,k=F[1]-w[1]+1,R=F[2]-w[2]+1,N=F[0]-w[0]-1,G=F[1]-w[1]-1,M=F[2]-w[2]-1;for(let e=2;e<840;e++){p[0]=g+e*u;E(p,0,f,0);m=n.getRgb(f,0);const t=e-y;S=Math.min(S,(m[0]-w[0]+1)/t);k=Math.min(k,(m[1]-w[1]+1)/t);R=Math.min(R,(m[2]-w[2]+1)/t);N=Math.max(N,(m[0]-w[0]-1)/t);G=Math.max(G,(m[1]-w[1]-1)/t);M=Math.max(M,(m[2]-w[2]-1)/t);if(!(N<=S&&G<=k&&M<=R)){const e=Util.makeHexColor(F[0],F[1],F[2]);d.push([b/840,e]);S=m[0]-F[0]+1;k=m[1]-F[1]+1;R=m[2]-F[2]+1;N=m[0]-F[0]-1;G=m[1]-F[1]-1;M=m[2]-F[2]-1;y=b;w=F}b=e;F=m}const U=Util.makeHexColor(F[0],F[1],F[2]);d.push([1,U]);let x="transparent";if(e.has("Background")){m=n.getRgb(e.get("Background"),0);x=Util.makeHexColor(m[0],m[1],m[2])}if(!C){d.unshift([0,x]);d[1][0]+=BaseShading.SMALL_NUMBER}if(!h){d.at(-1)[0]-=BaseShading.SMALL_NUMBER;d.push([1,x])}this.colorStops=d}getIR(){const{coordsArr:e,shadingType:t}=this;let i,a,s,r,n;if(t===Ia){a=[e[0],e[1]];s=[e[2],e[3]];r=null;n=null;i="axial"}else if(t===ca){a=[e[0],e[1]];s=[e[3],e[4]];r=e[2];n=e[5];i="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",i,this.bbox,this.colorStops,a,s,r,n]}}class MeshStreamReader{constructor(e,t){this.stream=e;this.context=t;this.buffer=0;this.bufferLength=0;const i=t.numComps;this.tmpCompsBuf=new Float32Array(i);const a=t.colorSpace.numComps;this.tmpCsCompsBuf=t.colorFn?new Float32Array(a):this.tmpCompsBuf}get hasData(){if(this.stream.end)return this.stream.pos0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){let t=this.buffer,i=this.bufferLength;if(32===e){if(0===i)return(this.stream.getByte()<<24|this.stream.getByte()<<16|this.stream.getByte()<<8|this.stream.getByte())>>>0;t=t<<24|this.stream.getByte()<<16|this.stream.getByte()<<8|this.stream.getByte();const e=this.stream.getByte();this.buffer=e&(1<>i)>>>0}if(8===e&&0===i)return this.stream.getByte();for(;i>i}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const e=this.context.bitsPerCoordinate,t=this.readBits(e),i=this.readBits(e),a=this.context.decode,s=e<32?1/((1<r?r:e;t=t>n?n:t;i=ie*s[t])):i;let n,g=-2;const o=[];for(const[e,t]of a.map(((e,t)=>[e,t])).sort((([e],[t])=>e-t)))if(-1!==e)if(e===g+1){n.push(r[t]);g+=1}else{g=e;n=[r[t]];o.push(e,n)}return o}(e),i=new Dict(null);i.set("BaseFont",Name.get(e));i.set("Type",Name.get("Font"));i.set("Subtype",Name.get("CIDFontType2"));i.set("Encoding",Name.get("Identity-H"));i.set("CIDToGIDMap",Name.get("Identity"));i.set("W",t);i.set("FirstChar",t[0]);i.set("LastChar",t.at(-2)+t.at(-1).length-1);const a=new Dict(null);i.set("FontDescriptor",a);const s=new Dict(null);s.set("Ordering","Identity");s.set("Registry","Adobe");s.set("Supplement",0);i.set("CIDSystemInfo",s);return i}class PostScriptParser{constructor(e){this.lexer=e;this.operators=[];this.token=null;this.prev=null}nextToken(){this.prev=this.token;this.token=this.lexer.getToken()}accept(e){if(this.token.type===e){this.nextToken();return!0}return!1}expect(e){if(this.accept(e))return!0;throw new FormatError(`Unexpected symbol: found ${this.token.type} expected ${e}.`)}parse(){this.nextToken();this.expect(as.LBRACE);this.parseBlock();this.expect(as.RBRACE);return this.operators}parseBlock(){for(;;)if(this.accept(as.NUMBER))this.operators.push(this.prev.value);else if(this.accept(as.OPERATOR))this.operators.push(this.prev.value);else{if(!this.accept(as.LBRACE))return;this.parseCondition()}}parseCondition(){const e=this.operators.length;this.operators.push(null,null);this.parseBlock();this.expect(as.RBRACE);if(this.accept(as.IF)){this.operators[e]=this.operators.length;this.operators[e+1]="jz"}else{if(!this.accept(as.LBRACE))throw new FormatError("PS Function: error parsing conditional.");{const t=this.operators.length;this.operators.push(null,null);const i=this.operators.length;this.parseBlock();this.expect(as.RBRACE);this.expect(as.IFELSE);this.operators[t]=this.operators.length;this.operators[t+1]="j";this.operators[e]=i;this.operators[e+1]="jz"}}}}const as={LBRACE:0,RBRACE:1,NUMBER:2,OPERATOR:3,IF:4,IFELSE:5};class PostScriptToken{static get opCache(){return shadow(this,"opCache",Object.create(null))}constructor(e,t){this.type=e;this.value=t}static getOperator(e){return PostScriptToken.opCache[e]||=new PostScriptToken(as.OPERATOR,e)}static get LBRACE(){return shadow(this,"LBRACE",new PostScriptToken(as.LBRACE,"{"))}static get RBRACE(){return shadow(this,"RBRACE",new PostScriptToken(as.RBRACE,"}"))}static get IF(){return shadow(this,"IF",new PostScriptToken(as.IF,"IF"))}static get IFELSE(){return shadow(this,"IFELSE",new PostScriptToken(as.IFELSE,"IFELSE"))}}class PostScriptLexer{constructor(e){this.stream=e;this.nextChar();this.strBuf=[]}nextChar(){return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(t<0)return Bt;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return new PostScriptToken(as.NUMBER,this.getNumber());case 123:this.nextChar();return PostScriptToken.LBRACE;case 125:this.nextChar();return PostScriptToken.RBRACE}const i=this.strBuf;i.length=0;i[0]=String.fromCharCode(t);for(;(t=this.nextChar())>=0&&(t>=65&&t<=90||t>=97&&t<=122);)i.push(String.fromCharCode(t));const a=i.join("");switch(a.toLowerCase()){case"if":return PostScriptToken.IF;case"ifelse":return PostScriptToken.IFELSE;default:return PostScriptToken.getOperator(a)}}getNumber(){let e=this.currentChar;const t=this.strBuf;t.length=0;t[0]=String.fromCharCode(e);for(;(e=this.nextChar())>=0&&(e>=48&&e<=57||45===e||46===e);)t.push(String.fromCharCode(e));const i=parseFloat(t.join(""));if(isNaN(i))throw new FormatError(`Invalid floating point number: ${i}`);return i}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,i){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,i){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,i){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,i){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#F=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#S(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#k(){return!(this._imageCache.size+e)):null}class PDFFunction{static getSampleArray(e,t,i,a){let s,r,n=1;for(s=0,r=e.length;s>o)*C;c&=(1<i?e=i:e0&&(l=r[h-1]);let Q=a[1];h>1,c=s.length>>1,C=new PostScriptEvaluator(g),h=Object.create(null);let l=8192;const Q=new Float32Array(c);return function constructPostScriptFn(e,t,i,a){let s,n,g="";const E=Q;for(s=0;se&&(n=e)}d[s]=n}if(l>0){l--;h[g]=d}i.set(d,a)}}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}class PostScriptStack{static MAX_STACK_SIZE=100;constructor(e){this.stack=e?Array.from(e):[]}push(e){if(this.stack.length>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");this.stack.push(e)}pop(){if(this.stack.length<=0)throw new Error("PostScript function stack underflow.");return this.stack.pop()}copy(e){if(this.stack.length+e>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");const t=this.stack;for(let i=t.length-e,a=e-1;a>=0;a--,i++)t.push(t[i])}index(e){this.push(this.stack[this.stack.length-e-1])}roll(e,t){const i=this.stack,a=i.length-e,s=i.length-1,r=a+(t-Math.floor(t/e)*e);for(let e=a,t=s;e0?t.push(n<>g);break;case"ceiling":n=t.pop();t.push(Math.ceil(n));break;case"copy":n=t.pop();t.copy(n);break;case"cos":n=t.pop();t.push(Math.cos(n%360/180*Math.PI));break;case"cvi":n=0|t.pop();t.push(n);break;case"cvr":break;case"div":g=t.pop();n=t.pop();t.push(n/g);break;case"dup":t.copy(1);break;case"eq":g=t.pop();n=t.pop();t.push(n===g);break;case"exch":t.roll(2,1);break;case"exp":g=t.pop();n=t.pop();t.push(n**g);break;case"false":t.push(!1);break;case"floor":n=t.pop();t.push(Math.floor(n));break;case"ge":g=t.pop();n=t.pop();t.push(n>=g);break;case"gt":g=t.pop();n=t.pop();t.push(n>g);break;case"idiv":g=t.pop();n=t.pop();t.push(n/g|0);break;case"index":n=t.pop();t.index(n);break;case"le":g=t.pop();n=t.pop();t.push(n<=g);break;case"ln":n=t.pop();t.push(Math.log(n));break;case"log":n=t.pop();t.push(Math.log10(n));break;case"lt":g=t.pop();n=t.pop();t.push(n=t?new AstLiteral(t):e.max<=t?e:new AstMin(e,t)}class PostScriptCompiler{compile(e,t,i){const a=[],s=[],r=t.length>>1,n=i.length>>1;let g,o,c,C,h,l,Q,E,u=0;for(let e=0;et.min){g.unshift("Math.max(",r,", ");g.push(")")}if(n4){a=!0;t=0}else{a=!1;t=1}const o=[];for(r=0;r=0&&"ET"===gs[e];--e)gs[e]="EN";for(let e=r+1;e0&&(t=gs[r-1]);let i=h;e+1E&&isOdd(E)&&(d=E)}for(E=u;E>=d;--E){let e=-1;for(r=0,n=o.length;r=0){reverseValues(ns,e,r);e=-1}}else e<0&&(e=r);e>=0&&reverseValues(ns,e,o.length)}for(r=0,n=ns.length;r"!==e||(ns[r]="")}return createBidiText(ns.join(""),a)}const os={style:"normal",weight:"normal"},Is={style:"normal",weight:"bold"},cs={style:"italic",weight:"normal"},Cs={style:"italic",weight:"bold"},hs=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:os,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:Is,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:cs,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:Cs,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:os,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:Is,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:cs,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:Cs,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:os,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:Is,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:cs,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:Cs,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:os,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:Is,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:cs,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:Cs,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:os,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:Is,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:cs,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:Cs,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:os}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}]]),ls=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter((e=>!t.has(e.toLowerCase()))).join(" ")}function generateFont({alias:e,local:t,path:i,fallback:a,style:s,ultimate:r},n,g,o=!0,c=!0,C=""){const h={style:null,ultimate:null};if(t){const e=C?` ${C}`:"";for(const i of t)n.push(`local(${i}${e})`)}if(e){const t=hs.get(e),r=C||function getStyleToAppend(e){switch(e){case Is:return"Bold";case cs:return"Italic";case Cs:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(s);Object.assign(h,generateFont(t,n,g,o&&!a,c&&!i,r))}s&&(h.style=s);r&&(h.ultimate=r);if(o&&a){const e=hs.get(a),{ultimate:t}=generateFont(e,n,g,o,c&&!i,C);h.ultimate||=t}c&&i&&g&&n.push(`url(${g}${i})`);return h}function getFontSubstitution(e,t,i,a,s,r){if(a.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==r&&"Type1"!==r||!/^[A-Z]{6}\+/.test(a)||(a=a.slice(7));const n=a=normalizeFontName(a);let g=e.get(n);if(g)return g;let o=hs.get(a);if(!o)for(const[e,t]of ls)if(a.startsWith(e)){a=`${t}${a.substring(e.length)}`;o=hs.get(a);break}let c=!1;if(!o){o=hs.get(s);c=!0}const C=`${t.getDocId()}_s${t.createFontId()}`;if(!o){if(!validateFontName(a)){warn(`Cannot substitute the font because of its name: ${a}`);e.set(n,null);return null}const t=/bold/gi.test(a),i=/oblique|italic/gi.test(a),s=t&&i&&Cs||t&&Is||i&&cs||os;g={css:`"${getFamilyName(a)}",${C}`,guessFallback:!0,loadedName:C,baseFontName:a,src:`local(${a})`,style:s};e.set(n,g);return g}const h=[];c&&validateFontName(a)&&h.push(`local(${a})`);const{style:l,ultimate:Q}=generateFont(o,h,i),E=null===Q,u=E?"":`,${Q}`;g={css:`"${getFamilyName(a)}",${C}${u}`,guessFallback:E,loadedName:C,baseFontName:a,src:h.join(","),style:l};e.set(n,g);return g}class ImageResizer{static#R=2048;static#y=FeatureTest.isImageDecoderSupported;constructor(e,t){this._imgData=e;this._isMask=t}static get canUseImageDecoder(){return shadow(this,"canUseImageDecoder",this.#y?ImageDecoder.isTypeSupported("image/bmp"):Promise.resolve(!1))}static needsToBeResized(e,t){if(e<=this.#R&&t<=this.#R)return!1;const{MAX_DIM:i}=this;if(e>i||t>i)return!0;const a=e*t;if(this._hasMaxArea)return a>this.MAX_AREA;if(a(this.MAX_AREA=this.#R**2)}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,65537,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#R,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#y=t}static _areGoodDims(e,t){try{const i=new OffscreenCanvas(e,t),a=i.getContext("2d");a.fillRect(0,0,1,1);const s=a.getImageData(0,0,1,1).data[3];i.width=i.height=1;return 0!==s}catch{return!1}}static _guessMax(e,t,i,a){for(;e+i+1pt){const e=this.#N();if(e)return e}const a=this._encodeBMP();let s,r;if(await ImageResizer.canUseImageDecoder){s=new ImageDecoder({data:a,type:"image/bmp",preferAnimation:!1,transfer:[a.buffer]});r=s.decode().catch((e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))})).finally((()=>{s.close()}))}else r=createImageBitmap(new Blob([a.buffer],{type:"image/bmp"}));const{MAX_AREA:n,MAX_DIM:g}=ImageResizer,o=Math.max(t/g,i/g,Math.sqrt(t*i/n)),c=Math.max(o,2),C=Math.round(10*(o+1.25))/10/c,h=Math.floor(Math.log2(C)),l=new Array(h+2).fill(2);l[0]=c;l.splice(-1,1,C/(1<>n,o=a>>n;let c,C=a;try{c=new Uint8Array(r)}catch{let e=Math.floor(Math.log2(r+1));for(;;)try{c=new Uint8Array(2**e-1);break}catch{e-=1}C=Math.floor((2**e-1)/(4*i));const t=i*C*4;t>n;e>3,n=i+3&-4;if(i!==n){const e=new Uint8Array(n*t);let a=0;for(let r=0,g=t*i;r>>8;t[i++]=255&s}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();i=t.byteLength}const a=i>>2,s=i-4*a,r=new Uint32Array(t.buffer,0,a);let n=0,g=0,o=this.h1,c=this.h2;const C=3432918353,h=461845907,l=11601,Q=13715;for(let e=0;e>>17;n=n*h&Qs|n*Q&Es;o^=n;o=o<<13|o>>>19;o=5*o+3864292196}else{g=r[e];g=g*C&Qs|g*l&Es;g=g<<15|g>>>17;g=g*h&Qs|g*Q&Es;c^=g;c=c<<13|c>>>19;c=5*c+3864292196}n=0;switch(s){case 3:n^=t[4*a+2]<<16;case 2:n^=t[4*a+1]<<8;case 1:n^=t[4*a];n=n*C&Qs|n*l&Es;n=n<<15|n>>>17;n=n*h&Qs|n*Q&Es;1&a?o^=n:c^=n}this.h1=o;this.h2=c}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&Qs|36045*e&Es;t=4283543511*t&Qs|(2950163797*(t<<16|e>>>16)&Qs)>>>16;e^=t>>>1;e=444984403*e&Qs|60499*e&Es;t=3301882366*t&Qs|(3120437893*(t<<16|e>>>16)&Qs)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}function addState(e,t,i,a,s){let r=e;for(let e=0,i=t.length-1;e1e3){c=Math.max(c,l);Q+=h+2;l=0;h=0}C.push({transform:t,x:l,y:Q,w:i.width,h:i.height});l+=i.width+2;h=Math.max(h,i.height)}const E=Math.max(c,l)+1,u=Q+h+1,d=new Uint8Array(E*u*4),f=E<<2;for(let e=0;e=0;){t[r-4]=t[r];t[r-3]=t[r+1];t[r-2]=t[r+2];t[r-1]=t[r+3];t[r+i]=t[r+i-4];t[r+i+1]=t[r+i-3];t[r+i+2]=t[r+i-2];t[r+i+3]=t[r+i-1];r-=f}}const p={width:E,height:u};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(E,u);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(d.buffer),E,u),0,0);p.bitmap=e.transferToImageBitmap();p.data=null}else{p.kind=S;p.data=d}i.splice(r,4*o,$e);a.splice(r,4*o,[p,C]);return r+1}));addState(us,[MA,xA,Ze,UA],null,(function iterateImageMaskGroup(e,t){const i=e.fnArray,a=(t-(e.iCurr-3))%4;switch(a){case 0:return i[t]===MA;case 1:return i[t]===xA;case 2:return i[t]===Ze;case 3:return i[t]===UA}throw new Error(`iterateImageMaskGroup - invalid pos: ${a}`)}),(function foundImageMaskGroup(e,t){const i=e.fnArray,a=e.argsArray,s=e.iCurr,r=s-3,n=s-2,g=s-1;let o=Math.floor((t-r)/4);if(o<10)return t-(t-r)%4;let c,C,h=!1;const l=a[g][0],Q=a[n][0],E=a[n][1],u=a[n][2],d=a[n][3];if(E===u){h=!0;c=n+4;let e=g+4;for(let t=1;t=4&&i[r-4]===i[n]&&i[r-3]===i[g]&&i[r-2]===i[o]&&i[r-1]===i[c]&&a[r-4][0]===C&&a[r-4][1]===h){l++;Q-=5}let E=Q+4;for(let e=1;e=i)break}a=(a||us)[e[t]];if(a&&!Array.isArray(a)){r.iCurr=t;t++;if(!a.checkFn||(0,a.checkFn)(r)){s=a;a=null}else a=null}else t++}this.state=a;this.match=s;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&E?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}set isOffscreenCanvasSupported(e){this.optimizer.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===UA||e===ee))&&this.flush()}addImageOps(e,t,i,a=!1){if(a){this.addOp(MA);this.addOp(GA,[[["SMask",!1]]])}void 0!==i&&this.addOp(Ye,["OC",i]);this.addOp(e,t);void 0!==i&&this.addOp(ve,[]);a&&this.addOp(UA)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(wA,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,i=e.length;ta&&(e=a);return e}function resizeImageMask(e,t,i,a,s,r){const n=s*r;let g;g=t<=8?new Uint8Array(n):t<=16?new Uint16Array(n):new Uint32Array(n);const o=i/s,c=a/r;let C,h,l,Q,E=0;const u=new Uint16Array(s),d=i;for(C=0;C0&&Number.isInteger(i.height)&&i.height>0&&(i.width!==l||i.height!==Q)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");l=i.width;Q=i.height}if(l<1||Q<1)throw new FormatError(`Invalid image width: ${l} or height: ${Q}`);this.width=l;this.height=Q;this.interpolate=c.get("I","Interpolate");this.imageMask=c.get("IM","ImageMask")||!1;this.matte=c.get("Matte")||!1;let E=i.bitsPerComponent;if(!E){E=c.get("BPC","BitsPerComponent");if(!E){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);E=1}}this.bpc=E;if(!this.imageMask){let s=c.getRaw("CS")||c.getRaw("ColorSpace");const r=!!s;if(r)this.jpxDecoderOptions?.smaskInData&&(s=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)s=Name.get("DeviceRGBA");else switch(i.numComps){case 1:s=Name.get("DeviceGray");break;case 3:s=Name.get("DeviceRGB");break;case 4:s=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${i.numComps} color components not supported.`)}this.colorSpace=ColorSpace.parse({cs:s,xref:e,resources:a?t:null,pdfFunctionFactory:g,localColorSpaceCache:o});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=r?this.numComp:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=c.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,E)||n&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<>3)*i,g=e.byteLength;let o,c;if(!a||s&&!(n===g))if(s){o=new Uint8Array(n);o.set(e);o.fill(255,g)}else o=new Uint8Array(e);else o=e;if(s)for(c=0;c>7&1;n[l+1]=h>>6&1;n[l+2]=h>>5&1;n[l+3]=h>>4&1;n[l+4]=h>>3&1;n[l+5]=h>>2&1;n[l+6]=h>>1&1;n[l+7]=1&h;l+=8}if(l>=1}}}}else{let i=0;h=0;for(l=0,C=r;l>a;s<0?s=0:s>c&&(s=c);n[l]=s;h&=(1<n[a+1]){t=255;break}}g[C]=t}}}if(g)for(C=0,l=3,h=t*a;C>3,C=t&&ImageResizer.needsToBeResized(i,a);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){s.kind=S;const e=s.data=await this.getImageBytes(g*n*4,{});return t?C?ImageResizer.createImage(s,!1):this.createBitmap(S,i,a,e):s}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===o?e=b:"DeviceRGB"!==this.colorSpace.name||8!==o||this.needsDecode||(e=F);if(e&&!this.smask&&!this.mask&&i===n&&a===g){const r=await this.#G(n,g);if(r)return r;const o=await this.getImageBytes(g*c,{});if(t)return C?ImageResizer.createImage({data:o,kind:e,width:i,height:a,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,n,g,o);s.kind=e;s.data=o;if(this.needsDecode){assert(e===b,"PDFImage.createImageData: The image must be grayscale.");const t=s.data;for(let e=0,i=t.length;e>3,n=await this.getImageBytes(a*r,{internal:!0}),g=this.getComponents(n);let o,c;if(1===s){c=i*a;if(this.needsDecode)for(o=0;o0&&t.args[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checkedh){const e="Image exceeded maximum allowed size and was removed.";if(this.options.ignoreErrors){warn(e);return}throw new Error(e)}let l;g.has("OC")&&(l=await this.parseMarkedContentProps(g.get("OC"),e));let Q,E;if(g.get("IM","ImageMask")||!1){const e=g.get("I","Interpolate"),i=c+7>>3,n=t.getBytes(i*C),h=g.getArray("D","Decode");if(this.parsingType3Font){Q=PDFImage.createRawMask({imgArray:n,width:c,height:C,imageIsFromDecodeStream:t instanceof DecodeStream,inverseDecode:h?.[0]>0,interpolate:e});Q.cached=!!s;E=[Q];a.addImageOps(Ze,E,l);if(s){const e={fn:Ze,args:E,optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}Q=await PDFImage.createMask({imgArray:n,width:c,height:C,imageIsFromDecodeStream:t instanceof DecodeStream,inverseDecode:h?.[0]>0,interpolate:e,isOffscreenCanvasSupported:this.options.isOffscreenCanvasSupported});if(Q.isSingleOpaquePixel){a.addImageOps(tt,[],l);if(s){const e={fn:tt,args:[],optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}const u=`mask_${this.idFactory.createObjId()}`;a.addDependency(u);Q.dataLen=Q.bitmap?Q.width*Q.height*4:Q.data.length;this._sendImgData(u,Q);E=[{data:u,width:Q.width,height:Q.height,interpolate:Q.interpolate,count:1}];a.addImageOps(Ze,E,l);if(s){const e={objId:u,fn:Ze,args:E,optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}const u=g.has("SMask")||g.has("Mask");if(i&&c+C<200&&!u){try{const s=new PDFImage({xref:this.xref,res:e,image:t,isInline:i,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:n});Q=await s.createImageData(!0,!1);a.isOffscreenCanvasSupported=this.options.isOffscreenCanvasSupported;a.addImageOps(_e,[Q],l)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!this.options.ignoreErrors)throw new Error(t);warn(t)}return}let d=`img_${this.idFactory.createObjId()}`,f=!1;if(this.parsingType3Font)d=`${this.idFactory.getDocId()}_type3_${d}`;else if(s&&o){f=this.globalImageCache.shouldCache(o,this.pageIndex);if(f){assert(!i,"Cannot cache an inline image globally.");d=`${this.idFactory.getDocId()}_${d}`}}a.addDependency(d);E=[d,c,C];a.addImageOps(ze,E,l,u);if(f){if(this.globalImageCache.hasDecodeFailed(o)){this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0});this._sendImgData(d,null,f);return}if(c*C>25e4||u){const e=await this.handler.sendWithPromise("commonobj",[d,"CopyLocalImage",{imageRef:o}]);if(e){this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0});this.globalImageCache.addByteSize(o,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:i,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:n}).then((async e=>{Q=await e.createImageData(!1,this.options.isOffscreenCanvasSupported);Q.dataLen=Q.bitmap?Q.width*Q.height*4:Q.data.length;Q.ref=o;f&&this.globalImageCache.addByteSize(o,Q.dataLen);return this._sendImgData(d,Q,f)})).catch((e=>{warn(`Unable to decode image "${d}": "${e}".`);o&&this.globalImageCache.addDecodeFailed(o);return this._sendImgData(d,null,f)}));if(s){const e={objId:d,fn:ze,args:E,optionalContent:l,hasMask:u};r.set(s,o,e);if(o){this._regionalImageCache.set(null,o,e);f&&this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0})}}}handleSMask(e,t,i,a,s,r){const n=e.get("G"),g={subtype:e.get("S").name,backdrop:e.get("BC")},o=e.get("TR");if(isPDFFunction(o)){const e=this._pdfFunctionFactory.create(o),t=new Uint8Array(256),i=new Float32Array(1);for(let a=0;a<256;a++){i[0]=a/255;e(i,0,i,0);t[a]=255*i[0]|0}g.transferMap=t}return this.buildFormXObject(t,n,g,i,a,s.state.clone(),r)}handleTransferFunction(e){let t;if(Array.isArray(e))t=e;else{if(!isPDFFunction(e))return null;t=[e]}const i=[];let a=0,s=0;for(const e of t){const t=this.xref.fetchIfRef(e);a++;if(isName(t,"Identity")){i.push(null);continue}if(!isPDFFunction(t))return null;const r=this._pdfFunctionFactory.create(t),n=new Uint8Array(256),g=new Float32Array(1);for(let e=0;e<256;e++){g[0]=e/255;r(g,0,g,0);n[e]=255*g[0]|0}i.push(n);s++}return 1!==a&&4!==a||0===s?null:i}handleTilingType(e,t,i,a,s,r,n,g){const o=new OperatorList,c=Dict.merge({xref:this.xref,dictArray:[s.get("Resources"),i]});return this.getOperatorList({stream:a,task:n,resources:c,operatorList:o}).then((function(){const i=o.getIR(),a=getTilingPatternIR(i,s,t);r.addDependencies(o.dependencies);r.addOp(e,a);s.objId&&g.set(null,s.objId,{operatorListIR:i,dict:s})})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}}))}async handleSetFont(e,t,i,a,s,r,n=null,g=null){const o=t?.[0]instanceof Name?t[0].name:null;let c=await this.loadFont(o,i,e,n,g);if(c.font.isType3Font)try{await c.loadType3Data(this,e,s);a.addDependencies(c.type3Dependencies)}catch(e){c=new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Type3 font load error: ${e}`),dict:c.font,evaluatorOptions:this.options})}r.font=c.font;c.send(this.handler);return c.loadedName}handleText(e,t){const i=t.font,a=i.charsToGlyphs(e);if(i.data){(!!(t.textRenderingMode&D)||"Pattern"===t.fillColorSpace.name||i.disableFontFace||this.options.disableFontFace)&&PartialEvaluator.buildFontPaths(i,a,this.handler,this.options)}return a}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:i,cacheKey:a,task:s,stateManager:r,localGStateCache:n,localColorSpaceCache:g}){const o=t.objId;let c=!0;const C=[];let h=Promise.resolve();for(const a of t.getKeys()){const n=t.get(a);switch(a){case"Type":break;case"LW":case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":C.push([a,n]);break;case"Font":c=!1;h=h.then((()=>this.handleSetFont(e,null,n[0],i,s,r.state).then((function(e){i.addDependency(e);C.push([a,[e,n[1]]])}))));break;case"BM":C.push([a,normalizeBlendMode(n)]);break;case"SMask":if(isName(n,"None")){C.push([a,!1]);break}if(n instanceof Dict){c=!1;h=h.then((()=>this.handleSMask(n,e,i,s,r,g)));C.push([a,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(n);C.push([a,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+a);break;default:info("Unknown graphic state operator "+a)}}await h;C.length>0&&i.addOp(GA,[C]);c&&n.set(a,o,C)}loadFont(e,t,i,a=null,s=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t,evaluatorOptions:this.options});let r;if(t)t instanceof Ref&&(r=t);else{const t=i.get("Font");t&&(r=t.getRaw(e))}if(r){if(this.type3FontRefs?.has(r))return errorFont();if(this.fontCache.has(r))return this.fontCache.get(r);try{t=this.xref.fetchIfRef(r)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=a||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:n,resolve:g}=Promise.withResolvers();let o;try{o=this.preEvaluateFont(t);o.cssFontInfo=s}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:c,hash:C}=o,h=r instanceof Ref;let l;if(C&&c instanceof Dict){const e=c.fontAliases||=Object.create(null);if(e[C]){const t=e[C].aliasRef;if(h&&t&&this.fontCache.has(t)){this.fontCache.putAlias(r,t);return this.fontCache.get(r)}}else e[C]={fontID:this.idFactory.createFontId()};h&&(e[C].aliasRef=r);l=e[C].fontID}else l=this.idFactory.createFontId();assert(l?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(h)this.fontCache.put(r,n);else{t.cacheKey=`cacheKey_${l}`;this.fontCache.put(t.cacheKey,n)}t.loadedName=`${this.idFactory.getDocId()}_${l}`;this.translateFont(o).then((e=>{g(new TranslatedFont({loadedName:t.loadedName,font:e,dict:t,evaluatorOptions:this.options}))})).catch((e=>{warn(`loadFont - translateFont failed: "${e}".`);g(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e instanceof Error?e.message:e),dict:t,evaluatorOptions:this.options}))}));return n}buildPath(e,t,i,a=!1){const s=e.length-1;i||(i=[]);if(s<0||e.fnArray[s]!==it){if(a){warn(`Encountered path operator "${t}" inside of a text object.`);e.addOp(MA,null)}let s;switch(t){case TA:const e=i[0]+i[2],t=i[1]+i[3];s=[Math.min(i[0],e),Math.min(i[1],t),Math.max(i[0],e),Math.max(i[1],t)];break;case LA:case HA:s=[i[0],i[1],i[0],i[1]];break;default:s=[1/0,1/0,-1/0,-1/0]}e.addOp(it,[[t],i,s]);a&&e.addOp(UA,null)}else{const a=e.argsArray[s];a[0].push(t);a[1].push(...i);const r=a[2];switch(t){case TA:const e=i[0]+i[2],t=i[1]+i[3];r[0]=Math.min(r[0],i[0],e);r[1]=Math.min(r[1],i[1],t);r[2]=Math.max(r[2],i[0],e);r[3]=Math.max(r[3],i[1],t);break;case LA:case HA:r[0]=Math.min(r[0],i[0]);r[1]=Math.min(r[1],i[1]);r[2]=Math.max(r[2],i[0]);r[3]=Math.max(r[3],i[1])}}}parseColorSpace({cs:e,resources:t,localColorSpaceCache:i}){return ColorSpace.parseAsync({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:i}).catch((e=>{if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}))}parseShading({shading:e,resources:t,localColorSpaceCache:i,localShadingPatternCache:a}){let s,r=a.get(e);if(r)return r;try{s=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,i).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);a.set(e,null);return null}throw t}r=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(r=`${this.idFactory.getDocId()}_type3_${r}`);a.set(e,r);this.parsingType3Font?this.handler.send("commonobj",[r,"Pattern",s]):this.handler.send("obj",[r,this.pageIndex,"Pattern",s]);return r}handleColorN(e,t,i,a,s,r,n,g,o,c){const C=i.pop();if(C instanceof Name){const h=s.getRaw(C.name),l=h instanceof Ref&&o.getByRef(h);if(l)try{const s=a.base?a.base.getRgb(i,0):null,r=getTilingPatternIR(l.operatorListIR,l.dict,s);e.addOp(t,r);return}catch{}const Q=this.xref.fetchIfRef(h);if(Q){const s=Q instanceof BaseStream?Q.dict:Q,C=s.get("PatternType");if(C===fs){const g=a.base?a.base.getRgb(i,0):null;return this.handleTilingType(t,g,r,Q,s,e,n,o)}if(C===ps){const i=s.get("Shading"),a=this.parseShading({shading:i,resources:r,localColorSpaceCache:g,localShadingPatternCache:c});if(a){const i=lookupMatrix(s.getArray("Matrix"),null);e.addOp(t,["Shading",a,i])}return}throw new FormatError(`Unknown PatternType: ${C}`)}}throw new FormatError(`Unknown PatternName: ${C}`)}_parseVisibilityExpression(e,t,i){if(++t>10){warn("Visibility expression is too deeply nested");return}const a=e.length,s=this.xref.fetchIfRef(e[0]);if(!(a<2)&&s instanceof Name){switch(s.name){case"And":case"Or":case"Not":i.push(s.name);break;default:warn(`Invalid operator ${s.name} in visibility expression`);return}for(let s=1;s0)return{type:"OCMD",expression:t}}const t=i.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const i of t)e.push(i.toString());else e.push(t.objId);return{type:a,ids:e,policy:i.get("P")instanceof Name?i.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:a,id:t.toString()}}return null}getOperatorList({stream:e,task:t,resources:i,operatorList:a,initialState:s=null,fallbackFontDict:r=null}){i||=Dict.empty;s||=new EvalState;if(!a)throw new Error('getOperatorList: missing "operatorList" parameter');const n=this,g=this.xref;let o=!1;const c=new LocalImageCache,C=new LocalColorSpaceCache,h=new LocalGStateCache,l=new LocalTilingPatternCache,Q=new Map,E=i.get("XObject")||Dict.empty,u=i.get("Pattern")||Dict.empty,d=new StateManager(s),f=new EvaluatorPreprocessor(e,g,d),p=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=f.savedStatesDepth;e0&&a.addOp(GA,[t]);e=null;continue}}next(new Promise((function(e,s){if(!S)throw new FormatError("GState must be referred to by name.");const r=i.get("ExtGState");if(!(r instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const g=r.get(F);if(!(g instanceof Dict))throw new FormatError("GState should be a dictionary.");n.setGState({resources:i,gState:g,operatorList:a,cacheKey:F,task:t,stateManager:d,localGStateCache:h,localColorSpaceCache:C}).then(e,s)})).catch((function(e){if(!(e instanceof AbortException)){if(!n.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}})));return;case LA:case HA:case JA:case YA:case vA:case KA:case TA:n.buildPath(a,s,e,o);continue;case Le:case He:case Ke:case Te:continue;case Ye:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);a.addOp(Ye,["OC",null]);continue}if("OC"===e[0].name){next(n.parseMarkedContentProps(e[1],i).then((e=>{a.addOp(Ye,["OC",e])})).catch((e=>{if(!(e instanceof AbortException)){if(!n.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);a.addOp(Ye,["OC",null])}})));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(w=0,D=e.length;w{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${t.name}" task: "${e}".`);closePendingRestoreOPS()}}))}getTextContent({stream:e,task:t,resources:s,stateManager:r=null,includeMarkedContent:n=!1,sink:g,seenStyles:o=new Set,viewBox:c,lang:C=null,markedContentData:h=null,disableNormalization:l=!1,keepWhiteSpace:Q=!1}){s||=Dict.empty;r||=new StateManager(new TextState);n&&(h||={level:0});const E={items:[],styles:Object.create(null),lang:C},u={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},d=[" "," "];let f=0;function saveLastChar(e){const t=(f+1)%2,i=" "!==d[f]&&" "===d[t];d[f]=e;f=t;return!Q&&i}function shouldAddWhitepsace(){return!Q&&" "!==d[f]&&" "===d[(f+1)%2]}function resetLastChars(){d[0]=d[1]=" ";f=0}const p=this,m=this.xref,y=[];let w=null;const D=new LocalImageCache,b=new LocalGStateCache,F=new EvaluatorPreprocessor(e,m,r);let S;function pushWhitespace({width:e=0,height:t=0,transform:i=u.prevTransform,fontName:a=u.fontName}){E.items.push({str:" ",dir:"ltr",width:e,height:t,transform:i,fontName:a,hasEOL:!1})}function getCurrentTextTransform(){const e=S.font,t=[S.fontSize*S.textHScale,0,0,S.fontSize,0,S.textRise];if(e.isType3Font&&(S.fontSize<=1||e.isCharBBox)&&!isArrayEqual(S.fontMatrix,a)){const i=e.bbox[3]-e.bbox[1];i>0&&(t[3]*=i*S.fontMatrix[3])}return Util.transform(S.ctm,Util.transform(S.textMatrix,t))}function ensureTextContentItem(){if(u.initialized)return u;const{font:e,loadedName:t}=S;if(!o.has(t)){o.add(t);E.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(p.options.fontExtraProperties&&e.systemFontInfo){const i=E.styles[t];i.fontSubstitution=e.systemFontInfo.css;i.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}u.fontName=t;const i=u.transform=getCurrentTextTransform();if(e.vertical){u.width=u.totalWidth=Math.hypot(i[0],i[1]);u.height=u.totalHeight=0;u.vertical=!0}else{u.width=u.totalWidth=0;u.height=u.totalHeight=Math.hypot(i[2],i[3]);u.vertical=!1}const a=Math.hypot(S.textLineMatrix[0],S.textLineMatrix[1]),s=Math.hypot(S.ctm[0],S.ctm[1]);u.textAdvanceScale=s*a;const{fontSize:r}=S;u.trackingSpaceMin=.102*r;u.notASpace=.03*r;u.negativeSpaceMax=-.2*r;u.spaceInFlowMin=.102*r;u.spaceInFlowMax=.6*r;u.hasEOL=!1;u.initialized=!0;return u}function updateAdvanceScale(){if(!u.initialized)return;const e=Math.hypot(S.textLineMatrix[0],S.textLineMatrix[1]),t=Math.hypot(S.ctm[0],S.ctm[1])*e;if(t!==u.textAdvanceScale){if(u.vertical){u.totalHeight+=u.height*u.textAdvanceScale;u.height=0}else{u.totalWidth+=u.width*u.textAdvanceScale;u.width=0}u.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");l||(t=function normalizeUnicode(e){if(!Ct){Ct=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ht=new Map([["ſt","ſt"]])}return e.replaceAll(Ct,((e,t,i)=>t?t.normalize("NFKC"):ht.get(i)))}(t));const i=bidi(t,-1,e.vertical);return{str:i.str,dir:i.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,i){const r=await p.loadFont(e,i,s);if(r.font.isType3Font)try{await r.loadType3Data(p,s,t)}catch{}S.loadedName=r.loadedName;S.font=r.font;S.fontMatrix=r.font.fontMatrix||a}function applyInverseRotation(e,t,i){const a=Math.hypot(i[0],i[1]);return[(i[0]*e+i[1]*t)/a,(i[2]*e+i[3]*t)/a]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let i=t[4],a=t[5];if(S.font?.vertical){if(ic[2]||a+ec[3])return!1}else if(i+ec[2]||ac[3])return!1;if(!S.font||!u.prevTransform)return!0;let s=u.prevTransform[4],r=u.prevTransform[5];if(s===i&&r===a)return!0;let n=-1;t[0]&&0===t[1]&&0===t[2]?n=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(n=t[1]>0?90:270);switch(n){case 0:break;case 90:[i,a]=[a,i];[s,r]=[r,s];break;case 180:[i,a,s,r]=[-i,-a,-s,-r];break;case 270:[i,a]=[-a,-i];[s,r]=[-r,-s];break;default:[i,a]=applyInverseRotation(i,a,t);[s,r]=applyInverseRotation(s,r,u.prevTransform)}if(S.font.vertical){const e=(r-a)/u.textAdvanceScale,t=i-s,n=Math.sign(u.height);if(e.5*u.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>u.width){appendEOL();return!0}e<=n*u.notASpace&&resetLastChars();if(e<=n*u.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else u.height+=e;else if(!addFakeSpaces(e,u.prevTransform,n))if(0===u.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else u.height+=e;Math.abs(t)>.25*u.width&&flushTextContentItem();return!0}const g=(i-s)/u.textAdvanceScale,o=a-r,C=Math.sign(u.width);if(g.5*u.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(o)>u.height){appendEOL();return!0}g<=C*u.notASpace&&resetLastChars();if(g<=C*u.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(g)})}else u.width+=g;else if(!addFakeSpaces(g,u.prevTransform,C))if(0===u.str.length){resetLastChars();pushWhitespace({width:Math.abs(g)})}else u.width+=g;Math.abs(o)>.25*u.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){const i=S.font;if(!e){const e=S.charSpacing+t;e&&(i.vertical?S.translateTextMatrix(0,-e):S.translateTextMatrix(e*S.textHScale,0));Q&&compareWithLastPosition(0);return}const a=i.charsToGlyphs(e),s=S.fontMatrix[0]*S.fontSize;for(let e=0,r=a.length;e0){const e=y.join("");y.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case he:if(!r.state.font){p.ensureStateFont(r.state);continue}buildTextContentItem({chars:N[0],extraSpacing:0});break;case Be:if(!r.state.font){p.ensureStateFont(r.state);continue}S.carriageReturn();buildTextContentItem({chars:N[0],extraSpacing:0});break;case Qe:if(!r.state.font){p.ensureStateFont(r.state);continue}S.wordSpacing=N[0];S.charSpacing=N[1];S.carriageReturn();buildTextContentItem({chars:N[2],extraSpacing:0});break;case xe:flushTextContentItem();w??=s.get("XObject")||Dict.empty;R=N[0]instanceof Name;f=N[0].name;if(R&&D.getByName(f))break;next(new Promise((function(e,i){if(!R)throw new FormatError("XObject must be referred to by name.");let a=w.getRaw(f);if(a instanceof Ref){if(D.getByRef(a)){e();return}if(p.globalImageCache.getData(a,p.pageIndex)){e();return}a=m.fetch(a)}if(!(a instanceof BaseStream))throw new FormatError("XObject should be a stream");const E=a.dict.get("Subtype");if(!(E instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==E.name){D.set(f,a.dict.objId,!0);e();return}const u=r.state.clone(),d=new StateManager(u),y=lookupMatrix(a.dict.getArray("Matrix"),null);y&&d.transform(y);enqueueChunk();const b={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;g.enqueue(e,t)},get desiredSize(){return g.desiredSize},get ready(){return g.ready}};p.getTextContent({stream:a,task:t,resources:a.dict.get("Resources")||s,stateManager:d,includeMarkedContent:n,sink:b,seenStyles:o,viewBox:c,lang:C,markedContentData:h,disableNormalization:l,keepWhiteSpace:Q}).then((function(){b.enqueueInvoked||D.set(f,a.dict.objId,!0);e()}),i)})).catch((function(e){if(!(e instanceof AbortException)){if(!p.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}})));return;case GA:R=N[0]instanceof Name;f=N[0].name;if(R&&b.getByName(f))break;next(new Promise((function(e,t){if(!R)throw new FormatError("GState must be referred to by name.");const i=s.get("ExtGState");if(!(i instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const a=i.get(f);if(!(a instanceof Dict))throw new FormatError("GState should be a dictionary.");const r=a.get("Font");if(r){flushTextContentItem();S.fontName=null;S.fontSize=r[1];handleSetFont(null,r[0]).then(e,t)}else{b.set(f,a.objId,!0);e()}})).catch((function(e){if(!(e instanceof AbortException)){if(!p.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}})));return;case Je:flushTextContentItem();if(n){h.level++;E.items.push({type:"beginMarkedContent",tag:N[0]instanceof Name?N[0].name:null})}break;case Ye:flushTextContentItem();if(n){h.level++;let e=null;N[1]instanceof Dict&&(e=N[1].get("MCID"));E.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${p.idFactory.getPageObjId()}_mc${e}`:null,tag:N[0]instanceof Name?N[0].name:null})}break;case ve:flushTextContentItem();if(n){if(0===h.level)break;h.level--;E.items.push({type:"endMarkedContent"})}break;case UA:!e||e.font===S.font&&e.fontSize===S.fontSize&&e.fontName===S.fontName||flushTextContentItem()}if(E.items.length>=g.desiredSize){d=!0;break}}if(d)next(ms);else{flushTextContentItem();enqueueChunk();e()}})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${t.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}}))}async extractDataStructures(e,t){const i=this.xref;let a;const s=this.readToUnicode(t.toUnicode);if(t.composite){const i=e.get("CIDSystemInfo");i instanceof Dict&&(t.cidSystemInfo={registry:stringToPDFString(i.get("Registry")),ordering:stringToPDFString(i.get("Ordering")),supplement:i.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(a=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const r=[];let n,g=null;if(e.has("Encoding")){n=e.get("Encoding");if(n instanceof Dict){g=n.get("BaseEncoding");g=g instanceof Name?g.name:null;if(n.has("Differences")){const e=n.get("Differences");let t=0;for(const a of e){const e=i.fetchIfRef(a);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);r[t++]=e.name}}}}else if(n instanceof Name)g=n.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==g&&"MacExpertEncoding"!==g&&"WinAnsiEncoding"!==g&&(g=null)}const o=!t.file||t.isInternalFont,c=qi()[t.name];g&&o&&c&&(g=null);if(g)t.defaultEncoding=getEncoding(g);else{const e=!!(t.flags&Mi),i=!!(t.flags&xi);n=hi;"TrueType"!==t.type||i||(n=li);if(e||c){n=Ci;o&&(/Symbol/i.test(t.name)?n=Bi:/Dingbats/i.test(t.name)?n=Qi:/Wingdings/i.test(t.name)&&(n=li))}t.defaultEncoding=n}t.differences=r;t.baseEncodingName=g;t.hasEncoding=!!g||r.length>0;t.dict=e;t.toUnicode=await s;const C=await this.buildToUnicode(t);t.toUnicode=C;a&&(t.cidToGidMap=this.readCidToGidMap(a,C));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const i=[],a=e.defaultEncoding.slice(),s=e.baseEncodingName,r=e.differences;for(const e in r){const t=r[e];".notdef"!==t&&(a[e]=t)}const n=wi();for(const r in a){let g=a[r];if(""===g)continue;let o=n[g];if(void 0!==o){i[r]=String.fromCharCode(o);continue}let c=0;switch(g[0]){case"G":3===g.length&&(c=parseInt(g.substring(1),16));break;case"g":5===g.length&&(c=parseInt(g.substring(1),16));break;case"C":case"c":if(g.length>=3&&g.length<=4){const i=g.substring(1);if(t){c=parseInt(i,16);break}c=+i;if(Number.isNaN(c)&&Number.isInteger(parseInt(i,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":o=getUnicodeForGlyph(g,n);-1!==o&&(c=o);break;default:switch(g){case"f_h":case"f_t":case"T_h":i[r]=g.replaceAll("_","");continue}}if(c>0&&c<=1114111&&Number.isInteger(c)){if(s&&c===+r){const e=getEncoding(s);if(e&&(g=e[r])){i[r]=String.fromCharCode(n[g]);continue}}i[r]=String.fromCodePoint(c)}}return i}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:i}=e.cidSystemInfo,a=Name.get(`${t}-${i}-UCS2`),s=await CMapFactory.create({encoding:a,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),r=[],n=[];e.cMap.forEach((function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const i=s.lookup(t);if(i){n.length=0;for(let e=0,t=i.length;e>1;(0!==s||t.has(r))&&(i[r]=s)}return i}extractWidths(e,t,i){const a=this.xref;let s=[],r=0;const n=[];let g;if(i.composite){const t=e.get("DW");r="number"==typeof t?Math.ceil(t):1e3;const o=e.get("W");if(Array.isArray(o))for(let e=0,t=o.length;e{const t=o.get(e),s=new OperatorList;return a.getOperatorList({stream:t,task:i,resources:c,operatorList:s}).then((()=>{s.fnArray[0]===ue&&this._removeType3ColorOperators(s,E);C[e]=s.getIR();for(const e of s.dependencies)n.add(e)})).catch((function(t){warn(`Type3 font resource "${e}" is not available.`);const i=new OperatorList;C[e]=i.getIR()}))}));this.type3Loaded=g.then((()=>{r.charProcOperatorList=C;if(this._bbox){r.isCharBBox=!0;r.bbox=this._bbox}}));return this.type3Loaded}_removeType3ColorOperators(e,t=NaN){const i=Util.normalizeRect(e.argsArray[0].slice(2)),a=i[2]-i[0],s=i[3]-i[1],r=Math.hypot(a,s);if(0===a||0===s){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===t||Math.round(r/t)>=10){this._bbox||(this._bbox=[1/0,1/0,-1/0,-1/0]);this._bbox[0]=Math.min(this._bbox[0],i[0]);this._bbox[1]=Math.min(this._bbox[1],i[1]);this._bbox[2]=Math.max(this._bbox[2],i[2]);this._bbox[3]=Math.max(this._bbox[3],i[3])}let n=0,g=e.length;for(;n=LA&&r<=zA;if(s.variableArgs)g>n&&info(`Command ${a}: expected [0, ${n}] args, but received ${g} args.`);else{if(g!==n){const e=this.nonProcessedArgs;for(;g>n;){e.push(t.shift());g--}for(;gEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(r,t);e.fn=r;e.args=t;return!0}if(i===Bt)return!1;if(null!==i){null===t&&(t=[]);t.push(i);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case MA:this.stateManager.save();break;case UA:this.stateManager.restore();break;case xA:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:i,args:a}=e;switch(0|i){case re:const[e,i]=a;e instanceof Name&&(t.fontName=e.name);"number"==typeof i&&i>0&&(t.fontSize=i);break;case Se:ColorSpace.singletons.rgb.getRgbItem(a,0,t.fontColor,0);break;case be:ColorSpace.singletons.gray.getRgbItem(a,0,t.fontColor,0);break;case Re:ColorSpace.singletons.cmyk.getRgbItem(a,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,i){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=i;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpace.singletons.gray},i=!1;const a=[];try{for(;;){e.args.length=0;if(i||!this.read(e))break;const{fn:s,args:r}=e;switch(0|s){case MA:a.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case UA:t=a.pop()||t;break;case ce:t.scaleFactor*=Math.hypot(r[0],r[1]);break;case re:const[e,s]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof s&&s>0&&(t.fontSize=s*t.scaleFactor);break;case fe:t.fillColorSpace=ColorSpace.parse({cs:r[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:this._localColorSpaceCache});break;case ye:t.fillColorSpace.getRgbItem(r,0,t.fontColor,0);break;case Se:ColorSpace.singletons.rgb.getRgbItem(r,0,t.fontColor,0);break;case be:ColorSpace.singletons.gray.getRgbItem(r,0,t.fontColor,0);break;case Re:ColorSpace.singletons.cmyk.getRgbItem(r,0,t.fontColor,0);break;case he:case le:case Be:case Qe:i=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,(e=>numberToString(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const i=new OffscreenCanvas(1,1);this.ctxMeasure=i.getContext("2d",{willReadFrequently:!0});FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.set("Type",Name.get("FontDescriptor"));e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.set("FontStretch",Name.get("Normal"));e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("CIDFontType0"));e.set("CIDToGIDMap",Name.get("Identity"));e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],i=[...this.widths.entries()].sort();let a=null,s=null;for(const[e,r]of i)if(a)if(e===a+s.length)s.push(r);else{t.push(a,s);a=e;s=[r]}else{a=e;s=[r]}a&&t.push(a,s);e.set("W",t);const r=new Dict(this.xref);r.set("Ordering","Identity");r.set("Registry","Adobe");r.set("Supplement",0);e.set("CIDSystemInfo",r);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type0"));e.set("Encoding",Name.get("Identity-H"));e.set("DescendantFonts",[this.descendantFontRef]);e.set("ToUnicode",Name.get("Identity-H"));return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const i of e.split(/\r\n?|\n/))for(const e of i.split("")){const i=e.charCodeAt(0);if(this.widths.has(i))continue;const a=t.measureText(e),s=Math.ceil(a.width);this.widths.set(i,s);this.firstChar=Math.min(i,this.firstChar);this.lastChar=Math.max(i,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,i){const[a,n,g,o]=e;let c=g-a,C=o-n;t%180!=0&&([c,C]=[C,c]);const h=s*i;return{coords:[0,C+r*i-h],bbox:[0,0,c,C],matrix:0!==t?getRotationMatrix(t,C,h):void 0}}createAppearance(e,t,i,a,n,g){const o=this._createContext(),c=[];let C=-1/0;for(const t of e.split(/\r\n?|\n/)){c.push(t);const e=o.measureText(t).width;C=Math.max(C,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let i=this.widths.get(e);if(void 0===i){const a=o.measureText(t);i=Math.ceil(a.width);this.widths.set(e,i);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}C*=a/1e3;const[h,l,Q,E]=t;let u=Q-h,d=E-l;i%180!=0&&([u,d]=[d,u]);let f=1;C>u&&(f=u/C);let p=1;const m=s*a,y=r*a,w=m*c.length;w>d&&(p=d/w);const D=a*Math.min(f,p),b=["q",`0 0 ${numberToString(u)} ${numberToString(d)} re W n`,"BT",`1 0 0 1 0 ${numberToString(d+y)} Tm 0 Tc ${getPdfColor(n,!0)}`,`/${this.fontName.name} ${numberToString(D)} Tf`],{resources:F}=this;if(1!==(g="number"==typeof g&&g>=0&&g<=1?g:1)){b.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",g);t.set("CA",g);t.set("Type",Name.get("ExtGState"));e.set("R0",t);F.set("ExtGState",e)}const S=numberToString(m);for(const e of c)b.push(`0 -${S} Td <${stringToUTF16HexString(e)}> Tj`);b.push("ET","Q");const k=b.join("\n"),R=new Dict(this.xref);R.set("Subtype",Name.get("Form"));R.set("Type",Name.get("XObject"));R.set("BBox",[0,0,u,d]);R.set("Length",k.length);R.set("Resources",F);if(i){const e=getRotationMatrix(i,u,d);R.set("Matrix",e)}const N=new StringStream(k);N.dict=R;return N}}class NameOrNumberTree{constructor(e,t,i){this.root=e;this.xref=t;this._type=i}getAll(){const e=new Map;if(!this.root)return e;const t=this.xref,i=new RefSet;i.put(this.root);const a=[this.root];for(;a.length>0;){const s=t.fetchIfRef(a.shift());if(!(s instanceof Dict))continue;if(s.has("Kids")){const e=s.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(i.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);a.push(t);i.put(t)}continue}const r=s.get(this._type);if(Array.isArray(r))for(let i=0,a=r.length;i10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const s=i.get("Kids");if(!Array.isArray(s))return null;let r=0,n=s.length-1;for(;r<=n;){const a=r+n>>1,g=t.fetchIfRef(s[a]),o=g.get("Limits");if(et.fetchIfRef(o[1]))){i=g;break}r=a+1}}if(r>n)return null}const s=i.get(this._type);if(Array.isArray(s)){let i=0,a=s.length-2;for(;i<=a;){const r=i+a>>1,n=r+(1&r),g=t.fetchIfRef(s[n]);if(eg))return s[n+1];i=n+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Qa=Object.create(null)}();!function clearPrimitiveCaches(){Qt=Object.create(null);Et=Object.create(null);ut=Object.create(null)}();!function clearUnicodeCaches(){ki.clear()}();JpxImage.cleanup()}function pickPlatformItem(e){return e instanceof Dict?e.has("UF")?e.get("UF"):e.has("F")?e.get("F"):e.has("Unix")?e.get("Unix"):e.has("Mac")?e.get("Mac"):e.has("DOS")?e.get("DOS"):null:null}class FileSpec{#U=!1;constructor(e,t,i=!1){if(e instanceof Dict){this.xref=t;this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");i||(e.has("EF")?this.#U=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){let e="";const t=pickPlatformItem(this.root);t&&"string"==typeof t&&(e=stringToPDFString(t).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"));return shadow(this,"filename",e||"unnamed")}get content(){if(!this.#U)return null;this._contentRef||=pickPlatformItem(this.root?.get("EF"));let e=null;if(this._contentRef){const t=this.xref.fetchIfRef(this._contentRef);t instanceof BaseStream?e=t.getBytes():warn("Embedded file specification points to non-existing/invalid content")}else warn("Embedded file specification does not have any content");return e}get description(){let e="";const t=this.root?.get("Desc");t&&"string"==typeof t&&(e=stringToPDFString(t));return shadow(this,"description",e)}get serializable(){return{rawFilename:this.filename,filename:(e=this.filename,e.substring(e.lastIndexOf("/")+1)),content:this.content,description:this.description};var e}}const ys=0,ws=-2,Ds=-3,bs=-4,Fs=-5,Ss=-6,ks=-9;function isWhitespace(e,t){const i=e[t];return" "===i||"\n"===i||"\r"===i||"\t"===i}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,((e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)}))}_parseContent(e,t){const i=[];let a=t;function skipWs(){for(;a"!==e[a]&&"/"!==e[a];)++a;const s=e.substring(t,a);skipWs();for(;a"!==e[a]&&"/"!==e[a]&&"?"!==e[a];){skipWs();let t="",s="";for(;a"!==e[i]&&"?"!==e[i]&&"/"!==e[i];)++i;const a=e.substring(t,i);!function skipWs(){for(;i"!==e[i+1]);)++i;return{name:a,value:e.substring(s,i),parsed:i-t}}parseXml(e){let t=0;for(;t",i);if(t<0){this.onError(ks);return}this.onEndElement(e.substring(i,t));i=t+1;break;case"?":++i;const a=this._parseProcessingInstruction(e,i);if("?>"!==e.substring(i+a.parsed,i+a.parsed+2)){this.onError(Ds);return}this.onPi(a.name,a.value);i+=a.parsed+2;break;case"!":if("--"===e.substring(i+1,i+3)){t=e.indexOf("--\x3e",i+3);if(t<0){this.onError(Fs);return}this.onComment(e.substring(i+3,t));i=t+3}else if("[CDATA["===e.substring(i+1,i+8)){t=e.indexOf("]]>",i+8);if(t<0){this.onError(ws);return}this.onCdata(e.substring(i+8,t));i=t+3}else{if("DOCTYPE"!==e.substring(i+1,i+8)){this.onError(Ss);return}{const a=e.indexOf("[",i+8);let s=!1;t=e.indexOf(">",i+8);if(t<0){this.onError(bs);return}if(a>0&&t>a){t=e.indexOf("]>",i+8);if(t<0){this.onError(bs);return}s=!0}const r=e.substring(i+8,t+(s?1:0));this.onDoctype(r);i=t+(s?2:1)}}break;default:const s=this._parseContent(e,i);if(null===s){this.onError(Ss);return}let r=!1;if("/>"===e.substring(i+s.parsed,i+s.parsed+2))r=!0;else if(">"!==e.substring(i+s.parsed,i+s.parsed+1)){this.onError(ks);return}this.onBeginElement(s.name,s.attributes,r);i+=s.parsed+(r?2:1)}}else{for(;i0}searchNode(e,t){if(t>=e.length)return this;const i=e[t];if(i.name.startsWith("#")&&t0){a.push([s,0]);s=s.childNodes[0]}else{if(0===a.length)return null;for(;0!==a.length;){const[e,t]=a.pop(),i=t+1;if(i");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=ys;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=ys;this.parseXml(e);if(this._errorCode!==ys)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,i=e.length;t\\376\\377([^<]+)/g,(function(e,t){const i=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,(function(e,t,i,a){return String.fromCharCode(64*t+8*i+1*a)})).replaceAll(/&(amp|apos|gt|lt|quot);/g,(function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)})),a=[">"];for(let e=0,t=i.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?a.push(String.fromCharCode(t)):a.push("&#x"+(65536+t).toString(16).substring(1)+";")}return a.join("")}))}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter((e=>"rdf:li"===e.nodeName))}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,i=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,i.map((e=>e.textContent.trim())))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const Rs=1,Ns=2,Gs=3,Ms=4,Us=5;class StructTreeRoot{constructor(e,t){this.dict=e;this.ref=t instanceof Ref?t:null;this.roleMap=new Map;this.structParentIds=null}init(){this.readRoleMap()}#x(e,t,i){if(!(e instanceof Ref)||t<0)return;this.structParentIds||=new RefSetCache;let a=this.structParentIds.get(e);if(!a){a=[];this.structParentIds.put(e,a)}a.push([t,i])}addAnnotationIdToPage(e,t){this.#x(e,t,Ms)}readRoleMap(){const e=this.dict.get("RoleMap");if(e instanceof Dict)for(const[t,i]of e)i instanceof Name&&this.roleMap.set(t,i.name)}static async canCreateStructureTree({catalogRef:e,pdfManager:t,newAnnotationsByPage:i}){if(!(e instanceof Ref)){warn("Cannot save the struct tree: no catalog reference.");return!1}let a=0,s=!0;for(const[e,r]of i){const{ref:i}=await t.getPage(e);if(!(i instanceof Ref)){warn(`Cannot save the struct tree: page ${e} has no ref.`);s=!0;break}for(const e of r)if(e.accessibilityData?.type){e.parentTreeId=a++;s=!1}}if(s){for(const e of i.values())for(const t of e)delete t.parentTreeId;return!1}return!0}static async createStructureTree({newAnnotationsByPage:e,xref:t,catalogRef:i,pdfManager:a,changes:s}){const r=a.catalog.cloneDict(),n=new RefSetCache;n.put(i,r);const g=t.getNewTemporaryRef();r.set("StructTreeRoot",g);const o=new Dict(t);o.set("Type",Name.get("StructTreeRoot"));const c=t.getNewTemporaryRef();o.set("ParentTree",c);const C=[];o.set("K",C);n.put(g,o);const h=new Dict(t),l=[];h.set("Nums",l);const Q=await this.#L({newAnnotationsByPage:e,structTreeRootRef:g,structTreeRoot:null,kids:C,nums:l,xref:t,pdfManager:a,changes:s,cache:n});o.set("ParentTreeNextKey",Q);n.put(c,h);for(const[e,t]of n.items())s.put(e,{data:t})}async canUpdateStructTree({pdfManager:e,xref:t,newAnnotationsByPage:i}){if(!this.ref){warn("Cannot update the struct tree: no root reference.");return!1}let a=this.dict.get("ParentTreeNextKey");if(!Number.isInteger(a)||a<0){warn("Cannot update the struct tree: invalid next key.");return!1}const s=this.dict.get("ParentTree");if(!(s instanceof Dict)){warn("Cannot update the struct tree: ParentTree isn't a dict.");return!1}const r=s.get("Nums");if(!Array.isArray(r)){warn("Cannot update the struct tree: nums isn't an array.");return!1}const n=new NumberTree(s,t);for(const t of i.keys()){const{pageDict:i}=await e.getPage(t);if(!i.has("StructParents"))continue;const a=i.get("StructParents");if(!Number.isInteger(a)||!Array.isArray(n.get(a))){warn(`Cannot save the struct tree: page ${t} has a wrong id.`);return!1}}let g=!0;for(const[t,s]of i){const{pageDict:i}=await e.getPage(t);StructTreeRoot.#H({elements:s,xref:this.dict.xref,pageDict:i,numberTree:n});for(const e of s)if(e.accessibilityData?.type){e.accessibilityData.structParent>=0||(e.parentTreeId=a++);g=!1}}if(g){for(const e of i.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:i}){const a=this.dict.xref,s=this.dict.clone(),r=this.ref,n=new RefSetCache;n.put(r,s);let g,o=s.getRaw("ParentTree");if(o instanceof Ref)g=a.fetch(o);else{g=o;o=a.getNewTemporaryRef();s.set("ParentTree",o)}g=g.clone();n.put(o,g);let c=g.getRaw("Nums"),C=null;if(c instanceof Ref){C=c;c=a.fetch(C)}c=c.slice();C||g.set("Nums",c);const h=await StructTreeRoot.#L({newAnnotationsByPage:e,structTreeRootRef:r,structTreeRoot:this,kids:null,nums:c,xref:a,pdfManager:t,changes:i,cache:n});if(-1!==h){s.set("ParentTreeNextKey",h);C&&n.put(C,c);for(const[e,t]of n.items())i.put(e,{data:t})}}static async#L({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:i,kids:a,nums:s,xref:r,pdfManager:n,changes:g,cache:o}){const c=Name.get("OBJR");let C,h=-1;for(const[l,Q]of e){const e=await n.getPage(l),{ref:E}=e,u=E instanceof Ref;for(const{accessibilityData:n,ref:d,parentTreeId:f,structTreeParent:p}of Q){if(!n?.type)continue;const{structParent:Q}=n;if(i&&Number.isInteger(Q)&&Q>=0){let t=(C||=new Map).get(l);if(void 0===t){t=new StructTreePage(i,e.pageDict).collectObjects(E);C.set(l,t)}const a=t?.get(Q);if(a){const e=r.fetch(a).clone();StructTreeRoot.#J(e,n);g.put(a,{data:e});continue}}h=Math.max(h,f);const m=r.getNewTemporaryRef(),y=new Dict(r);StructTreeRoot.#J(y,n);await this.#Y({structTreeParent:p,tagDict:y,newTagRef:m,structTreeRootRef:t,fallbackKids:a,xref:r,cache:o});const w=new Dict(r);y.set("K",w);w.set("Type",c);u&&w.set("Pg",E);w.set("Obj",d);o.put(m,y);s.push(f,m)}}return h+1}static#J(e,{type:t,title:i,lang:a,alt:s,expanded:r,actualText:n}){e.set("S",Name.get(t));i&&e.set("T",stringToAsciiOrUTF16BE(i));a&&e.set("Lang",stringToAsciiOrUTF16BE(a));s&&e.set("Alt",stringToAsciiOrUTF16BE(s));r&&e.set("E",stringToAsciiOrUTF16BE(r));n&&e.set("ActualText",stringToAsciiOrUTF16BE(n))}static#H({elements:e,xref:t,pageDict:i,numberTree:a}){const s=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);let i=s.get(e);if(!i){i=[];s.set(e,i)}i.push(t)}const r=i.get("StructParents");if(!Number.isInteger(r))return;const n=a.get(r),updateElement=(e,i,a)=>{const r=s.get(e);if(r){const e=i.getRaw("P"),s=t.fetchIfRef(e);if(e instanceof Ref&&s instanceof Dict){const e={ref:a,dict:i};for(const t of r)t.structTreeParent=e}return!0}return!1};for(const e of n){if(!(e instanceof Ref))continue;const i=t.fetch(e),a=i.get("K");if(Number.isInteger(a))updateElement(a,i,e);else if(Array.isArray(a))for(let s of a){s=t.fetchIfRef(s);if(Number.isInteger(s)&&updateElement(s,i,e))break;if(!(s instanceof Dict))continue;if(!isName(s.get("Type"),"MCR"))break;const a=s.get("MCID");if(Number.isInteger(a)&&updateElement(a,i,e))break}}}static async#Y({structTreeParent:e,tagDict:t,newTagRef:i,structTreeRootRef:a,fallbackKids:s,xref:r,cache:n}){let g,o=null;if(e){({ref:o}=e);g=e.dict.getRaw("P")||a}else g=a;t.set("P",g);const c=r.fetchIfRef(g);if(!c){s.push(i);return}let C=n.get(g);if(!C){C=c.clone();n.put(g,C)}const h=C.getRaw("K");let l=h instanceof Ref?n.get(h):null;if(!l){l=r.fetchIfRef(h);l=Array.isArray(l)?l.slice():[h];const e=r.getNewTemporaryRef();C.set("K",e);n.put(e,l)}const Q=l.indexOf(o);l.splice(Q>=0?Q+1:l.length,0,i)}}class StructElementNode{constructor(e,t){this.tree=e;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:i}=this.tree;return i.roleMap.has(t)?i.roleMap.get(t):t}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const i=this.dict.get("K");if(Array.isArray(i))for(const t of i){const i=this.parseKid(e,t);i&&this.kids.push(i)}else{const t=this.parseKid(e,i);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:Rs,mcid:t,pageObjId:e});let i=null;t instanceof Ref?i=this.dict.xref.fetch(t):t instanceof Dict&&(i=t);if(!i)return null;const a=i.getRaw("Pg");a instanceof Ref&&(e=a.toString());const s=i.get("Type")instanceof Name?i.get("Type").name:null;if("MCR"===s){if(this.tree.pageDict.objId!==e)return null;const t=i.getRaw("Stm");return new StructElement({type:Ns,refObjId:t instanceof Ref?t.toString():null,pageObjId:e,mcid:i.get("MCID")})}if("OBJR"===s){if(this.tree.pageDict.objId!==e)return null;const t=i.getRaw("Obj");return new StructElement({type:Gs,refObjId:t instanceof Ref?t.toString():null,pageObjId:e})}return new StructElement({type:Us,dict:i})}}class StructElement{constructor({type:e,dict:t=null,mcid:i=null,pageObjId:a=null,refObjId:s=null}){this.type=e;this.dict=t;this.mcid=i;this.pageObjId=a;this.refObjId=s;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.rootDict=e?e.dict:null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const i=this.root.structParentIds?.get(e);if(!i)return null;const a=new Map,s=new NumberTree(t,this.rootDict.xref);for(const[e]of i){const t=s.getRaw(e);t instanceof Ref&&a.set(e,t)}return a}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const t=this.rootDict.get("ParentTree");if(!t)return;const i=this.pageDict.get("StructParents"),a=this.root.structParentIds?.get(e);if(!Number.isInteger(i)&&!a)return;const s=new Map,r=new NumberTree(t,this.rootDict.xref);if(Number.isInteger(i)){const e=r.get(i);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.rootDict.xref.fetch(t),s)}if(a)for(const[e,t]of a){const i=r.get(e);if(i){const e=this.addNode(this.rootDict.xref.fetchIfRef(i),s);1===e?.kids?.length&&e.kids[0].type===Gs&&(e.kids[0].type=t)}}}addNode(e,t,i=0){if(i>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const a=new StructElementNode(this,e);t.set(e,a);const s=e.get("P");if(!s||isName(s.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,a)||t.delete(e);return a}const r=this.addNode(s,t,i+1);if(!r)return a;let n=!1;for(const t of r.kids)if(t.type===Us&&t.dict===e){t.parentNode=a;n=!0}n||t.delete(e);return a}addTopLevelNode(e,t){const i=this.rootDict.get("K");if(!i)return!1;if(i instanceof Dict){if(i.objId!==e.objId)return!1;this.nodes[0]=t;return!0}if(!Array.isArray(i))return!0;let a=!1;for(let s=0;s40){warn("StructTree too deep to be fully serialized.");return}const a=Object.create(null);a.role=e.role;a.children=[];t.children.push(a);let s=e.dict.get("Alt");"string"!=typeof s&&(s=e.dict.get("ActualText"));"string"==typeof s&&(a.alt=stringToPDFString(s));const r=e.dict.get("A");if(r instanceof Dict){const e=lookupNormalRect(r.getArray("BBox"),null);if(e)a.bbox=e;else{const e=r.get("Width"),t=r.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(a.bbox=[0,0,e,t])}}const n=e.dict.get("Lang");"string"==typeof n&&(a.lang=stringToPDFString(n));for(const t of e.kids){const e=t.type===Us?t.parentNode:null;e?nodeToSerializable(e,a,i+1):t.type===Rs||t.type===Ns?a.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===Gs?a.children.push({type:"object",id:t.refObjId}):t.type===Ms&&a.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}function isValidExplicitDest(e){if(!Array.isArray(e)||e.length<2)return!1;const[t,i,...a]=e;if(!(t instanceof Ref||Number.isInteger(t)))return!1;if(!(i instanceof Name))return!1;const s=a.length;let r=!0;switch(i.name){case"XYZ":if(s<2||s>3)return!1;break;case"Fit":case"FitB":return 0===s;case"FitH":case"FitBH":case"FitV":case"FitBV":if(s>1)return!1;break;case"FitR":if(4!==s)return!1;r=!1;break;default:return!1}for(const e of a)if(!("number"==typeof e||r&&null===e))return!1;return!0}function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return isValidExplicitDest(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t);if(isValidExplicitDest(t))return JSON.stringify(t)}return null}class Catalog{constructor(e,t){this.pdfManager=e;this.xref=t;this._catDict=t.getCatalogObj();if(!(this._catDict instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict;this._actualNumPages=null;this.fontCache=new RefSetCache;this.builtInCMapCache=new Map;this.standardFontDataCache=new Map;this.globalImageCache=new GlobalImageCache;this.pageKidsCountCache=new RefSetCache;this.pageIndexCache=new RefSetCache;this.pageDictCache=new RefSetCache;this.nonBlendModesSet=new RefSet;this.systemFontCache=new Map}cloneDict(){return this._catDict.clone()}get version(){const e=this._catDict.get("Version");if(e instanceof Name){if(ft.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this._catDict.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this._catDict.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this._catDict.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this._catDict.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this._catDict.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this._catDict.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const i=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(i instanceof BaseStream&&i.dict instanceof Dict){const e=i.dict.get("Type"),a=i.dict.get("Subtype");if(isName(e,"Metadata")&&isName(a,"XML")){const e=stringToUTF8String(i.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this._readMarkInfo()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}_readMarkInfo(){const e=this._catDict.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const i in t){const a=e.get(i);"boolean"==typeof a&&(t[i]=a)}return t}get structTreeRoot(){let e=null;try{e=this._readStructTreeRoot()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}_readStructTreeRoot(){const e=this._catDict.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const i=new StructTreeRoot(t,e);i.init();return i}get toplevelPagesDict(){const e=this._catDict.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this._readDocumentOutline()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}_readDocumentOutline(){let e=this._catDict.get("Outlines");if(!(e instanceof Dict))return null;e=e.getRaw("First");if(!(e instanceof Ref))return null;const t={items:[]},i=[{obj:e,parent:t}],a=new RefSet;a.put(e);const s=this.xref,r=new Uint8ClampedArray(3);for(;i.length>0;){const t=i.shift(),n=s.fetchIfRef(t.obj);if(null===n)continue;n.has("Title")||warn("Invalid outline item encountered.");const g={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:n,resultObj:g,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const o=n.get("Title"),c=n.get("F")||0,C=n.getArray("C"),h=n.get("Count");let l=r;!isNumberArray(C,3)||0===C[0]&&0===C[1]&&0===C[2]||(l=ColorSpace.singletons.rgb.getRgb(C,0));const Q={action:g.action,attachment:g.attachment,dest:g.dest,url:g.url,unsafeUrl:g.unsafeUrl,newWindow:g.newWindow,setOCGState:g.setOCGState,title:"string"==typeof o?stringToPDFString(o):"",color:l,count:Number.isInteger(h)?h:void 0,bold:!!(2&c),italic:!!(1&c),items:[]};t.parent.items.push(Q);e=n.getRaw("First");if(e instanceof Ref&&!a.has(e)){i.push({obj:e,parent:Q});a.put(e)}e=n.getRaw("Next");if(e instanceof Ref&&!a.has(e)){i.push({obj:e,parent:t.parent});a.put(e)}}return t.items.length>0?t.items:null}get permissions(){let e=null;try{e=this._readPermissions()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}_readPermissions(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const i=[];for(const e in y){const a=y[e];t&a&&i.push(a)}return i}get optionalContentConfig(){let e=null;try{const t=this._catDict.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const i=t.get("D");if(!i)return shadow(this,"optionalContentConfig",null);const a=t.get("OCGs");if(!Array.isArray(a))return shadow(this,"optionalContentConfig",null);const s=new RefSetCache;for(const e of a)e instanceof Ref&&!s.has(e)&&s.put(e,this.#v(e));e=this.#K(i,s)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#v(e){const t=this.xref.fetch(e),i={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},a=t.get("Name");"string"==typeof a&&(i.name=stringToPDFString(a));let s=t.getArray("Intent");Array.isArray(s)||(s=[s]);s.every((e=>e instanceof Name))&&(i.intent=s.map((e=>e.name)));const r=t.get("Usage");if(!(r instanceof Dict))return i;const n=i.usage,g=r.get("Print");if(g instanceof Dict){const e=g.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":n.print={printState:e.name}}}const o=r.get("View");if(o instanceof Dict){const e=o.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":n.view={viewState:e.name}}}return i}#K(e,t){function parseOnOff(e){const i=[];if(Array.isArray(e))for(const a of e)a instanceof Ref&&t.has(a)&&i.push(a.toString());return i}function parseOrder(e,i=0){if(!Array.isArray(e))return null;const s=[];for(const r of e){if(r instanceof Ref&&t.has(r)){a.put(r);s.push(r.toString());continue}const e=parseNestedOrder(r,i);e&&s.push(e)}if(i>0)return s;const r=[];for(const[e]of t.items())a.has(e)||r.push(e.toString());r.length&&s.push({name:null,order:r});return s}function parseNestedOrder(e,t){if(++t>s){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const a=i.fetchIfRef(e);if(!Array.isArray(a))return null;const r=i.fetchIfRef(a[0]);if("string"!=typeof r)return null;const n=parseOrder(a.slice(1),t);return n?.length?{name:stringToPDFString(r),order:n}:null}const i=this.xref,a=new RefSet,s=10;!function parseRBGroups(e){if(Array.isArray(e))for(const a of e){const e=i.fetchIfRef(a);if(!Array.isArray(e)||!e.length)continue;const s=new Set;for(const i of e)if(i instanceof Ref&&t.has(i)&&!s.has(i.toString())){s.add(i.toString());t.get(i).rbGroups.push(s)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this._actualNumPages=e}get hasActualNumPages(){return null!==this._actualNumPages}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.hasActualNumPages?this._actualNumPages:this._pagesCount}get destinations(){const e=this._readDests(),t=Object.create(null);if(e instanceof NameTree)for(const[i,a]of e.getAll()){const e=fetchDest(a);e&&(t[stringToPDFString(i)]=e)}else if(e instanceof Dict)for(const[i,a]of e){const e=fetchDest(a);e&&(t[i]=e)}return shadow(this,"destinations",t)}getDestination(e){const t=this._readDests();if(t instanceof NameTree){const i=fetchDest(t.get(e));if(i)return i;const a=this.destinations[e];if(a){warn(`Found "${e}" at an incorrect position in the NameTree.`);return a}}else if(t instanceof Dict){const i=fetchDest(t.get(e));if(i)return i}return null}_readDests(){const e=this._catDict.get("Names");return e?.has("Dests")?new NameTree(e.getRaw("Dests"),this.xref):this._catDict.has("Dests")?this._catDict.get("Dests"):void 0}get pageLabels(){let e=null;try{e=this._readPageLabels()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}_readPageLabels(){const e=this._catDict.getRaw("PageLabels");if(!e)return null;const t=new Array(this.numPages);let i=null,a="";const s=new NumberTree(e,this.xref).getAll();let r="",n=1;for(let e=0,g=this.numPages;e=1))throw new FormatError("Invalid start in PageLabel dictionary.");n=e}else n=1}switch(i){case"D":r=n;break;case"R":case"r":r=toRomanNumerals(n,"r"===i);break;case"A":case"a":const e=26,t="a"===i?97:65,a=n-1;r=String.fromCharCode(t+a%e).repeat(Math.floor(a/e)+1);break;default:if(i)throw new FormatError(`Invalid style "${i}" in PageLabel dictionary.`);r=""}t[e]=a+r;n++}return t}get pageLayout(){const e=this._catDict.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this._catDict.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this._catDict.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const i of e.getKeys()){const a=e.get(i);let s;switch(i){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof a&&(s=a);break;case"NonFullScreenPageMode":if(a instanceof Name)switch(a.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":s=a.name;break;default:s="UseNone"}break;case"Direction":if(a instanceof Name)switch(a.name){case"L2R":case"R2L":s=a.name;break;default:s="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(a instanceof Name)switch(a.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":s=a.name;break;default:s="CropBox"}break;case"PrintScaling":if(a instanceof Name)switch(a.name){case"None":case"AppDefault":s=a.name;break;default:s="AppDefault"}break;case"Duplex":if(a instanceof Name)switch(a.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":s=a.name;break;default:s="None"}break;case"PrintPageRange":if(Array.isArray(a)&&a.length%2==0){a.every(((e,t,i)=>Number.isInteger(e)&&e>0&&(0===t||e>=i[t-1])&&e<=this.numPages))&&(s=a)}break;case"NumCopies":Number.isInteger(a)&&a>0&&(s=a);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${i}.`);continue}if(void 0!==s){t||(t=Object.create(null));t[i]=s}else warn(`Bad value, for key "${i}", in ViewerPreferences: ${a}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this._catDict.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const i=new Dict(this.xref);i.set("A",e);const a={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:i,resultObj:a});Array.isArray(a.dest)?t.dest=a.dest:a.action&&(t.action=a.action)}else Array.isArray(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const i=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,a]of i.getAll()){const i=new FileSpec(a,this.xref);t||(t=Object.create(null));t[stringToPDFString(e)]=i.serializable}}return shadow(this,"attachments",t)}get xfaImages(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const i=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,a]of i.getAll()){t||(t=new Dict(this.xref));t.set(stringToPDFString(e),a)}}return shadow(this,"xfaImages",t)}_collectJavaScript(){const e=this._catDict.get("Names");let t=null;function appendIfJavaScriptDict(e,i){if(!(i instanceof Dict))return;if(!isName(i.get("S"),"JavaScript"))return;let a=i.get("JS");if(a instanceof BaseStream)a=a.getString();else if("string"!=typeof a)return;a=stringToPDFString(a).replaceAll("\0","");a&&(t||=new Map).set(e,a)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,i]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e),i)}const i=this._catDict.get("OpenAction");i&&appendIfJavaScriptDict("OpenAction",i);return t}get jsActions(){const e=this._collectJavaScript();let t=collectActions(this.xref,this._catDict,fA);if(e){t||=Object.create(null);for(const[i,a]of e)i in t?t[i].push(a):t[i]=[a]}return shadow(this,"jsActions",t)}async fontFallback(e,t){const i=await Promise.all(this.fontCache);for(const a of i)if(a.loadedName===e){a.fallback(t);return}}async cleanup(e=!1){clearGlobalCaches();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();const t=await Promise.all(this.fontCache);for(const{dict:e}of t)delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],i=new RefSet,a=this._catDict.getRaw("Pages");a instanceof Ref&&i.put(a);const s=this.xref,r=this.pageKidsCountCache,n=this.pageIndexCache,g=this.pageDictCache;let o=0;for(;t.length;){const a=t.pop();if(a instanceof Ref){const c=r.get(a);if(c>=0&&o+c<=e){o+=c;continue}if(i.has(a))throw new FormatError("Pages tree contains circular reference.");i.put(a);const C=await(g.get(a)||s.fetchAsync(a));if(C instanceof Dict){let t=C.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!C.has("Kids")){r.has(a)||r.put(a,1);n.has(a)||n.put(a,o);if(o===e)return[C,a];o++;continue}}t.push(C);continue}if(!(a instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:c}=a;let C=a.getRaw("Count");C instanceof Ref&&(C=await s.fetchAsync(C));if(Number.isInteger(C)&&C>=0){c&&!r.has(c)&&r.put(c,C);if(o+C<=e){o+=C;continue}}let h=a.getRaw("Kids");h instanceof Ref&&(h=await s.fetchAsync(h));if(!Array.isArray(h)){let t=a.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!a.has("Kids")){if(o===e)return[a,null];o++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=h.length-1;e>=0;e--){const i=h[e];t.push(i);a===this.toplevelPagesDict&&i instanceof Ref&&!g.has(i)&&g.put(i,s.fetchAsync(i))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,i=[{currentNode:this.toplevelPagesDict,posInKids:0}],a=new RefSet,s=this._catDict.getRaw("Pages");s instanceof Ref&&a.put(s);const r=new Map,n=this.xref,g=this.pageIndexCache;let o=0;function addPageDict(e,t){t&&!g.has(t)&&g.put(t,o);r.set(o++,[e,t])}function addPageError(i){if(i instanceof XRefEntryException&&!e)throw i;if(e&&t&&0===o){warn(`getAllPageDicts - Skipping invalid first page: "${i}".`);i=Dict.empty}r.set(o++,[i,null])}for(;i.length>0;){const e=i.at(-1),{currentNode:t,posInKids:s}=e;let r=t.getRaw("Kids");if(r instanceof Ref)try{r=await n.fetchAsync(r)}catch(e){addPageError(e);break}if(!Array.isArray(r)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(s>=r.length){i.pop();continue}const g=r[s];let o;if(g instanceof Ref){if(a.has(g)){addPageError(new FormatError("Pages tree contains circular reference."));break}a.put(g);try{o=await n.fetchAsync(g)}catch(e){addPageError(e);break}}else o=g;if(!(o instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let c=o.getRaw("Type");if(c instanceof Ref)try{c=await n.fetchAsync(c)}catch(e){addPageError(e);break}isName(c,"Page")||!o.has("Kids")?addPageDict(o,g instanceof Ref?g:null):i.push({currentNode:o,posInKids:0});e.posInKids++}return r}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const i=this.xref;let a=0;const next=t=>function pagesBeforeRef(t){let a,s=0;return i.fetchAsync(t).then((function(i){if(isRefsEqual(t,e)&&!isDict(i,"Page")&&!(i instanceof Dict&&!i.has("Type")&&i.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!i)return null;if(!(i instanceof Dict))throw new FormatError("Node must be a dictionary.");a=i.getRaw("Parent");return i.getAsync("Parent")})).then((function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")})).then((function(e){if(!e)return null;const r=[];let n=!1;for(const a of e){if(!(a instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(a,t)){n=!0;break}r.push(i.fetchAsync(a).then((function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?s+=e.get("Count"):s++})))}if(!n)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(r).then((function(){return[s,a]}))}))}(t).then((t=>{if(!t){this.pageIndexCache.put(e,a);return a}const[i,s]=t;a+=i;return next(s)}));return next(e)}get baseUrl(){const e=this._catDict.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static parseDestDictionary({destDict:e,resultObj:t,docBaseUrl:i=null,docAttachments:a=null}){if(!(e instanceof Dict)){warn("parseDestDictionary: `destDict` must be a dictionary.");return}let s,r,n=e.get("A");if(!(n instanceof Dict))if(e.has("Dest"))n=e.get("Dest");else{n=e.get("AA");n instanceof Dict&&(n.has("D")?n=n.get("D"):n.has("U")&&(n=n.get("U")))}if(n instanceof Dict){const e=n.get("S");if(!(e instanceof Name)){warn("parseDestDictionary: Invalid type in Action dictionary.");return}const i=e.name;switch(i){case"ResetForm":const e=n.get("Flags"),g=!(1&("number"==typeof e?e:0)),o=[],c=[];for(const e of n.get("Fields")||[])e instanceof Ref?c.push(e.toString()):"string"==typeof e&&o.push(stringToPDFString(e));t.resetForm={fields:o,refs:c,include:g};break;case"URI":s=n.get("URI");s instanceof Name&&(s="/"+s.name);break;case"GoTo":r=n.get("D");break;case"Launch":case"GoToR":const C=n.get("F");if(C instanceof Dict){const e=new FileSpec(C,null,!0),{rawFilename:t}=e.serializable;s=t}else"string"==typeof C&&(s=C);const h=fetchRemoteDest(n);h&&"string"==typeof s&&(s=s.split("#",1)[0]+"#"+h);const l=n.get("NewWindow");"boolean"==typeof l&&(t.newWindow=l);break;case"GoToE":const Q=n.get("T");let E;if(a&&Q instanceof Dict){const e=Q.get("R"),t=Q.get("N");isName(e,"C")&&"string"==typeof t&&(E=a[stringToPDFString(t)])}if(E){t.attachment=E;const e=fetchRemoteDest(n);e&&(t.attachmentDest=e)}else warn('parseDestDictionary - unimplemented "GoToE" action.');break;case"Named":const u=n.get("N");u instanceof Name&&(t.action=u.name);break;case"SetOCGState":const d=n.get("State"),f=n.get("PreserveRB");if(!Array.isArray(d)||0===d.length)break;const p=[];for(const e of d)if(e instanceof Name)switch(e.name){case"ON":case"OFF":case"Toggle":p.push(e.name)}else e instanceof Ref&&p.push(e.toString());if(p.length!==d.length)break;t.setOCGState={state:p,preserveRB:"boolean"!=typeof f||f};break;case"JavaScript":const m=n.get("JS");let y;m instanceof BaseStream?y=m.getString():"string"==typeof m&&(y=m);const w=y&&recoverJsURL(stringToPDFString(y));if(w){s=w.url;t.newWindow=w.newWindow;break}default:if("JavaScript"===i||"SubmitForm"===i)break;warn(`parseDestDictionary - unsupported action: "${i}".`)}}else e.has("Dest")&&(r=e.get("Dest"));if("string"==typeof s){const e=createValidAbsoluteUrl(s,i,{addDefaultProtocol:!0,tryConvertEncoding:!0});e&&(t.url=e.href);t.unsafeUrl=s}if(r){r instanceof Name&&(r=r.name);"string"==typeof r?t.dest=stringToPDFString(r):isValidExplicitDest(r)&&(t.dest=r)}}}function addChildren(e,t){if(e instanceof Dict)e=e.getRawValues();else if(e instanceof BaseStream)e=e.dict.getRawValues();else if(!Array.isArray(e))return;for(const a of e)((i=a)instanceof Ref||i instanceof Dict||i instanceof BaseStream||Array.isArray(i))&&t.push(a);var i}class ObjectLoader{constructor(e,t,i){this.dict=e;this.keys=t;this.xref=i;this.refSet=null}async load(){if(this.xref.stream.isDataLoaded)return;const{keys:e,dict:t}=this;this.refSet=new RefSet;const i=[];for(const a of e){const e=t.getRaw(a);void 0!==e&&i.push(e)}return this._walk(i)}async _walk(e){const t=[],i=[];for(;e.length;){let a=e.pop();if(a instanceof Ref){if(this.refSet.has(a))continue;try{this.refSet.put(a);a=this.xref.fetch(a)}catch(e){if(!(e instanceof MissingDataException)){warn(`ObjectLoader._walk - requesting all data: "${e}".`);this.refSet=null;const{manager:t}=this.xref.stream;return t.requestAllChunks()}t.push(a);i.push({begin:e.begin,end:e.end})}}if(a instanceof BaseStream){const e=a.getBaseStreams();if(e){let s=!1;for(const t of e)if(!t.isDataLoaded){s=!0;i.push({begin:t.start,end:t.end})}s&&t.push(a)}}addChildren(a,e)}if(i.length){await this.xref.stream.manager.requestRanges(i);for(const e of t)e instanceof Ref&&this.refSet.remove(e);return this._walk(t)}this.refSet=null}}const xs=Symbol(),Ls=Symbol(),Hs=Symbol(),Js=Symbol(),Ys=Symbol(),vs=Symbol(),Ks=Symbol(),Ts=Symbol(),qs=Symbol(),Os=Symbol("content"),Ws=Symbol("data"),js=Symbol(),Xs=Symbol("extra"),Zs=Symbol(),Vs=Symbol(),zs=Symbol(),_s=Symbol(),$s=Symbol(),Ar=Symbol(),er=Symbol(),tr=Symbol(),ir=Symbol(),ar=Symbol(),sr=Symbol(),rr=Symbol(),nr=Symbol(),gr=Symbol(),or=Symbol(),Ir=Symbol(),cr=Symbol(),Cr=Symbol(),hr=Symbol(),lr=Symbol(),Qr=Symbol(),Er=Symbol(),ur=Symbol(),dr=Symbol(),fr=Symbol(),pr=Symbol(),mr=Symbol(),yr=Symbol(),wr=Symbol(),Dr=Symbol(),br=Symbol(),Fr=Symbol(),Sr=Symbol("namespaceId"),kr=Symbol("nodeName"),Rr=Symbol(),Nr=Symbol(),Gr=Symbol(),Mr=Symbol(),Ur=Symbol(),xr=Symbol(),Lr=Symbol(),Hr=Symbol(),Jr=Symbol("root"),Yr=Symbol(),vr=Symbol(),Kr=Symbol(),Tr=Symbol(),qr=Symbol(),Or=Symbol(),Pr=Symbol(),Wr=Symbol(),jr=Symbol(),Xr=Symbol(),Zr=Symbol(),Vr=Symbol("uid"),zr=Symbol(),_r={config:{id:0,check:e=>e.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},$r={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},An=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:i}){if(!e)return t;e=e.trim();const a=parseInt(e,10);return!isNaN(a)&&i(a)?a:t}function getFloat({data:e,defaultValue:t,validate:i}){if(!e)return t;e=e.trim();const a=parseFloat(e);return!isNaN(a)&&i(a)?a:t}function getKeyword({data:e,defaultValue:t,validate:i}){return e&&i(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const i=e.trim().match(An);if(!i)return getMeasurement(t);const[,a,s]=i,r=parseFloat(a);if(isNaN(r))return getMeasurement(t);if(0===r)return 0;const n=$r[s];return n?n(r):r}function getRatio(e){if(!e)return{num:1,den:1};const t=e.trim().split(/\s*:\s*/).map((e=>parseFloat(e))).filter((e=>!isNaN(e)));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[i,a]=t;return{num:i,den:a}}function getRelevant(e){return e?e.trim().split(/\s+/).map((e=>({excluded:"-"===e[0],viewname:e.substring(1)}))):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,i,a){this.success=e;this.html=t;this.bbox=i;this.breakNode=a}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const i=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,i)}addPdfFont(e){const t=e.cssFontInfo,i=t.fontFamily;let a=this.fonts.get(i);if(!a){a=Object.create(null);this.fonts.set(i,a);this.defaultFont||(this.defaultFont=a)}let s="";const r=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?s=r>=700?"bolditalic":"italic":r>=700&&(s="bold");if(!s){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(s="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(s+="italic")}s||(s="regular");a[s]=e}getDefault(){return this.defaultFont}find(e,t=!0){let i=this.fonts.get(e)||this.cache.get(e);if(i)return i;const a=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let s=e.replaceAll(a,"");i=this.fonts.get(s);if(i){this.cache.set(e,i);return i}s=s.toLowerCase();const r=[];for(const[e,t]of this.fonts.entries())e.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(t);if(0===r.length)for(const[,e]of this.fonts.entries())e.regular.name?.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(e);if(0===r.length){s=s.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts.entries())e.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(t)}if(0===r.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(e);if(r.length>=1){1!==r.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,r[0]);return r[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,i,a){this.lineHeight=i;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(a);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const s=a.find(e.typeface);if(s){this.pdfFont=selectFont(e,s);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(a))}else[this.pdfFont,this.xfaFont]=this.defaultFont(a)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,i,a){this.fontFinder=a;this.stack=[new FontInfo(e,t,i,a)]}pushData(e,t,i){const a=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||(e[t]=a.xfaFont[t]);for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=a.paraMargin[e]);const s=new FontInfo(e,t,i||a.lineHeight,this.fontFinder);s.pdfFont||(s.pdfFont=a.pdfFont);this.stack.push(s)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,i,a){this.glyphs=[];this.fontSelector=new FontSelector(e,t,i,a);this.extraHeight=0}pushData(e,t,i){this.fontSelector.pushData(e,t,i)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),i=t.xfaFont.size;if(t.pdfFont){const a=t.xfaFont.letterSpacing,s=t.pdfFont,r=s.lineHeight||1.2,n=t.lineHeight||Math.max(1.2,r)*i,g=r-(void 0===s.lineGap?.2:s.lineGap),o=Math.max(1,g)*i,c=i/1e3,C=s.defaultWidth||s.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=s.encodeString(t).join(""),i=s.charsToGlyphs(e);for(const e of i){const t=e.width||C;this.glyphs.push([t*c+a,n,o,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}else{for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([i,1.2*i,i,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}}compute(e){let t=-1,i=0,a=0,s=0,r=0,n=0,g=!1,o=!0;for(let c=0,C=this.glyphs.length;ce){a=Math.max(a,r);r=0;s+=n;n=d;t=-1;i=0;g=!0;o=!1}else{n=Math.max(d,n);i=r;r+=C;t=c}else if(r+C>e){s+=n;n=d;if(-1!==t){c=t;a=Math.max(a,i);r=0;t=-1;i=0}else{a=Math.max(a,r);r=C}g=!0;o=!1}else{r+=C;n=Math.max(d,n)}}a=Math.max(a,r);s+=n+this.extraHeight;return{width:1.02*a,height:s,isBroken:g}}}const en=/^[^.[]+/,tn=/^[^\]]+/,an=0,sn=1,rn=2,nn=3,gn=4,on=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[rr]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),In=new WeakMap;function parseExpression(e,t,i=!0){let a=e.match(en);if(!a)return null;let[s]=a;const r=[{name:s,cacheName:"."+s,index:0,js:null,formCalc:null,operator:an}];let n=s.length;for(;n0&&C.push(e)}if(0!==C.length||g||0!==o)e=isFinite(c)?C.filter((e=>ce[c])):C.flat();else{const i=t[Ir]();if(!(t=i))return null;o=-1;e=[t]}}return 0===e.length?null:e}function createDataNode(e,t,i){const a=parseExpression(i);if(!a)return null;if(a.some((e=>e.operator===sn)))return null;const s=on.get(a[0].name);let r=0;if(s){e=s(e,t);r=1}else e=t||e;for(let t=a.length;re[Pr]())).join("")}get[hn](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,hn,e._attributes)}[pr](e){let t=this;for(;t;){if(t===e)return!0;t=t[Ir]()}return!1}[Ir](){return this[wn]}[or](){return this[Ir]()}[rr](e=null){return e?this[e]:this[ln]}[js](){const e=Object.create(null);this[Os]&&(e.$content=this[Os]);for(const t of Object.getOwnPropertyNames(this)){const i=this[t];null!==i&&(i instanceof XFAObject?e[t]=i[js]():i instanceof XFAObjectArray?i.isEmpty()||(e[t]=i.dump()):e[t]=i)}return e}[Zr](){return null}[jr](){return HTMLResult.EMPTY}*[nr](){for(const e of this[rr]())yield e}*[un](e,t){for(const i of this[nr]())if(!e||t===e.has(i[kr])){const e=this[$s](),t=i[jr](e);t.success||(this[Xs].failingNode=i);yield t}}[Vs](){return null}[Ls](e,t){this[Xs].children.push(e)}[$s](){}[Js]({filter:e=null,include:t=!0}){if(this[Xs].generator){const e=this[$s](),t=this[Xs].failingNode[jr](e);if(!t.success)return t;t.html&&this[Ls](t.html,t.bbox);delete this[Xs].failingNode}else this[Xs].generator=this[un](e,t);for(;;){const e=this[Xs].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Ls](t.html,t.bbox)}this[Xs].generator=null;return HTMLResult.EMPTY}[Tr](e){this[bn]=new Set(Object.keys(e))}[fn](e){const t=this[hn],i=this[bn];return[...e].filter((e=>t.has(e)&&!i.has(e)))}[Yr](e,t=new Set){for(const i of this[ln])i[Dn](e,t)}[Dn](e,t){const i=this[dn](e,t);i?this[cn](i,e,t):this[Yr](e,t)}[dn](e,t){const{use:i,usehref:a}=this;if(!i&&!a)return null;let s=null,r=null,n=null,g=i;if(a){g=a;a.startsWith("#som(")&&a.endsWith(")")?r=a.slice(5,-1):a.startsWith(".#som(")&&a.endsWith(")")?r=a.slice(6,-1):a.startsWith("#")?n=a.slice(1):a.startsWith(".#")&&(n=a.slice(2))}else i.startsWith("#")?n=i.slice(1):r=i;this.use=this.usehref="";if(n)s=e.get(n);else{s=searchNode(e.get(Jr),this,r,!0,!1);s&&(s=s[0])}if(!s){warn(`XFA - Invalid prototype reference: ${g}.`);return null}if(s[kr]!==this[kr]){warn(`XFA - Incompatible prototype: ${s[kr]} !== ${this[kr]}.`);return null}if(t.has(s)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(s);const o=s[dn](e,t);o&&s[cn](o,e,t);s[Yr](e,t);t.delete(s);return s}[cn](e,t,i){if(i.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[Os]&&e[Os]&&(this[Os]=e[Os]);new Set(i).add(e);for(const t of this[fn](e[bn])){this[t]=e[t];this[bn]&&this[bn].add(t)}for(const a of Object.getOwnPropertyNames(this)){if(this[hn].has(a))continue;const s=this[a],r=e[a];if(s instanceof XFAObjectArray){for(const e of s[ln])e[Dn](t,i);for(let a=s[ln].length,n=r[ln].length;aXFAObject[Bn](e))):"object"==typeof e&&null!==e?Object.assign({},e):e}[Ts](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[Vr]=`${e[kr]}${Sn++}`;e[ln]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[hn].has(t)){e[t]=XFAObject[Bn](this[t]);continue}const i=this[t];e[t]=i instanceof XFAObjectArray?new XFAObjectArray(i[mn]):null}for(const t of this[ln]){const i=t[kr],a=t[Ts]();e[ln].push(a);a[wn]=e;null===e[i]?e[i]=a:e[i][ln].push(a)}return e}[rr](e=null){return e?this[ln].filter((t=>t[kr]===e)):this[ln]}[Ar](e){return this[e]}[er](e,t,i=!0){return Array.from(this[tr](e,t,i))}*[tr](e,t,i=!0){if("parent"!==e){for(const i of this[ln]){i[kr]===e&&(yield i);i.name===e&&(yield i);(t||i[Dr]())&&(yield*i[tr](e,t,!1))}i&&this[hn].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[wn]}}class XFAObjectArray{constructor(e=1/0){this[mn]=e;this[ln]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[ln].length<=this[mn]){this[ln].push(e);return!0}warn(`XFA - node "${e[kr]}" accepts no more than ${this[mn]} children`);return!1}isEmpty(){return 0===this[ln].length}dump(){return 1===this[ln].length?this[ln][0][js]():this[ln].map((e=>e[js]()))}[Ts](){const e=new XFAObjectArray(this[mn]);e[ln]=this[ln].map((e=>e[Ts]()));return e}get children(){return this[ln]}clear(){this[ln].length=0}}class XFAAttribute{constructor(e,t,i){this[wn]=e;this[kr]=t;this[Os]=i;this[qs]=!1;this[Vr]="attribute"+Sn++}[Ir](){return this[wn]}[fr](){return!0}[ir](){return this[Os].trim()}[qr](e){e=e.value||"";this[Os]=e.toString()}[Pr](){return this[Os]}[pr](e){return this[wn]===e||this[wn][pr](e)}}class XmlObject extends XFAObject{constructor(e,t,i={}){super(e,t);this[Os]="";this[Qn]=null;if("#text"!==t){const e=new Map;this[Cn]=e;for(const[t,a]of Object.entries(i))e.set(t,new XFAAttribute(this,t,a));if(i.hasOwnProperty(Rr)){const e=i[Rr].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Qn]=!1:"dataValue"===e&&(this[Qn]=!0))}}this[qs]=!1}[Xr](e){const t=this[kr];if("#text"===t){e.push(encodeToXmlString(this[Os]));return}const i=utf8StringToString(t),a=this[Sr]===kn?"xfa:":"";e.push(`<${a}${i}`);for(const[t,i]of this[Cn].entries()){const a=utf8StringToString(t);e.push(` ${a}="${encodeToXmlString(i[Os])}"`)}null!==this[Qn]&&(this[Qn]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[Os]||0!==this[ln].length){e.push(">");if(this[Os])"string"==typeof this[Os]?e.push(encodeToXmlString(this[Os])):this[Os][Xr](e);else for(const t of this[ln])t[Xr](e);e.push(``)}else e.push("/>")}[Nr](e){if(this[Os]){const e=new XmlObject(this[Sr],"#text");this[Hs](e);e[Os]=this[Os];this[Os]=""}this[Hs](e);return!0}[Mr](e){this[Os]+=e}[Zs](){if(this[Os]&&this[ln].length>0){const e=new XmlObject(this[Sr],"#text");this[Hs](e);e[Os]=this[Os];delete this[Os]}}[jr](){return"#text"===this[kr]?HTMLResult.success({name:"#text",value:this[Os]}):HTMLResult.EMPTY}[rr](e=null){return e?this[ln].filter((t=>t[kr]===e)):this[ln]}[_s](){return this[Cn]}[Ar](e){const t=this[Cn].get(e);return void 0!==t?t:this[rr](e)}*[tr](e,t){const i=this[Cn].get(e);i&&(yield i);for(const i of this[ln]){i[kr]===e&&(yield i);t&&(yield*i[tr](e,t))}}*[zs](e,t){const i=this[Cn].get(e);!i||t&&i[qs]||(yield i);for(const i of this[ln])yield*i[zs](e,t)}*[sr](e,t,i){for(const a of this[ln]){a[kr]!==e||i&&a[qs]||(yield a);t&&(yield*a[sr](e,t,i))}}[fr](){return null===this[Qn]?0===this[ln].length||this[ln][0][Sr]===_r.xhtml.id:this[Qn]}[ir](){return null===this[Qn]?0===this[ln].length?this[Os].trim():this[ln][0][Sr]===_r.xhtml.id?this[ln][0][Pr]().trim():null:this[Os].trim()}[qr](e){e=e.value||"";this[Os]=e.toString()}[js](e=!1){const t=Object.create(null);e&&(t.$ns=this[Sr]);this[Os]&&(t.$content=this[Os]);t.$name=this[kr];t.children=[];for(const i of this[ln])t.children.push(i[js](e));t.attributes=Object.create(null);for(const[e,i]of this[Cn])t.attributes[e]=i[Os];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[Os]=""}[Mr](e){this[Os]+=e}[Zs](){}}class OptionObject extends ContentObject{constructor(e,t,i){super(e,t);this[yn]=i}[Zs](){this[Os]=getKeyword({data:this[Os],defaultValue:this[yn][0],validate:e=>this[yn].includes(e)})}[Ys](e){super[Ys](e);delete this[yn]}}class StringObject extends ContentObject{[Zs](){this[Os]=this[Os].trim()}}class IntegerObject extends ContentObject{constructor(e,t,i,a){super(e,t);this[En]=i;this[Fn]=a}[Zs](){this[Os]=getInteger({data:this[Os],defaultValue:this[En],validate:this[Fn]})}[Ys](e){super[Ys](e);delete this[En];delete this[Fn]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,(e=>1===e))}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,(e=>0===e))}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const Rn={anchorType(e,t){const i=e[or]();if(i&&(!i.layout||"position"===i.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const i=e[or]();let a=e.w;const s=e.h;if(i.layout?.includes("row")){const t=i[Xs],s=e.colSpan;let r;if(-1===s){r=t.columnWidths.slice(t.currentColumn).reduce(((e,t)=>e+t),0);t.currentColumn=0}else{r=t.columnWidths.slice(t.currentColumn,t.currentColumn+s).reduce(((e,t)=>e+t),0);t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(r)||(a=e.w=r)}t.width=""!==a?measureToString(a):"auto";t.height=""!==s?measureToString(s):"auto"},position(e,t){const i=e[or]();if(!i?.layout||"position"===i.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[kr])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[Zr]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[or]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,i,a,s,r){const n=new TextMeasure(t,i,a,s);"string"==typeof e?n.addString(e):e[Ur](n);return n.compute(r)}function layoutNode(e,t){let i=null,a=null,s=!1;if((!e.w||!e.h)&&e.value){let r=0,n=0;if(e.margin){r=e.margin.leftInset+e.margin.rightInset;n=e.margin.topInset+e.margin.bottomInset}let g=null,o=null;if(e.para){o=Object.create(null);g=""===e.para.lineHeight?null:e.para.lineHeight;o.top=""===e.para.spaceAbove?0:e.para.spaceAbove;o.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;o.left=""===e.para.marginLeft?0:e.para.marginLeft;o.right=""===e.para.marginRight?0:e.para.marginRight}let c=e.font;if(!c){const t=e[cr]();let i=e[Ir]();for(;i&&i!==t;){if(i.font){c=i.font;break}i=i[Ir]()}}const C=(e.w||t.width)-r,h=e[Cr].fontFinder;if(e.value.exData&&e.value.exData[Os]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[Os],c,o,g,h,C);a=t.width;i=t.height;s=t.isBroken}else{const t=e.value[Pr]();if(t){const e=layoutText(t,c,o,g,h,C);a=e.width;i=e.height;s=e.isBroken}}null===a||e.w||(a+=r);null===i||e.h||(i+=n)}return{w:a,h:i,isBroken:s}}function computeBbox(e,t,i){let a;if(""!==e.w&&""!==e.h)a=[e.x,e.y,e.w,e.h];else{if(!i)return null;let s=e.w;if(""===s){if(0===e.maxW){const t=e[or]();s="position"===t.layout&&""!==t.w?0:e.minW}else s=Math.min(e.maxW,i.width);t.attributes.style.width=measureToString(s)}let r=e.h;if(""===r){if(0===e.maxH){const t=e[or]();r="position"===t.layout&&""!==t.h?0:e.minH}else r=Math.min(e.maxH,i.height);t.attributes.style.height=measureToString(r)}a=[e.x,e.y,s,r]}return a}function fixDimensions(e){const t=e[or]();if(t.layout?.includes("row")){const i=t[Xs],a=e.colSpan;let s;s=-1===a?i.columnWidths.slice(i.currentColumn).reduce(((e,t)=>e+t),0):i.columnWidths.slice(i.currentColumn,i.currentColumn+a).reduce(((e,t)=>e+t),0);isNaN(s)||(e.w=s)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=e.columnWidths.reduce(((e,t)=>e+t),0))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const i=Object.create(null);for(const a of t){const t=e[a];if(null!==t)if(Rn.hasOwnProperty(a))Rn[a](e,i);else if(t instanceof XFAObject){const e=t[Zr]();e?Object.assign(i,e):warn(`(DEBUG) - XFA - style for ${a} not implemented yet`)}}return i}function createWrapper(e,t){const{attributes:i}=t,{style:a}=i,s={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};i.class.push("xfaWrapped");if(e.border){const{widths:i,insets:r}=e.border[Xs];let n,g,o=r[0],c=r[3];const C=r[0]+r[2],h=r[1]+r[3];switch(e.border.hand){case"even":o-=i[0]/2;c-=i[3]/2;n=`calc(100% + ${(i[1]+i[3])/2-h}px)`;g=`calc(100% + ${(i[0]+i[2])/2-C}px)`;break;case"left":o-=i[0];c-=i[3];n=`calc(100% + ${i[1]+i[3]-h}px)`;g=`calc(100% + ${i[0]+i[2]-C}px)`;break;case"right":n=h?`calc(100% - ${h}px)`:"100%";g=C?`calc(100% - ${C}px)`:"100%"}const l=["xfaBorder"];isPrintOnly(e.border)&&l.push("xfaPrintOnly");const Q={name:"div",attributes:{class:l,style:{top:`${o}px`,left:`${c}px`,width:n,height:g}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==a[e]){Q.attributes.style[e]=a[e];delete a[e]}s.children.push(Q,t)}else s.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==a[e]){s.attributes.style[e]=a[e];delete a[e]}s.attributes.style.position="absolute"===a.position?"absolute":"relative";delete a.position;if(a.alignSelf){s.attributes.style.alignSelf=a.alignSelf;delete a.alignSelf}return s}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const i="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),a=getMeasurement(e[i],"0px");e[i]=a-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[cr]()[Xs].paraStack;return t.length?t.at(-1):null}function setPara(e,t,i){if(i.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const a=getCurrentPara(e);if(a){const e=i.attributes.style;e.display="flex";e.flexDirection="column";switch(a.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=a[Zr]();for(const[i,a]of Object.entries(t))i in e||(e[i]=a)}}}function setFontFamily(e,t,i,a){if(!i){delete a.fontFamily;return}const s=stripQuotes(e.typeface);a.fontFamily=`"${s}"`;const r=i.find(s);if(r){const{fontFamily:i}=r.regular.cssFontInfo;i!==s&&(a.fontFamily=`"${i}"`);const n=getCurrentPara(t);if(n&&""!==n.lineHeight)return;if(a.lineHeight)return;const g=selectFont(e,r);g&&(a.lineHeight=Math.max(1.2,g.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[Xs])return null;const t={name:"div",attributes:e[Xs].attributes,children:e[Xs].children};if(e[Xs].failingNode){const i=e[Xs].failingNode[Vs]();i&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[i])):t.children.push(i))}return 0===t.children.length?null:t}function addHTML(e,t,i){const a=e[Xs],s=a.availableSpace,[r,n,g,o]=i;switch(e.layout){case"position":a.width=Math.max(a.width,r+g);a.height=Math.max(a.height,n+o);a.children.push(t);break;case"lr-tb":case"rl-tb":if(!a.line||1===a.attempt){a.line=createLine(e,[]);a.children.push(a.line);a.numberInLine=0}a.numberInLine+=1;a.line.children.push(t);if(0===a.attempt){a.currentWidth+=g;a.height=Math.max(a.height,a.prevHeight+o)}else{a.currentWidth=g;a.prevHeight=a.height;a.height+=o;a.attempt=0}a.width=Math.max(a.width,a.currentWidth);break;case"rl-row":case"row":{a.children.push(t);a.width+=g;a.height=Math.max(a.height,o);const e=measureToString(a.height);for(const t of a.children)t.attributes.style.height=e;break}case"table":case"tb":a.width=Math.min(s.width,Math.max(a.width,g));a.height+=o;a.children.push(t)}}function getAvailableSpace(e){const t=e[Xs].availableSpace,i=e.margin?e.margin.topInset+e.margin.bottomInset:0,a=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[Xs].attempt?{width:t.width-a-e[Xs].currentWidth,height:t.height-i-e[Xs].prevHeight}:{width:t.width-a,height:t.height-i-e[Xs].height};case"rl-row":case"row":return{width:e[Xs].columnWidths.slice(e[Xs].currentColumn).reduce(((e,t)=>e+t)),height:t.height-a};case"table":case"tb":return{width:t.width-a,height:t.height-i-e[Xs].height};default:return t}}function checkDimensions(e,t){if(null===e[cr]()[Xs].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const i=e[or](),a=i[Xs]?.attempt||0,[,s,r,n]=function getTransformedBBox(e){let t,i,a=""===e.w?NaN:e.w,s=""===e.h?NaN:e.h,[r,n]=[0,0];switch(e.anchorType||""){case"bottomCenter":[r,n]=[a/2,s];break;case"bottomLeft":[r,n]=[0,s];break;case"bottomRight":[r,n]=[a,s];break;case"middleCenter":[r,n]=[a/2,s/2];break;case"middleLeft":[r,n]=[0,s/2];break;case"middleRight":[r,n]=[a,s/2];break;case"topCenter":[r,n]=[a/2,0];break;case"topRight":[r,n]=[a,0]}switch(e.rotate||0){case 0:[t,i]=[-r,-n];break;case 90:[t,i]=[-n,r];[a,s]=[s,-a];break;case 180:[t,i]=[r,n];[a,s]=[-a,-s];break;case 270:[t,i]=[n,-r];[a,s]=[-s,a]}return[e.x+t+Math.min(0,a),e.y+i+Math.min(0,s),Math.abs(a),Math.abs(s)]}(e);switch(i.layout){case"lr-tb":case"rl-tb":return 0===a?e[cr]()[Xs].noLayoutFailure?""!==e.w?Math.round(r-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(n-t.height)>2)&&(""!==e.w?Math.round(r-t.width)<=2||0===i[Xs].numberInLine&&t.height>2:t.width>2):!!e[cr]()[Xs].noLayoutFailure||!(""!==e.h&&Math.round(n-t.height)>2)&&((""===e.w||Math.round(r-t.width)<=2||!i[wr]())&&t.height>2);case"table":case"tb":return!!e[cr]()[Xs].noLayoutFailure||(""===e.h||e[yr]()?(""===e.w||Math.round(r-t.width)<=2||!i[wr]())&&t.height>2:Math.round(n-t.height)<=2);case"position":if(e[cr]()[Xs].noLayoutFailure)return!0;if(""===e.h||Math.round(n+s-t.height)<=2)return!0;return n+s>e[cr]()[Xs].currentContentArea.h;case"rl-row":case"row":return!!e[cr]()[Xs].noLayoutFailure||(""===e.h||Math.round(n-t.height)<=2);default:return!0}}const Nn=_r.template.id,Gn="http://www.w3.org/2000/svg",Mn=/^H(\d+)$/,Un=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),xn=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[ar]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Hs](t);e.value=t}e.value[qr](t)}function*getContainedChildren(e){for(const t of e[rr]())t instanceof SubformSet?yield*t[nr]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[Or]=e[Ir]()[Or];return}if(e[Or])return;let t=null;for(const i of e.traversal[rr]())if("next"===i.operation){t=i;break}if(!t||!t.ref){e[Or]=e[Ir]()[Or];return}const i=e[cr]();e[Or]=++i[Or];const a=i[vr](t.ref,e);if(!a)return;e=a[0]}}function applyAssist(e,t){const i=e.assist;if(i){const e=i[jr]();e&&(t.title=e);const a=i.role.match(Mn);if(a){const e="heading",i=a[1];t.role=e;t["aria-level"]=i}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const i=e[Ir]();"row"===i.layout&&(t.role="TH"===i.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[Os]?t.speak[Os]:t.toolTip?t.toolTip[Os]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[cr]();if(null===t[Xs].firstUnsplittable){t[Xs].firstUnsplittable=e;t[Xs].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[cr]();t[Xs].firstUnsplittable===e&&(t[Xs].noLayoutFailure=!1)}function handleBreak(e){if(e[Xs])return!1;e[Xs]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[cr]();let i=null;if(e.target){i=t[vr](e.target,e[Ir]());if(!i)return!1;i=i[0]}const{currentPageArea:a,currentContentArea:s}=t[Xs];if("pageArea"===e.targetType){i instanceof PageArea||(i=null);if(e.startNew){e[Xs].target=i||a;return!0}if(i&&i!==a){e[Xs].target=i;return!0}return!1}i instanceof ContentArea||(i=null);const r=i&&i[Ir]();let n,g=r;if(e.startNew)if(i){const e=r.contentArea.children,t=e.indexOf(s),a=e.indexOf(i);-1!==t&&te;a[Xs].noLayoutFailure=!0;const n=t[jr](i);e[Ls](n.html,n.bbox);a[Xs].noLayoutFailure=s;t[or]=r}class AppearanceFilter extends StringObject{constructor(e){super(Nn,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(Nn,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[jr](){const e=this.edge||new Edge({}),t=e[Zr](),i=Object.create(null);"visible"===this.fill?.presence?Object.assign(i,this.fill[Zr]()):i.fill="transparent";i.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);i.stroke=t.color;let a;const s={xmlns:Gn,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)a={name:"ellipse",attributes:{xmlns:Gn,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:i}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,r=this.sweepAngle>180?1:0,[n,g,o,c]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];a={name:"path",attributes:{xmlns:Gn,d:`M ${n} ${g} A 50 50 0 ${r} 0 ${o} ${c}`,vectorEffect:"non-scaling-stroke",style:i}};Object.assign(s,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const r={name:"svg",children:[a],attributes:s};if(hasMargin(this[Ir]()[Ir]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[r]});r.attributes.style.position="absolute";return HTMLResult.success(r)}}class Area extends XFAObject{constructor(e){super(Nn,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[nr](){yield*getContainedChildren(this)}[Dr](){return!0}[dr](){return!0}[Ls](e,t){const[i,a,s,r]=t;this[Xs].width=Math.max(this[Xs].width,i+s);this[Xs].height=Math.max(this[Xs].height,a+r);this[Xs].children.push(e)}[$s](){return this[Xs].availableSpace}[jr](e){const t=toStyle(this,"position"),i={style:t,id:this[Vr],class:["xfaArea"]};isPrintOnly(this)&&i.class.push("xfaPrintOnly");this.name&&(i.xfaName=this.name);const a=[];this[Xs]={children:a,width:0,height:0,availableSpace:e};const s=this[Js]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!s.success){if(s.isBreak())return s;delete this[Xs];return HTMLResult.FAILURE}t.width=measureToString(this[Xs].width);t.height=measureToString(this[Xs].height);const r={name:"div",attributes:i,children:a},n=[this.x,this.y,this[Xs].width,this[Xs].height];delete this[Xs];return HTMLResult.success(r,n)}}class Assist extends XFAObject{constructor(e){super(Nn,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[jr](){return this.toolTip?.[Os]||null}}class Barcode extends XFAObject{constructor(e){super(Nn,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(Nn,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(Nn,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(Nn,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(Nn,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[jr](e){return valueToHtml(1===this[Os]?"1":"0")}}class Border extends XFAObject{constructor(e){super(Nn,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[ar](){if(!this[Xs]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let i=e.length;i<4;i++)e.push(t)}const t=e.map((e=>e.thickness)),i=[0,0,0,0];if(this.margin){i[0]=this.margin.topInset;i[1]=this.margin.rightInset;i[2]=this.margin.bottomInset;i[3]=this.margin.leftInset}this[Xs]={widths:t,insets:i,edges:e}}return this[Xs]}[Zr](){const{edges:e}=this[ar](),t=e.map((e=>{const t=e[Zr]();t.color||="#000000";return t})),i=Object.create(null);this.margin&&Object.assign(i,this.margin[Zr]());"visible"===this.fill?.presence&&Object.assign(i,this.fill[Zr]());if(this.corner.children.some((e=>0!==e.radius))){const e=this.corner.children.map((e=>e[Zr]()));if(2===e.length||3===e.length){const t=e.at(-1);for(let i=e.length;i<4;i++)e.push(t)}i.borderRadius=e.map((e=>e.radius)).join(" ")}switch(this.presence){case"invisible":case"hidden":i.borderStyle="";break;case"inactive":i.borderStyle="none";break;default:i.borderStyle=t.map((e=>e.style)).join(" ")}i.borderWidth=t.map((e=>e.width)).join(" ");i.borderColor=t.map((e=>e.color)).join(" ");return i}}class Break extends XFAObject{constructor(e){super(Nn,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(Nn,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(Nn,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[jr](e){this[Xs]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(Nn,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[jr](e){const t=this[Ir]()[Ir](),i={name:"button",attributes:{id:this[Vr],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[Os]);if(!t)continue;const a=fixURL(t.url);a&&i.children.push({name:"a",attributes:{id:"link"+this[Vr],href:a,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(i)}}class Calculate extends XFAObject{constructor(e){super(Nn,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(Nn,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[qr](e){_setValue(this,e)}[ar](e){if(!this[Xs]){let{width:t,height:i}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":i=this.reserve<=0?i:this.reserve}this[Xs]=layoutNode(this,{width:t,height:i})}return this[Xs]}[jr](e){if(!this.value)return HTMLResult.EMPTY;this[Lr]();const t=this.value[jr](e).html;if(!t){this[xr]();return HTMLResult.EMPTY}const i=this.reserve;if(this.reserve<=0){const{w:t,h:i}=this[ar](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=i}}const a=[];"string"==typeof t?a.push({name:"#text",value:t}):a.push(t);const s=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(s.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(s.height=measureToString(this.reserve))}setPara(this,null,t);this[xr]();this.reserve=i;return HTMLResult.success({name:"div",attributes:{style:s,class:["xfaCaption"]},children:a})}}class Certificate extends StringObject{constructor(e){super(Nn,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(Nn,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(Nn,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle("margin"),i=measureToString(this.size);t.width=t.height=i;let a,s,r;const n=this[Ir]()[Ir](),g=n.items.children.length&&n.items.children[0][jr]().html||[],o={on:(void 0!==g[0]?g[0]:"on").toString(),off:(void 0!==g[1]?g[1]:"off").toString()},c=(n.value?.[Pr]()||"off")===o.on||void 0,C=n[or](),h=n[Vr];let l;if(C instanceof ExclGroup){r=C[Vr];a="radio";s="xfaRadio";l=C[Ws]?.[Vr]||C[Vr]}else{a="checkbox";s="xfaCheckbox";l=n[Ws]?.[Vr]||n[Vr]}const Q={name:"input",attributes:{class:[s],style:t,fieldId:h,dataId:l,type:a,checked:c,xfaOn:o.on,xfaOff:o.off,"aria-label":ariaLabel(n),"aria-required":!1}};r&&(Q.attributes.name=r);if(isRequired(n)){Q.attributes["aria-required"]=!0;Q.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[Q]})}}class ChoiceList extends XFAObject{constructor(e){super(Nn,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","margin"),i=this[Ir]()[Ir](),a={fontSize:`calc(${i.font?.size||10}px * var(--scale-factor))`},s=[];if(i.items.children.length>0){const e=i.items;let t=0,r=0;if(2===e.children.length){t=e.children[0].save;r=1-t}const n=e.children[t][jr]().html,g=e.children[r][jr]().html;let o=!1;const c=i.value?.[Pr]()||"";for(let e=0,t=n.length;eMath.min(Math.max(0,parseInt(e.trim(),10)),255))).map((e=>isNaN(e)?0:e));if(r.length<3)return{r:i,g:a,b:s};[i,a,s]=r;return{r:i,g:a,b:s}}(e.value):"";this.extras=null}[hr](){return!1}[Zr](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(Nn,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(Nn,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(Nn,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[jr](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},i=["xfaContentarea"];isPrintOnly(this)&&i.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:i,id:this[Vr]}})}}class Corner extends XFAObject{constructor(e){super(Nn,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(Nn,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class DateTime extends ContentObject{constructor(e){super(Nn,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(Nn,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin"),i=this[Ir]()[Ir](),a={name:"input",attributes:{type:"text",fieldId:i[Vr],dataId:i[Ws]?.[Vr]||i[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(i),"aria-required":!1}};if(isRequired(i)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Decimal extends ContentObject{constructor(e){super(Nn,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseFloat(this[Os].trim());this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(Nn,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(Nn,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(Nn,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(Nn,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(Nn,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[qr](e){_setValue(this,e)}[jr](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Lr]();const t=this.w,i=this.h,{w:a,h:s,isBroken:r}=layoutNode(this,e);if(a&&""===this.w){if(r&&this[or]()[wr]()){this[xr]();return HTMLResult.FAILURE}this.w=a}s&&""===this.h&&(this.h=s);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=i;this[xr]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const n=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,n);if(n.margin){n.padding=n.margin;delete n.margin}const g=["xfaDraw"];this.font&&g.push("xfaFont");isPrintOnly(this)&&g.push("xfaPrintOnly");const o={style:n,id:this[Vr],class:g};this.name&&(o.xfaName=this.name);const c={name:"div",attributes:o,children:[]};applyAssist(this,o);const C=computeBbox(this,c,e),h=this.value?this.value[jr](e).html:null;if(null===h){this.w=t;this.h=i;this[xr]();return HTMLResult.success(createWrapper(this,c),C)}c.children.push(h);setPara(this,n,h);this.w=t;this.h=i;this[xr]();return HTMLResult.success(createWrapper(this,c),C)}}class Edge extends XFAObject{constructor(e){super(Nn,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[Zr]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(Nn,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(Nn,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(Nn,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(Nn,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(Nn,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(Nn,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(Nn,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(Nn,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(Nn,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[ur](){return"text/html"===this.contentType}[Nr](e){if("text/html"===this.contentType&&e[Sr]===_r.xhtml.id){this[Os]=e;return!0}if("text/xml"===this.contentType){this[Os]=e;return!0}return!1}[jr](e){return"text/html"===this.contentType&&this[Os]?this[Os][jr](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(Nn,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(Nn,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[dr](){return!0}[hr](){return!0}[qr](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Hs](e);t.value=e}t.value[qr](e)}}[wr](){return this.layout.endsWith("-tb")&&0===this[Xs].attempt&&this[Xs].numberInLine>0||this[Ir]()[wr]()}[yr](){const e=this[or]();if(!e[yr]())return!1;if(void 0!==this[Xs]._isSplittable)return this[Xs]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[Xs]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[Xs].numberInLine)return!1;this[Xs]._isSplittable=!0;return!0}[Vs](){return flushHTML(this)}[Ls](e,t){addHTML(this,e,t)}[$s](){return getAvailableSpace(this)}[jr](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],i={id:this[Vr],class:[]};setAccess(this,i.class);this[Xs]||(this[Xs]=Object.create(null));Object.assign(this[Xs],{children:t,attributes:i,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const a=this[yr]();a||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["field"]);if(this.layout.includes("row")){const e=this[or]().columnWidths;if(Array.isArray(e)&&e.length>0){this[Xs].columnWidths=e;this[Xs].currentColumn=0}}const r=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),n=["xfaExclgroup"],g=layoutClass(this);g&&n.push(g);isPrintOnly(this)&&n.push("xfaPrintOnly");i.style=r;i.class=n;this.name&&(i.xfaName=this.name);this[Lr]();const o="lr-tb"===this.layout||"rl-tb"===this.layout,c=o?2:1;for(;this[Xs].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[dr](){return!0}[qr](e){_setValue(this,e)}[jr](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[Cr]=this[Cr];this[Hs](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Hs](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[Xs];this[Lr]();const t=this.caption?this.caption[jr](e).html:null,i=this.w,a=this.h;let s=0,r=0;if(this.margin){s=this.margin.leftInset+this.margin.rightInset;r=this.margin.topInset+this.margin.bottomInset}let n=null;if(""===this.w||""===this.h){let t=null,i=null,a=0,g=0;if(this.ui.checkButton)a=g=this.ui.checkButton.size;else{const{w:t,h:i}=layoutNode(this,e);if(null!==t){a=t;g=i}else g=function fonts_getMetrics(e,t=!1){let i=null;if(e){const t=stripQuotes(e.typeface),a=e[Cr].fontFinder.find(t);i=selectFont(e,a)}if(!i)return{lineHeight:12,lineGap:2,lineNoGap:10};const a=e.size||10,s=i.lineHeight?Math.max(t?0:1.2,i.lineHeight):1.2,r=void 0===i.lineGap?.2:i.lineGap;return{lineHeight:s*a,lineGap:r*a,lineNoGap:Math.max(1,s-r)*a}}(this.font,!0).lineNoGap}n=getBorderDims(this.ui[ar]());a+=n.w;g+=n.h;if(this.caption){const{w:s,h:r,isBroken:n}=this.caption[ar](e);if(n&&this[or]()[wr]()){this[xr]();return HTMLResult.FAILURE}t=s;i=r;switch(this.caption.placement){case"left":case"right":case"inline":t+=a;break;case"top":case"bottom":i+=g}}else{t=a;i=g}if(t&&""===this.w){t+=s;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(Nn,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseFloat(this[Os].trim());this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class template_Font extends XFAObject{constructor(e){super(Nn,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[Ys](e){super[Ys](e);this[Cr].usedTypefaces.add(this.typeface)}[Zr](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[Cr].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(Nn,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(Nn,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(Nn,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(Nn,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[jr](){if(this.contentType&&!Un.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[Cr].images&&this[Cr].images.get(this.href);if(!e&&(this.href||!this[Os]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=function fromBase64Util(e){return Uint8Array.fromBase64?Uint8Array.fromBase64(e):stringToBytes(atob(e))}(this[Os]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,i]of xn)if(e.length>t.length&&t.every(((t,i)=>t===e[i]))){this.contentType=i;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let i;switch(this.aspect){case"fit":case"actual":break;case"height":i={height:"100%",objectFit:"fill"};break;case"none":i={width:"100%",height:"100%",objectFit:"fill"};break;case"width":i={width:"100%",objectFit:"fill"}}const a=this[Ir]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:i,src:URL.createObjectURL(t),alt:a?ariaLabel(a[Ir]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(Nn,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(Nn,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseInt(this[Os].trim(),10);this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class Issuers extends XFAObject{constructor(e){super(Nn,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(Nn,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[jr](){const e=[];for(const t of this[rr]())e.push(t[Pr]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(Nn,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(Nn,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(Nn,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[jr](){const e=this[Ir]()[Ir](),t=this.edge||new Edge({}),i=t[Zr](),a=Object.create(null),s="visible"===t.presence?t.thickness:0;a.strokeWidth=measureToString(s);a.stroke=i.color;let r,n,g,o,c="100%",C="100%";if(e.w<=s){[r,n,g,o]=["50%",0,"50%","100%"];c=a.strokeWidth}else if(e.h<=s){[r,n,g,o]=[0,"50%","100%","50%"];C=a.strokeWidth}else"\\"===this.slope?[r,n,g,o]=[0,0,"100%","100%"]:[r,n,g,o]=[0,"100%","100%",0];const h={name:"svg",children:[{name:"line",attributes:{xmlns:Gn,x1:r,y1:n,x2:g,y2:o,style:a}}],attributes:{xmlns:Gn,width:c,height:C,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[h]});h.attributes.style.position="absolute";return HTMLResult.success(h)}}class Linear extends XFAObject{constructor(e){super(Nn,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[Zr]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(Nn,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){this[Os]=getStringOption(this[Os],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(Nn,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(Nn,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Zr](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(Nn,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(Nn,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const i=e.trim().split(/\s*,\s*/).map((e=>getMeasurement(e,"-1")));if(i.length<4||i[2]<0||i[3]<0)return{x:t,y:t,width:t,height:t};const[a,s,r,n]=i;return{x:a,y:s,width:r,height:n}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(Nn,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(Nn,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin"),i=this[Ir]()[Ir](),a={name:"input",attributes:{type:"text",fieldId:i[Vr],dataId:i[Ws]?.[Vr]||i[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(i),"aria-required":!1}};if(isRequired(i)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Occur extends XFAObject{constructor(e){super(Nn,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Ys](){const e=this[Ir](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[br](){if(!this[Xs]){this[Xs]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[Xs].numberOfUsee.oddOrEven===t&&e.pagePosition===i));if(a)return a;a=this.pageArea.children.find((e=>"any"===e.oddOrEven&&e.pagePosition===i));if(a)return a;a=this.pageArea.children.find((e=>"any"===e.oddOrEven&&"any"===e.pagePosition));return a||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(Nn,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map(((e,t)=>t%2==1?getMeasurement(e):e));this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[Zr](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[Zr]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(Nn,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(Nn,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";const t=this.color?this.color[Zr]():"#000000",i="repeating-linear-gradient",a=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${i}(to top,${a}) ${i}(to right,${a})`;case"crossDiagonal":return`${i}(45deg,${a}) ${i}(-45deg,${a})`;case"diagonalLeft":return`${i}(45deg,${a})`;case"diagonalRight":return`${i}(-45deg,${a})`;case"horizontal":return`${i}(to top,${a})`;case"vertical":return`${i}(to right,${a})`}return""}}class Picture extends StringObject{constructor(e){super(Nn,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(Nn,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(Nn,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";const t=this.color?this.color[Zr]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(Nn,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(Nn,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(Nn,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[jr](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[Zr](),i=Object.create(null);"visible"===this.fill?.presence?Object.assign(i,this.fill[Zr]()):i.fill="transparent";i.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);i.stroke=t.color;const a=(this.corner.children.length?this.corner.children[0]:new Corner({}))[Zr](),s={name:"svg",children:[{name:"rect",attributes:{xmlns:Gn,width:"100%",height:"100%",x:0,y:0,rx:a.radius,ry:a.radius,style:i}}],attributes:{xmlns:Gn,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[Ir]()[Ir]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[s]});s.attributes.style.position="absolute";return HTMLResult.success(s)}}class RefElement extends StringObject{constructor(e){super(Nn,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(Nn,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(Nn,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(Nn,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(Nn,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(Nn,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(Nn,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Zr](e){return e?e[Zr]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(Nn,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(Nn,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(Nn,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map((e=>"-1"===e?-1:getMeasurement(e)));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[or](){const e=this[Ir]();return e instanceof SubformSet?e[or]():e}[dr](){return!0}[wr](){return this.layout.endsWith("-tb")&&0===this[Xs].attempt&&this[Xs].numberInLine>0||this[Ir]()[wr]()}*[nr](){yield*getContainedChildren(this)}[Vs](){return flushHTML(this)}[Ls](e,t){addHTML(this,e,t)}[$s](){return getAvailableSpace(this)}[yr](){const e=this[or]();if(!e[yr]())return!1;if(void 0!==this[Xs]._isSplittable)return this[Xs]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[Xs]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[Xs]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[Xs].numberInLine)return!1;this[Xs]._isSplittable=!0;return!0}[jr](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[Cr]=this[Cr];this[Hs](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[Cr]=this[Cr];this[Hs](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[Cr]=this[Cr];this[Hs](e);this.overflow.push(e)}this[Hr](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[Xs]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],i={id:this[Vr],class:[]};setAccess(this,i.class);this[Xs]||(this[Xs]=Object.create(null));Object.assign(this[Xs],{children:t,line:null,attributes:i,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const a=this[cr](),s=a[Xs].noLayoutFailure,r=this[yr]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const n=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[or]().columnWidths;if(Array.isArray(e)&&e.length>0){this[Xs].columnWidths=e;this[Xs].currentColumn=0}}const g=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),o=["xfaSubform"],c=layoutClass(this);c&&o.push(c);i.style=g;i.class=o;this.name&&(i.xfaName=this.name);if(this.overflow){const t=this.overflow[ar]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Lr]();const C="lr-tb"===this.layout||"rl-tb"===this.layout,h=C?2:1;for(;this[Xs].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[Xs].afterBreakAfter=p;return HTMLResult.breakNode(e)}}delete this[Xs];return p}}class SubformSet extends XFAObject{constructor(e){super(Nn,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[nr](){yield*getContainedChildren(this)}[or](){let e=this[Ir]();for(;!(e instanceof Subform);)e=e[Ir]();return e}[dr](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(Nn,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){this[Os]=new Map(this[Os].split(this.delimiter).map((e=>{(e=e.split("=",2))[0]=e[0].trim();return e})))}}class SubjectDNs extends XFAObject{constructor(e){super(Nn,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(Nn,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(Nn,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[Zs](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[Or]=5e3}[yr](){return!0}[vr](e,t){return e.startsWith("#")?[this[lr].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[Wr](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[Xs]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[vs]();const t=e.pageSet.pageArea.children,i={name:"div",children:[]};let a=null,s=null,r=null;if(e.breakBefore.children.length>=1){s=e.breakBefore.children[0];r=s.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){s=e.subform.children[0].breakBefore.children[0];r=s.target}else if(e.break?.beforeTarget){s=e.break;r=s.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){s=e.subform.children[0].break;r=s.beforeTarget}if(s){const e=this[vr](r,s[Ir]());if(e instanceof PageArea){a=e;s[Xs]={}}}a||(a=t[0]);a[Xs]={numberOfUse:1};const n=a[Ir]();n[Xs]={numberOfUse:1,pageIndex:n.pageArea.children.indexOf(a),pageSetIndex:0};let g,o=null,c=null,C=!0,h=0,l=0;for(;;){if(C)h=0;else{i.children.pop();if(3==++h){warn("XFA - Something goes wrong: please file a bug.");return i}}g=null;this[Xs].currentPageArea=a;const t=a[jr]().html;i.children.push(t);if(o){this[Xs].noLayoutFailure=!0;t.children.push(o[jr](a[Xs].space).html);o=null}if(c){this[Xs].noLayoutFailure=!0;t.children.push(c[jr](a[Xs].space).html);c=null}const s=a.contentArea.children,r=t.children.filter((e=>e.attributes.class.includes("xfaContentarea")));C=!1;this[Xs].firstUnsplittable=null;this[Xs].noLayoutFailure=!1;const flush=t=>{const i=e[Vs]();if(i){C||=i.children?.length>0;r[t].children.push(i)}};for(let t=l,a=s.length;t0;r[t].children.push(h.html)}else!C&&i.children.length>1&&i.children.pop();return i}if(h.isBreak()){const e=h.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){o=this[vr](e.leader,e[Ir]());o=o?o[0]:null}if(e.trailer){c=this[vr](e.trailer,e[Ir]());c=c?c[0]:null}if("pageArea"===e.targetType){g=e[Xs].target;t=1/0}else if(e[Xs].target){g=e[Xs].target;l=e[Xs].index+1;t=1/0}else t=e[Xs].index}else if(this[Xs].overflowNode){const e=this[Xs].overflowNode;this[Xs].overflowNode=null;const i=e[ar](),a=i.target;i.addLeader=null!==i.leader;i.addTrailer=null!==i.trailer;flush(t);const r=t;t=1/0;if(a instanceof PageArea)g=a;else if(a instanceof ContentArea){const e=s.indexOf(a);if(-1!==e)e>r?t=e-1:l=e;else{g=a[Ir]();l=g.contentArea.children.indexOf(a)}}}else flush(t)}this[Xs].pageNumber+=1;g&&(g[br]()?g[Xs].numberOfUse+=1:g=null);a=g||a[gr]();yield null}}}class Text extends ContentObject{constructor(e){super(Nn,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[xs](){return!0}[Nr](e){if(e[Sr]===_r.xhtml.id){this[Os]=e;return!0}warn(`XFA - Invalid content in Text: ${e[kr]}.`);return!1}[Mr](e){this[Os]instanceof XFAObject||super[Mr](e)}[Zs](){"string"==typeof this[Os]&&(this[Os]=this[Os].replaceAll("\r\n","\n"))}[ar](){return"string"==typeof this[Os]?this[Os].split(/[\u2029\u2028\n]/).reduce(((e,t)=>{t&&e.push(t);return e}),[]).join("\n"):this[Os][Pr]()}[jr](e){if("string"==typeof this[Os]){const e=valueToHtml(this[Os]).html;if(this[Os].includes("\u2029")){e.name="div";e.children=[];this[Os].split("\u2029").map((e=>e.split(/[\u2028\n]/).reduce(((e,t)=>{e.push({name:"span",value:t},{name:"br"});return e}),[]))).forEach((t=>{e.children.push({name:"p",children:t})}))}else if(/[\u2028\n]/.test(this[Os])){e.name="div";e.children=[];this[Os].split(/[\u2028\n]/).forEach((t=>{e.children.push({name:"span",value:t},{name:"br"})}))}return HTMLResult.success(e)}return this[Os][jr](e)}}class TextEdit extends XFAObject{constructor(e){super(Nn,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin");let i;const a=this[Ir]()[Ir]();""===this.multiLine&&(this.multiLine=a instanceof Draw?1:0);i=1===this.multiLine?{name:"textarea",attributes:{dataId:a[Ws]?.[Vr]||a[Vr],fieldId:a[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:a[Ws]?.[Vr]||a[Vr],fieldId:a[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){i.attributes["aria-required"]=!0;i.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[i]})}}class Time extends StringObject{constructor(e){super(Nn,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(Nn,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(Nn,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(Nn,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(Nn,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[Dr](){return!1}}class Ui extends XFAObject{constructor(e){super(Nn,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[ar](){if(void 0===this[Xs]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[Xs]=t;return t}}this[Xs]=null}return this[Xs]}[jr](e){const t=this[ar]();return t?t[jr](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(Nn,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(Nn,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[qr](e){const t=this[Ir]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Hs](this.image)}this.image[Os]=e[Os];return}const i=e[kr];if(null===this[i]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Hr](t)}}this[e[kr]]=e;this[Hs](e)}else this[i][Os]=e[Os]}[Pr](){if(this.exData)return"string"==typeof this.exData[Os]?this.exData[Os].trim():this.exData[Os][Pr]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[Os]||"").toString().trim()}return null}[jr](e){for(const t of Object.getOwnPropertyNames(this)){const i=this[t];if(i instanceof XFAObject)return i[jr](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(Nn,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[Dr](){return!0}}class TemplateNamespace{static[zr](e,t){if(TemplateNamespace.hasOwnProperty(e)){const i=TemplateNamespace[e](t);i[Tr](t);return i}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Ln=_r.datasets.id;function createText(e){const t=new Text({});t[Os]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(_r.datasets.id,"data");this.emptyMerge=0===this.data[rr]().length;this.root.form=this.form=e.template[Ts]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,i){e[Ws]=t;if(e[hr]())if(t[fr]()){const i=t[ir]();e[qr](createText(i))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const i=t[rr]().map((e=>e[Os].trim())).join("\n");e[qr](createText(i))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[fr]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,i,a){if(!e)return null;let s,r;for(let a=0;a<3;a++){s=i[sr](e,!1,!0);for(;;){r=s.next().value;if(!r)break;if(t===r[fr]())return r}if(i[Sr]===_r.datasets.id&&"data"===i[kr])break;i=i[Ir]()}if(!a)return null;s=this.data[sr](e,!0,!1);r=s.next().value;if(r)return r;s=this.data[zs](e,!0);r=s.next().value;return r?.[fr]()?r:null}_setProperties(e,t){if(e.hasOwnProperty("setProperty"))for(const{ref:i,target:a,connection:s}of e.setProperty.children){if(s)continue;if(!i)continue;const r=searchNode(this.root,t,i,!1,!1);if(!r){warn(`XFA - Invalid reference: ${i}.`);continue}const[n]=r;if(!n[pr](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const g=searchNode(this.root,e,a,!1,!1);if(!g){warn(`XFA - Invalid target: ${a}.`);continue}const[o]=g;if(!o[pr](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const c=o[Ir]();if(o instanceof SetProperty||c instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(o instanceof BindItems||c instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const C=n[Pr](),h=o[kr];if(o instanceof XFAAttribute){const e=Object.create(null);e[h]=C;const t=Reflect.construct(Object.getPrototypeOf(c).constructor,[e]);c[h]=t[h]}else if(o.hasOwnProperty(Os)){o[Ws]=n;o[Os]=C;o[Zs]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!e.hasOwnProperty("items")||!e.hasOwnProperty("bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Hr](t);e.items.clear();const i=new Items({}),a=new Items({});e[Hs](i);e.items.push(i);e[Hs](a);e.items.push(a);for(const{ref:s,labelRef:r,valueRef:n,connection:g}of e.bindItems.children){if(g)continue;if(!s)continue;const e=searchNode(this.root,t,s,!1,!1);if(e)for(const t of e){if(!t[pr](this.datasets)){warn(`XFA - Invalid ref (${s}): must be a datasets child.`);continue}const e=searchNode(this.root,t,r,!0,!1);if(!e){warn(`XFA - Invalid label: ${r}.`);continue}const[g]=e;if(!g[pr](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const o=searchNode(this.root,t,n,!0,!1);if(!o){warn(`XFA - Invalid value: ${n}.`);continue}const[c]=o;if(!c[pr](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const C=createText(g[Pr]()),h=createText(c[Pr]());i[Hs](C);i.text.push(C);a[Hs](h);a.text.push(h)}else warn(`XFA - Invalid reference: ${s}.`)}}_bindOccurrences(e,t,i){let a;if(t.length>1){a=e[Ts]();a[Hr](a.occur);a.occur=null}this._bindValue(e,t[0],i);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const s=e[Ir](),r=e[kr],n=s[Qr](e);for(let e=1,g=t.length;et.name===e.name)).length:i[a].children.length;const r=i[Qr](e)+1,n=t.initial-s;if(n){const t=e[Ts]();t[Hr](t.occur);t.occur=null;i[a].push(t);i[Er](r,t);for(let e=1;e0)this._bindOccurrences(a,[e[0]],null);else if(this.emptyMerge){const e=t[Sr]===Ln?-1:t[Sr],i=a[Ws]=new XmlObject(e,a.name||"root");t[Hs](i);this._bindElement(a,i)}continue}if(!a[dr]())continue;let e=!1,s=null,r=null,n=null;if(a.bind){switch(a.bind.match){case"none":this._setAndBind(a,t);continue;case"global":e=!0;break;case"dataRef":if(!a.bind.ref){warn(`XFA - ref is empty in node ${a[kr]}.`);this._setAndBind(a,t);continue}r=a.bind.ref}a.bind.picture&&(s=a.bind.picture[Os])}const[g,o]=this._getOccurInfo(a);if(r){n=searchNode(this.root,t,r,!0,!1);if(null===n){n=createDataNode(this.data,t,r);if(!n)continue;this._isConsumeData()&&(n[qs]=!0);this._setAndBind(a,n);continue}this._isConsumeData()&&(n=n.filter((e=>!e[qs])));n.length>o?n=n.slice(0,o):0===n.length&&(n=null);n&&this._isConsumeData()&&n.forEach((e=>{e[qs]=!0}))}else{if(!a.name){this._setAndBind(a,t);continue}if(this._isConsumeData()){const i=[];for(;i.length0?i:null}else{n=t[sr](a.name,!1,this.emptyMerge).next().value;if(!n){if(0===g){i.push(a);continue}const e=t[Sr]===Ln?-1:t[Sr];n=a[Ws]=new XmlObject(e,a.name);this.emptyMerge&&(n[qs]=!0);t[Hs](n);this._setAndBind(a,n);continue}this.emptyMerge&&(n[qs]=!0);n=[n]}}n?this._bindOccurrences(a,n,s):g>0?this._setAndBind(a,t):i.push(a)}i.forEach((e=>e[Ir]()[Hr](e)))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[rr]()]];for(;t.length>0;){const i=t.at(-1),[a,s]=i;if(a+1===s.length){t.pop();continue}const r=s[++i[0]],n=e.get(r[Vr]);if(n)r[qr](n);else{const t=r[_s]();for(const i of t.values()){const t=e.get(i[Vr]);if(t){i[qr](t);break}}}const g=r[rr]();g.length>0&&t.push([-1,g])}const i=[''];if(this.dataset)for(const e of this.dataset[rr]())"data"!==e[kr]&&e[Xr](i);this.data[Xr](i);i.push("");return i.join("")}}const Hn=_r.config.id;class Acrobat extends XFAObject{constructor(e){super(Hn,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Hn,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Hn,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Hn,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Hn,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Hn,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Hn,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Hn,"adobeExtensionLevel",0,(e=>e>=1&&e<=8))}}class Agent extends XFAObject{constructor(e){super(Hn,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Hn,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Hn,"amd")}}class config_Area extends XFAObject{constructor(e){super(Hn,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Hn,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Hn,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Hn,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Hn,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Hn,"behaviorOverride")}[Zs](){this[Os]=new Map(this[Os].trim().split(/\s+/).filter((e=>e.includes(":"))).map((e=>e.split(":",2))))}}class Cache extends XFAObject{constructor(e){super(Hn,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Hn,"change")}}class Common extends XFAObject{constructor(e){super(Hn,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Hn,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Hn,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Hn,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Hn,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Hn,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Hn,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Hn,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Hn,"copies",1,(e=>e>=1))}}class Creator extends StringObject{constructor(e){super(Hn,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Hn,"currentPage",0,(e=>e>=0))}}class Data extends XFAObject{constructor(e){super(Hn,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Hn,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Hn,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Hn,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Hn,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Hn,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Hn,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Hn,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Hn,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Hn,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Hn,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Hn,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Hn,"enforce")}}class Equate extends XFAObject{constructor(e){super(Hn,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Hn,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,i=this._unicodeRange;for(let a of i.split(",").map((e=>e.trim())).filter((e=>!!e))){a=a.split("-",2).map((e=>{const i=e.match(t);return i?parseInt(i[1],16):0}));1===a.length&&a.push(a[0]);e.push(a)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Hn,"exclude")}[Zs](){this[Os]=this[Os].trim().split(/\s+/).filter((e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e)))}}class ExcludeNS extends StringObject{constructor(e){super(Hn,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Hn,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Hn,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Hn,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Hn,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Hn,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Hn,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Hn,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Hn,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Hn,"interactive")}}class Jog extends OptionObject{constructor(e){super(Hn,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Hn,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Hn,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Hn,"level",0,(e=>e>0))}}class Linearized extends Option01{constructor(e){super(Hn,"linearized")}}class Locale extends StringObject{constructor(e){super(Hn,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Hn,"localeSet")}}class Log extends XFAObject{constructor(e){super(Hn,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Hn,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Hn,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Hn,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Hn,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Hn,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Hn,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Hn,"msgId",1,(e=>e>=1))}}class NameAttr extends StringObject{constructor(e){super(Hn,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Hn,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Hn,"numberOfCopies",null,(e=>e>=2&&e<=5))}}class OpenAction extends XFAObject{constructor(e){super(Hn,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Hn,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Hn,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Hn,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Hn,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Hn,"packets")}[Zs](){"*"!==this[Os]&&(this[Os]=this[Os].trim().split(/\s+/).filter((e=>["config","datasets","template","xfdf","xslt"].includes(e))))}}class PageOffset extends XFAObject{constructor(e){super(Hn,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Hn,"pageRange")}[Zs](){const e=this[Os].trim().split(/\s+/).map((e=>parseInt(e,10))),t=[];for(let i=0,a=e.length;i!1))}}class Pcl extends XFAObject{constructor(e){super(Hn,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Hn,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Hn,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Hn,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Hn,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Hn,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Hn,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Hn,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Hn,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Hn,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Hn,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Hn,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Hn,"printerName")}}class Producer extends StringObject{constructor(e){super(Hn,"producer")}}class Ps extends XFAObject{constructor(e){super(Hn,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Hn,"range")}[Zs](){this[Os]=this[Os].trim().split(/\s*,\s*/,2).map((e=>e.split("-").map((e=>parseInt(e.trim(),10))))).filter((e=>e.every((e=>!isNaN(e))))).map((e=>{1===e.length&&e.push(e[0]);return e}))}}class Record extends ContentObject{constructor(e){super(Hn,"record")}[Zs](){this[Os]=this[Os].trim();const e=parseInt(this[Os],10);!isNaN(e)&&e>=0&&(this[Os]=e)}}class Relevant extends ContentObject{constructor(e){super(Hn,"relevant")}[Zs](){this[Os]=this[Os].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Hn,"rename")}[Zs](){this[Os]=this[Os].trim();(this[Os].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[Os]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Hn,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Hn,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Hn,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Hn,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Hn,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Hn,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Hn,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Hn,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Hn,"startPage",0,(e=>!0))}}class SubmitFormat extends OptionObject{constructor(e){super(Hn,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Hn,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Hn,"subsetBelow",100,(e=>e>=0&&e<=100))}}class SuppressBanner extends Option01{constructor(e){super(Hn,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Hn,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Hn,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Hn,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Hn,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Hn,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Hn,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Hn,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Hn,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Hn,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Hn,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Hn,"validateApprovalSignatures")}[Zs](){this[Os]=this[Os].trim().split(/\s+/).filter((e=>["docReady","postSign"].includes(e)))}}class ValidationMessaging extends OptionObject{constructor(e){super(Hn,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Hn,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Hn,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Hn,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Hn,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Hn,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Hn,"window")}[Zs](){const e=this[Os].trim().split(/\s*,\s*/,2).map((e=>parseInt(e,10)));if(e.some((e=>isNaN(e))))this[Os]=[0,0];else{1===e.length&&e.push(e[0]);this[Os]=e}}}class Xdc extends XFAObject{constructor(e){super(Hn,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Hn,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Hn,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Hn,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[zr](e,t){if(ConfigNamespace.hasOwnProperty(e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const Jn=_r.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(Jn,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(Jn,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(Jn,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(Jn,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(Jn,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(Jn,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(Jn,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(Jn,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(Jn,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(Jn,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(Jn,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(Jn,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[zr](e,t){if(ConnectionSetNamespace.hasOwnProperty(e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const Yn=_r.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(Yn,"data",e)}[mr](){return!0}}class Datasets extends XFAObject{constructor(e){super(Yn,"datasets",!0);this.data=null;this.Signature=null}[Nr](e){const t=e[kr];("data"===t&&e[Sr]===Yn||"Signature"===t&&e[Sr]===_r.signature.id)&&(this[t]=e);this[Hs](e)}}class DatasetsNamespace{static[zr](e,t){if(DatasetsNamespace.hasOwnProperty(e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const vn=_r.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(vn,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(vn,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(vn,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(vn,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(vn,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(vn,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(vn,"day")}}class DayNames extends XFAObject{constructor(e){super(vn,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(vn,"era")}}class EraNames extends XFAObject{constructor(e){super(vn,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(vn,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(vn,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(vn,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(vn,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(vn,"month")}}class MonthNames extends XFAObject{constructor(e){super(vn,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(vn,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(vn,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(vn,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(vn,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(vn,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(vn,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(vn,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(vn,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[zr](e,t){if(LocaleSetNamespace.hasOwnProperty(e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const Kn=_r.signature.id;class signature_Signature extends XFAObject{constructor(e){super(Kn,"signature",!0)}}class SignatureNamespace{static[zr](e,t){if(SignatureNamespace.hasOwnProperty(e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const Tn=_r.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(Tn,"stylesheet",!0)}}class StylesheetNamespace{static[zr](e,t){if(StylesheetNamespace.hasOwnProperty(e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const qn=_r.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(qn,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[Gr](e){const t=_r[e[kr]];return t&&e[Sr]===t.id}}class XdpNamespace{static[zr](e,t){if(XdpNamespace.hasOwnProperty(e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const On=_r.xhtml.id,Pn=Symbol(),Wn=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),jn=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,Math.min(parseInt(e)/100)).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,Math.min(parseInt(e)/100)).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),Xn=/\s+/g,Zn=/[\r\n]+/g,Vn=/\r\n?/g;function mapStyle(e,t,i){const a=Object.create(null);if(!e)return a;const s=Object.create(null);for(const[t,i]of e.split(";").map((e=>e.split(":",2)))){const e=jn.get(t);if(""===e)continue;let r=i;e&&(r="string"==typeof e?e:e(i,s));t.endsWith("scale")?a.transform=a.transform?`${a[t]} ${r}`:r:a[t.replaceAll(/-([a-zA-Z])/g,((e,t)=>t.toUpperCase()))]=r}a.fontFamily&&setFontFamily({typeface:a.fontFamily,weight:a.fontWeight||"normal",posture:a.fontStyle||"normal",size:s.fontSize||0},t,t[Cr].fontFinder,a);if(i&&a.verticalAlign&&"0px"!==a.verticalAlign&&a.fontSize){const e=.583,t=.333,i=getMeasurement(a.fontSize);a.fontSize=measureToString(i*e);a.verticalAlign=measureToString(Math.sign(getMeasurement(a.verticalAlign))*i*t)}i&&a.fontSize&&(a.fontSize=`calc(${a.fontSize} * var(--scale-factor))`);fixTextIndent(a);return a}const zn=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(On,t);this[Pn]=!1;this.style=e.style||""}[Ys](e){super[Ys](e);this.style=function checkStyle(e){return e.style?e.style.trim().split(/\s*;\s*/).filter((e=>!!e)).map((e=>e.split(/\s*:\s*/,2))).filter((([t,i])=>{"font-family"===t&&e[Cr].usedTypefaces.add(i);return Wn.has(t)})).map((e=>e.join(":"))).join(";"):""}(this)}[xs](){return!zn.has(this[kr])}[Mr](e,t=!1){if(t)this[Pn]=!0;else{e=e.replaceAll(Zn,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(Xn," "))}e&&(this[Os]+=e)}[Ur](e,t=!0){const i=Object.create(null),a={top:NaN,bottom:NaN,left:NaN,right:NaN};let s=null;for(const[e,t]of this.style.split(";").map((e=>e.split(":",2))))switch(e){case"font-family":i.typeface=stripQuotes(t);break;case"font-size":i.size=getMeasurement(t);break;case"font-weight":i.weight=t;break;case"font-style":i.posture=t;break;case"letter-spacing":i.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map((e=>getMeasurement(e)));switch(e.length){case 1:a.top=a.bottom=a.left=a.right=e[0];break;case 2:a.top=a.bottom=e[0];a.left=a.right=e[1];break;case 3:a.top=e[0];a.bottom=e[2];a.left=a.right=e[1];break;case 4:a.top=e[0];a.left=e[1];a.bottom=e[2];a.right=e[3]}break;case"margin-top":a.top=getMeasurement(t);break;case"margin-bottom":a.bottom=getMeasurement(t);break;case"margin-left":a.left=getMeasurement(t);break;case"margin-right":a.right=getMeasurement(t);break;case"line-height":s=getMeasurement(t)}e.pushData(i,a,s);if(this[Os])e.addString(this[Os]);else for(const t of this[rr]())"#text"!==t[kr]?t[Ur](e):e.addString(t[Os]);t&&e.popFont()}[jr](e){const t=[];this[Xs]={children:t};this[Js]({});if(0===t.length&&!this[Os])return HTMLResult.EMPTY;let i;i=this[Pn]?this[Os]?this[Os].replaceAll(Vn,"\n"):void 0:this[Os]||void 0;return HTMLResult.success({name:this[kr],attributes:{href:this.href,style:mapStyle(this.style,this,this[Pn])},children:t,value:i})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Ur](e){e.pushFont({weight:"bold"});super[Ur](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[jr](e){const t=super[jr](e),{html:i}=t;if(!i)return HTMLResult.EMPTY;i.name="div";i.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[Pr](){return"\n"}[Ur](e){e.addString("\n")}[jr](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[jr](e){const t=[];this[Xs]={children:t};this[Js]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[Os]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Ur](e){e.pushFont({posture:"italic"});super[Ur](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Ur](e){super[Ur](e,!1);e.addString("\n");e.addPara();e.popFont()}[Pr](){return this[Ir]()[rr]().at(-1)===this?super[Pr]():super[Pr]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[zr](e,t){if(XhtmlNamespace.hasOwnProperty(e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const _n={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[zr](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[lr]=e}[Nr](e){this.element=e;return!0}[Zs](){super[Zs]();if(this.element.template instanceof Template){this[lr].set(Jr,this.element);this.element.template[Yr](this[lr]);this.element.template[lr]=this[lr]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[Nr](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(_r).map((({id:e})=>e)));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:i,namespace:a,prefixes:s}){const r=null!==a;if(r){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(a)}s&&this._addNamespacePrefix(s);if(i.hasOwnProperty(Rr)){const e=_n.datasets,t=i[Rr];let a=null;for(const[i,s]of Object.entries(t)){if(this._getNamespaceToUse(i)===e){a={xfa:s};break}}a?i[Rr]=a:delete i[Rr]}const n=this._getNamespaceToUse(e),g=n?.[zr](t,i)||new Empty;g[mr]()&&this._nsAgnosticLevel++;(r||s||g[mr]())&&(g[Ks]={hasNamespace:r,prefixes:s,nsAgnostic:g[mr]()});return g}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[i,{check:a}]of Object.entries(_r))if(a(e)){t=_n[i];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:i}of e){const e=this._searchNamespace(i);let a=this._namespacePrefixes.get(t);if(!a){a=[];this._namespacePrefixes.set(t,a)}a.push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:i,nsAgnostic:a}=e;t&&(this._currentNamespace=this._namespaceStack.pop());i&&i.forEach((({prefix:e})=>{this._namespacePrefixes.get(e).pop()}));a&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=ys;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===ys){this._current[Zs]();return this._current.element}}onText(e){e=e.replace(this._nbsps,(e=>e.slice(1)+" "));this._richText||this._current[xs]()?this._current[Mr](e,this._richText):this._whiteRegex.test(e)||this._current[Mr](e.trim())}onCdata(e){this._current[Mr](e)}_mkAttributes(e,t){let i=null,a=null;const s=Object.create({});for(const{name:r,value:n}of e)if("xmlns"===r)i?warn(`XFA - multiple namespace definition in <${t}>`):i=n;else if(r.startsWith("xmlns:")){const e=r.substring(6);a||(a=[]);a.push({prefix:e,value:n})}else{const e=r.indexOf(":");if(-1===e)s[r]=n;else{let t=s[Rr];t||(t=s[Rr]=Object.create(null));const[i,a]=[r.slice(0,e),r.slice(e+1)];(t[i]||=Object.create(null))[a]=n}}return[i,a,s]}_getNameAndPrefix(e,t){const i=e.indexOf(":");return-1===i?[e,null]:[e.substring(i+1),t?"":e.substring(0,i)]}onBeginElement(e,t,i){const[a,s,r]=this._mkAttributes(t,e),[n,g]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),o=this._builder.build({nsPrefix:g,name:n,attributes:r,namespace:a,prefixes:s});o[Cr]=this._globalData;if(i){o[Zs]();this._current[Nr](o)&&o[Kr](this._ids);o[Ys](this._builder)}else{this._stack.push(this._current);this._current=o}}onEndElement(e){const t=this._current;if(t[ur]()&&"string"==typeof t[Os]){const e=new XFAParser;e._globalData=this._globalData;const i=e.parse(t[Os]);t[Os]=null;t[Nr](i)}t[Zs]();this._current=this._stack.pop();this._current[Nr](t)&&t[Kr](this._ids);t[Ys](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[Cr].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return this.root&&this.form}_createPagesHelper(){const e=this.form[Wr]();return new Promise(((t,i)=>{const nextIteration=()=>{try{const i=e.next();i.done?t(i.value):setTimeout(nextIteration,0)}catch(e){i(e)}};setTimeout(nextIteration,0)}))}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map((e=>{const{width:t,height:i}=e.attributes.style;return[0,0,parseInt(t),parseInt(i)]}))}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[Cr].images=e}setFonts(e){this.form[Cr].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[Cr].usedTypefaces){e=stripQuotes(e);this.form[Cr].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[Cr].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[kr])){const e=XhtmlNamespace.body({});e[Hs](t);t=e}const i=t[jr]();if(!i.success)return null;const{html:a}=i,{attributes:s}=a;if(s){s.class&&(s.class=s.class.filter((e=>!e.startsWith("xfa"))));s.dir="auto"}return{html:a,str:t[Pr]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments")]).then((([t,i,a,s,r])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:i,structTreeRoot:a,baseUrl:s,attachments:r})),(e=>{warn(`createGlobals: "${e}".`);return null}))}static async create(e,t,i,a,s,r,n){const g=s?await this._getPageIndex(e,t,i.pdfManager):null;return i.pdfManager.ensure(this,"_create",[e,t,i,a,s,r,g,n])}static _create(e,t,i,a,s=!1,r=null,n=null,g=null){const o=e.fetchIfRef(t);if(!(o instanceof Dict))return;const{acroForm:c,pdfManager:C}=i,h=t instanceof Ref?t.toString():`annot_${a.createObjId()}`;let l=o.get("Subtype");l=l instanceof Name?l.name:null;const Q={xref:e,ref:t,dict:o,subtype:l,id:h,annotationGlobals:i,collectFields:s,orphanFields:r,needAppearances:!s&&!0===c.get("NeedAppearances"),pageIndex:n,evaluatorOptions:C.evaluatorOptions,pageRef:g};switch(l){case"Link":return new LinkAnnotation(Q);case"Text":return new TextAnnotation(Q);case"Widget":let e=getInheritableProperty({dict:o,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(Q);case"Btn":return new ButtonWidgetAnnotation(Q);case"Ch":return new ChoiceWidgetAnnotation(Q);case"Sig":return new SignatureWidgetAnnotation(Q)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(Q);case"Popup":return new PopupAnnotation(Q);case"FreeText":return new FreeTextAnnotation(Q);case"Line":return new LineAnnotation(Q);case"Square":return new SquareAnnotation(Q);case"Circle":return new CircleAnnotation(Q);case"PolyLine":return new PolylineAnnotation(Q);case"Polygon":return new PolygonAnnotation(Q);case"Caret":return new CaretAnnotation(Q);case"Ink":return new InkAnnotation(Q);case"Highlight":return new HighlightAnnotation(Q);case"Underline":return new UnderlineAnnotation(Q);case"Squiggly":return new SquigglyAnnotation(Q);case"StrikeOut":return new StrikeOutAnnotation(Q);case"Stamp":return new StampAnnotation(Q);case"FileAttachment":return new FileAttachmentAnnotation(Q);default:s||warn(l?`Unimplemented annotation type "${l}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(Q)}}static async _getPageIndex(e,t,i){try{const a=await e.fetchIfRefAsync(t);if(!(a instanceof Dict))return-1;const s=a.getRaw("P");if(s instanceof Ref)try{return await i.ensureCatalog("getPageIndex",[s])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(a.has("Kids"))return-1;const r=await i.ensureDoc("numPages");for(let e=0;ee/255))}function getQuadPoints(e,t){const i=e.getArray("QuadPoints");if(!isNumberArray(i,null)||0===i.length||i.length%8>0)return null;const a=new Float32Array(i.length);for(let e=0,s=i.length;et[2]||Et[3]))return null;a.set([l,u,Q,u,l,E,Q,E],e)}return a}function getTransformMatrix(e,t,i){const[a,s,r,n]=Util.getAxialAlignedBoundingBox(t,i);if(a===r||s===n)return[1,0,0,1,e[0],e[1]];const g=(e[2]-e[0])/(r-a),o=(e[3]-e[1])/(n-s);return[g,0,0,o,e[0]-a*g,e[1]-s*o]}class Annotation{constructor(e){const{dict:t,xref:i,annotationGlobals:a,ref:s,orphanFields:r}=e,n=r?.get(s);n&&t.set("Parent",n);this.setTitle(t.get("T"));this.setContents(t.get("Contents"));this.setModificationDate(t.get("M"));this.setFlags(t.get("F"));this.setRectangle(t.getArray("Rect"));this.setColor(t.getArray("C"));this.setBorderStyle(t);this.setAppearance(t);this.setOptionalContent(t);const g=t.get("MK");this.setBorderAndBackgroundColors(g);this.setRotation(g,t);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const o=!!(this.flags&eA),c=!!(this.flags&tA);this.data={annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:e.subtype,hasOwnCanvas:!1,noRotate:!!(this.flags&$),noHTML:o&&c,isEditable:!1,structParent:-1};if(a.structTreeRoot){let i=t.get("StructParent");this.data.structParent=i=Number.isInteger(i)&&i>=0?i:-1;a.structTreeRoot.addAnnotationIdToPage(e.pageRef,i)}if(e.collectFields){const a=t.get("Kids");if(Array.isArray(a)){const e=[];for(const t of a)t instanceof Ref&&e.push(t.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(i,t,dA);this.data.fieldName=this._constructFieldName(t);this.data.pageIndex=e.pageIndex}const C=t.get("IT");C instanceof Name&&(this.data.it=C.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:i}=this;if(void 0===e){if(void 0===t)return;return t?i&~_:i&~z|_}if(e){i|=_;return t?i&~AA|z:i&~z|AA}i&=~(z|AA);return t?i&~_:i|_}_isViewable(e){return!this._hasFlag(e,V)&&!this._hasFlag(e,AA)}_isPrintable(e){return this._hasFlag(e,_)&&!this._hasFlag(e,z)&&!this._hasFlag(e,V)}mustBeViewed(e,t){const i=e?.get(this.data.id)?.noView;return void 0!==i?!i:this.viewable&&!this._hasFlag(this.flags,z)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:i}=e,a=getInheritableProperty({dict:t,key:"DA"})||i.acroForm.get("DA");this._defaultAppearance="string"==typeof a?a:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&V&&"Annotation"!==this.constructor.name&&(this.flags^=V)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const i=e[t];if(i instanceof Name)switch(i.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=i.name;continue}warn(`Ignoring invalid lineEnding: ${i}`)}}setRotation(e,t){this.rotation=0;let i=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(i)&&0!==i){i%=360;i<0&&(i+=360);i%90==0&&(this.rotation=i)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t)&&t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const i=t.get("N");if(i instanceof BaseStream){this.appearance=i;return}if(!(i instanceof Dict))return;const a=e.get("AS");if(!(a instanceof Name&&i.has(a.name)))return;const s=i.get(a.name);s instanceof BaseStream&&(this.appearance=s)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}loadResources(e,t){return t.dict.getAsync("Resources").then((t=>{if(!t)return;return new ObjectLoader(t,e,t.xref).load().then((function(){return t}))}))}async getOperatorList(e,t,a,s){const{hasOwnCanvas:r,id:n,rect:g}=this.data;let c=this.appearance;const C=!!(r&&a&o);if(C&&(g[0]===g[2]||g[1]===g[3])){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!c){if(!C)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};c=new StringStream("");c.dict=new Dict}const h=c.dict,l=await this.loadResources(["ExtGState","ColorSpace","Pattern","Shading","XObject","Font"],c),Q=lookupRect(h.getArray("BBox"),[0,0,1,1]),E=lookupMatrix(h.getArray("Matrix"),i),u=getTransformMatrix(g,Q,E),d=new OperatorList;let f;this.oc&&(f=await e.parseMarkedContentProps(this.oc,null));void 0!==f&&d.addOp(Ye,["OC",f]);d.addOp(je,[n,g,u,E,C]);await e.getOperatorList({stream:c,task:t,resources:l,operatorList:d,fallbackFontDict:this._fallbackFontDict});d.addOp(Xe,[]);void 0!==f&&d.addOp(ve,[]);this.reset();return{opList:d,separateForm:!1,separateCanvas:C}}async save(e,t,i,a){return null}get hasTextContent(){return!1}async extractTextContent(e,t,i){if(!this.appearance)return;const a=await this.loadResources(["ExtGState","Font","Properties","XObject"],this.appearance),s=[],r=[];let n=null;const g={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){n||=t.transform.slice(-2);r.push(t.str);if(t.hasEOL){s.push(r.join("").trimEnd());r.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:a,includeMarkedContent:!0,keepWhiteSpace:!0,sink:g,viewBox:i});this.reset();r.length&&s.push(r.join("").trimEnd());if(s.length>1||s[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),i=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(n,t,i);this.data.textContent=s}}_transformPoint(e,t,i){const{rect:a}=this.data;t||=[0,0,1,1];i||=[1,0,0,1,0,0];const s=getTransformMatrix(a,t,i);s[4]-=a[0];s[5]-=a[1];e=Util.applyTransform(e,s);return Util.applyTransform(e,i)}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let i=e;const a=new RefSet;e.objId&&a.put(e.objId);for(;i.has("Parent");){i=i.get("Parent");if(!(i instanceof Dict)||i.objId&&a.has(i.objId))break;i.objId&&a.put(i.objId);i.has("T")&&t.unshift(stringToPDFString(i.get("T")))}return t.join(".")}}class AnnotationBorderStyle{constructor(){this.width=1;this.rawWidth=1;this.style=lA;this.dashArray=[3];this.horizontalCornerRadius=0;this.verticalCornerRadius=0}setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const i=(t[2]-t[0])/2,a=(t[3]-t[1])/2;if(i>0&&a>0&&(e>i||e>a)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=lA;break;case"D":this.style=BA;break;case"B":this.style=QA;break;case"I":this.style=EA;break;case"U":this.style=uA}}setDashArray(e,t=!1){if(Array.isArray(e)){let i=!0,a=!0;for(const t of e){if(!(+t>=0)){i=!1;break}t>0&&(a=!1)}if(0===e.length||i&&!a){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const i=t.get("RT");this.data.replyType=i instanceof Name?i.name:Z}let i=null;if(this.data.replyType===X){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;i=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;i=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=i instanceof Ref?i.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:t,strokeColor:i,fillColor:a,blendMode:s,strokeAlpha:r,fillAlpha:n,pointsCallback:g}){let o=Number.MAX_VALUE,c=Number.MAX_VALUE,C=Number.MIN_VALUE,h=Number.MIN_VALUE;const l=["q"];t&&l.push(t);i&&l.push(`${i[0]} ${i[1]} ${i[2]} RG`);a&&l.push(`${a[0]} ${a[1]} ${a[2]} rg`);const Q=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=Q.length;e"string"==typeof e)).map((e=>stringToPDFString(e))):e instanceof Name?stringToPDFString(e.name):"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,AA)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(0===t)return i;return getRotationMatrix(t,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1])}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const i=this.data.rect[2]-this.data.rect[0],a=this.data.rect[3]-this.data.rect[1],s=0===t||180===t?`0 0 ${i} ${a} re`:`0 0 ${a} ${i} re`;let r="";this.backgroundColor&&(r=`${getPdfColor(this.backgroundColor,!0)} ${s} f `);if(this.borderColor){r+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${s} S `}return r}async getOperatorList(e,t,i,a){if(i&h&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,i,a);const s=await this._getAppearance(e,t,i,a);if(this.appearance&&null===s)return super.getOperatorList(e,t,i,a);const r=new OperatorList;if(!this._defaultAppearance||null===s)return{opList:r,separateForm:!1,separateCanvas:!1};const n=!!(this.data.hasOwnCanvas&&i&o),g=[0,0,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1]],c=getTransformMatrix(this.data.rect,g,[1,0,0,1,0,0]);let C;this.oc&&(C=await e.parseMarkedContentProps(this.oc,null));void 0!==C&&r.addOp(Ye,["OC",C]);r.addOp(je,[this.data.id,this.data.rect,c,this.getRotationMatrix(a),n]);const l=new StringStream(s);await e.getOperatorList({stream:l,task:t,resources:this._fieldResources.mergedResources,operatorList:r});r.addOp(Xe,[]);void 0!==C&&r.addOp(ve,[]);return{opList:r,separateForm:!1,separateCanvas:n}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);this.borderColor&&t.set("BC",getPdfColorArray(this.borderColor));this.backgroundColor&&t.set("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,i,a){const{dict:s,ref:r}=function getParentToUpdate(e,t,i){const a=new RefSet,s=e,r={dict:null,ref:null};for(;e instanceof Dict&&!a.has(t);){a.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return r;e=i.fetch(t)}if(e instanceof Dict&&e!==s){r.dict=e;r.ref=t}return r}(e,this.ref,i);if(s){if(!a.has(r)){const e=s.clone();e.set("V",t);a.put(r,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,s){const r=a?.get(this.data.id),n=this._buildFlags(r?.noView,r?.noPrint);let g=r?.value,o=r?.rotation;if(g===this.data.fieldValue||void 0===g){if(!this._hasValueFromXFA&&void 0===o&&void 0===n)return;g||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(g)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(g,this.data.fieldValue)&&void 0===n)return;void 0===o&&(o=this.rotation);let c=null;if(!this._needAppearances){c=await this._getAppearance(e,t,C,a);if(null===c&&void 0===n)return}let h=!1;if(c?.needAppearances){h=!0;c=null}const{xref:l}=e,Q=l.fetchIfRef(this.ref);if(!(Q instanceof Dict))return;const E=new Dict(l);for(const e of Q.getKeys())"AP"!==e&&E.set(e,Q.getRaw(e));if(void 0!==n){E.set("F",n);if(null===c&&!h){const e=Q.getRaw("AP");e&&E.set("AP",e)}}const u={path:this.data.fieldName,value:g},d=this.setValue(E,Array.isArray(g)?g.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(g),l,s);this.amendSavedDict(a,d||E);const f=this._getMKDict(o);f&&E.set("MK",f);s.put(this.ref,{data:E,xfa:u,needAppearances:h});if(null!==c){const e=l.getNewTemporaryRef(),t=new Dict(l);E.set("AP",t);t.set("N",e);const r=this._getSaveFieldResources(l),n=new StringStream(c),g=n.dict=new Dict(l);g.set("Subtype",Name.get("Form"));g.set("Resources",r);g.set("BBox",[0,0,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1]]);const o=this.getRotationMatrix(a);o!==i&&g.set("Matrix",o);s.put(e,{data:n,xfa:null,needAppearances:!1})}E.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,i,a){if(this.hasFieldFlag(rA))return null;const s=a?.get(this.data.id);let r,g;if(s){r=s.formattedValue||s.value;g=s.rotation}if(void 0===g&&void 0===r&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const o=this.getBorderAndBackgroundAppearances(a);if(void 0===r){r=this.data.fieldValue;if(!r)return`/Tx BMC q ${o}Q EMC`}Array.isArray(r)&&1===r.length&&(r=r[0]);assert("string"==typeof r,"Expected `value` to be a string.");r=r.trimEnd();if(this.data.combo){const e=this.data.options.find((({exportValue:e})=>r===e));r=e?.displayValue||r}if(""===r)return`/Tx BMC q ${o}Q EMC`;void 0===g&&(g=this.rotation);let c,h=-1;if(this.data.multiLine){c=r.split(/\r\n?|\n/).map((e=>e.normalize("NFC")));h=c.length}else c=[r.replace(/\r\n?|\n/,"").normalize("NFC")];let l=this.data.rect[3]-this.data.rect[1],Q=this.data.rect[2]-this.data.rect[0];90!==g&&270!==g||([Q,l]=[l,Q]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let E,u,d,f=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const p=[];let m=!1;for(const e of c){const t=f.encodeString(e);t.length>1&&(m=!0);p.push(t.join(""))}if(m&&i&C)return{needAppearances:!0};if(m&&this._isOffscreenCanvasSupported){const i=this.data.comb?"monospace":"sans-serif",a=new FakeUnicodeFont(e.xref,i),s=a.createFontResources(c.join("")),n=s.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const t of n.getKeys())e.set(t,n.getRaw(t))}else this._fieldResources.mergedResources.set("Font",n);const g=a.fontName.name;f=await WidgetAnnotation._getFontData(e,t,{fontName:g,fontSize:0},s);for(let e=0,t=p.length;e2)return`/Tx BMC q ${o}BT `+E+` 1 0 0 1 ${numberToString(2)} ${numberToString(b)} Tm (${escapeString(p[0])}) Tj ET Q EMC`;return`/Tx BMC q ${o}BT `+E+` 1 0 0 1 0 0 Tm ${this._renderText(p[0],f,u,Q,D,{shift:0},2,b)} ET Q EMC`}static async _getFontData(e,t,i,a){const s=new OperatorList,r={font:null,clone(){return this}},{fontName:n,fontSize:g}=i;await e.handleSetFont(a,[n&&Name.get(n),g],null,s,t,r,null);return r.font}_getTextWidth(e,t){return t.charsToGlyphs(e).reduce(((e,t)=>e+t.width),0)/1e3}_computeFontSize(e,t,i,a,r){let{fontSize:n}=this.data.defaultAppearanceData,g=(n||12)*s,o=Math.round(e/g);if(!n){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===r){const r=this._getTextWidth(i,a);n=roundWithTwoDigits(Math.min(e/s,t/r));o=1}else{const c=i.split(/\r\n?|\n/),C=[];for(const e of c){const t=a.encodeString(e).join(""),i=a.charsToGlyphs(t),s=a.getCharPositions(t);C.push({line:t,glyphs:i,positions:s})}const isTooBig=i=>{let s=0;for(const r of C){s+=this._splitLine(null,a,i,t,r).length*i;if(s>e)return!0}return!1};o=Math.max(o,r);for(;;){g=e/o;n=roundWithTwoDigits(g/s);if(!isTooBig(n))break;o++}}const{fontName:c,fontColor:C}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:i}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(i,!0)}`}({fontSize:n,fontName:c,fontColor:C})}return[this._defaultAppearance,n,e/o]}_renderText(e,t,i,a,s,r,n,g){let o;if(1===s){o=(a-this._getTextWidth(e,t)*i)/2}else if(2===s){o=a-this._getTextWidth(e,t)*i-n}else o=n;const c=numberToString(o-r.shift);r.shift=o;return`${c} ${g=numberToString(g)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:i,acroFormResources:a}=this._fieldResources,s=this.data.defaultAppearanceData?.fontName;if(!s)return t||Dict.empty;for(const e of[t,i])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(s))return e}if(a instanceof Dict){const i=a.get("Font");if(i instanceof Dict&&i.has(s)){const a=new Dict(e);a.set(s,i.getRaw(s));const r=new Dict(e);r.set("Font",a);return Dict.merge({xref:e,dictArray:[r,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=z;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let i=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(i)||i<0||i>2)&&(i=null);this.data.textAlignment=i;let a=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(a)||a<0)&&(a=0);this.data.maxLen=a;this.data.multiLine=this.hasFieldFlag(sA);this.data.comb=this.hasFieldFlag(hA)&&!this.hasFieldFlag(sA)&&!this.hasFieldFlag(rA)&&!this.hasFieldFlag(IA)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(CA)}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,i,a,s,r,n,g,o,c,C){const h=s/this.data.maxLen,l=this.getBorderAndBackgroundAppearances(C),Q=[],E=t.getCharPositions(i);for(const[e,t]of E)Q.push(`(${escapeString(i.substring(e,t))}) Tj`);const u=Q.join(` ${numberToString(h)} 0 Td `);return`/Tx BMC q ${l}BT `+e+` 1 0 0 1 ${numberToString(n)} ${numberToString(g+o)} Tm ${u} ET Q EMC`}_getMultilineAppearance(e,t,i,a,s,r,n,g,o,c,C,h){const l=[],Q=s-2*g,E={shift:0};for(let e=0,r=t.length;ea){o.push(e.substring(l,i));l=i;Q=u;c=-1;h=-1}else{Q+=u;c=i;C=s;h=t}else if(Q+u>a)if(-1!==c){o.push(e.substring(l,C));l=C;t=h+1;c=-1;Q=0}else{o.push(e.substring(l,i));l=i;Q=u}else Q+=u}lt?`\\${t}`:"\\s+"));new RegExp(`^\\s*${r}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.hasFieldFlag(rA),charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;this.data.checkBox=!this.hasFieldFlag(nA)&&!this.hasFieldFlag(gA);this.data.radioButton=this.hasFieldFlag(nA)&&!this.hasFieldFlag(gA);this.data.pushButton=this.hasFieldFlag(gA);this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,s){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,s);let r=null,n=null;if(s){const e=s.get(this.data.id);r=e?e.value:null;n=e?e.rotation:null}if(null===r&&this.appearance)return super.getOperatorList(e,t,a,s);null==r&&(r=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const g=r?this.checkedAppearance:this.uncheckedAppearance;if(g){const r=this.appearance,o=lookupMatrix(g.dict.getArray("Matrix"),i);n&&g.dict.set("Matrix",this.getRotationMatrix(s));this.appearance=g;const c=super.getOperatorList(e,t,a,s);this.appearance=r;g.dict.set("Matrix",o);return c}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,i,a){this.data.checkBox?this._saveCheckbox(e,t,i,a):this.data.radioButton&&this._saveRadioButton(e,t,i,a)}async _saveCheckbox(e,t,i,a){if(!i)return;const s=i.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let n=s?.rotation,g=s?.value;if(void 0===n&&void 0===r){if(void 0===g)return;if(this.data.fieldValue===this.data.exportValue===g)return}let o=e.xref.fetchIfRef(this.ref);if(!(o instanceof Dict))return;o=o.clone();void 0===n&&(n=this.rotation);void 0===g&&(g=this.data.fieldValue===this.data.exportValue);const c={path:this.data.fieldName,value:g?this.data.exportValue:""},C=Name.get(g?this.data.exportValue:"Off");this.setValue(o,C,e.xref,a);o.set("AS",C);o.set("M",`D:${getModificationDate()}`);void 0!==r&&o.set("F",r);const h=this._getMKDict(n);h&&o.set("MK",h);a.put(this.ref,{data:o,xfa:c,needAppearances:!1})}async _saveRadioButton(e,t,i,a){if(!i)return;const s=i.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let n=s?.rotation,g=s?.value;if(void 0===n&&void 0===r){if(void 0===g)return;if(this.data.fieldValue===this.data.buttonValue===g)return}let o=e.xref.fetchIfRef(this.ref);if(!(o instanceof Dict))return;o=o.clone();void 0===g&&(g=this.data.fieldValue===this.data.buttonValue);void 0===n&&(n=this.rotation);const c={path:this.data.fieldName,value:g?this.data.buttonValue:""},C=Name.get(g?this.data.buttonValue:"Off");g&&this.setValue(o,C,e.xref,a);o.set("AS",C);o.set("M",`D:${getModificationDate()}`);void 0!==r&&o.set("F",r);const h=this._getMKDict(n);h&&o.set("MK",h);a.put(this.ref,{data:o,xfa:c,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const i=this.data.rect[2]-this.data.rect[0],a=this.data.rect[3]-this.data.rect[1],s=[0,0,i,a],r=.8*Math.min(i,a);let n,g;if("check"===t){n={width:.755*r,height:.705*r};g="3"}else if("disc"===t){n={width:.791*r,height:.705*r};g="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const o=`q BT /PdfJsZaDb ${r} Tf 0 g ${numberToString((i-n.width)/2)} ${numberToString((a-n.height)/2)} Td (${g}) Tj ET Q`,c=new Dict(e.xref);c.set("FormType",1);c.set("Subtype",Name.get("Form"));c.set("Type",Name.get("XObject"));c.set("BBox",s);c.set("Matrix",[1,0,0,1,0,0]);c.set("Length",o.length);const C=new Dict(e.xref),h=new Dict(e.xref);h.set("PdfJsZaDb",this.fallbackFontDict);C.set("Font",h);c.set("Resources",C);this.checkedAppearance=new StringStream(o);this.checkedAppearance.dict=c;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const i=t.get("N");if(!(i instanceof Dict))return;const a=this._decodeFormValue(e.dict.get("AS"));"string"==typeof a&&(this.data.fieldValue=a);const s=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",r=i.getKeys();if(0===r.length)r.push("Off",s);else if(1===r.length)"Off"===r[0]?r.push(s):r.unshift("Off");else if(r.includes(s)){r.length=0;r.push("Off",s)}else{const e=r.find((e=>"Off"!==e));r.length=0;r.push("Off",e)}r.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=r[1];const n=i.get(this.data.exportValue);this.checkedAppearance=n instanceof BaseStream?n:null;const g=i.get("Off");this.uncheckedAppearance=g instanceof BaseStream?g:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const i=t.get("V");i instanceof Name&&(this.data.fieldValue=this._decodeFormValue(i))}const i=e.dict.get("AP");if(!(i instanceof Dict))return;const a=i.get("N");if(!(a instanceof Dict))return;for(const e of a.getKeys())if("Off"!==e){this.data.buttonValue=this._decodeFormValue(e);break}const s=a.get(this.data.buttonValue);this.checkedAppearance=s instanceof BaseStream?s:null;const r=a.get("Off");this.uncheckedAppearance=r instanceof BaseStream?r:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:i}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:i.baseUrl,docAttachments:i.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.set("BaseFont",Name.get("ZapfDingbats"));e.set("Type",Name.get("FallbackType"));e.set("Subtype",Name.get("FallbackType"));e.set("Encoding",Name.get("ZapfDingbatsEncoding"));return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const a=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(a))for(let e=0,t=a.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map((e=>({exportValue:e,displayValue:e}))));this.data.combo=this.hasFieldFlag(oA);this.data.multiSelect=this.hasFieldFlag(cA);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let i=e?.get(this.data.id)?.value;Array.isArray(i)||(i=[i]);const a=[],{options:s}=this.data;for(let e=0,t=0,r=s.length;ei){i=a;t=e}}[Q,E]=this._computeFontSize(e,c-4,t,l,-1)}const u=E*s,d=(u-E)/2,f=Math.floor(o/u);let p=0;if(h.length>0){const e=Math.min(...h),t=Math.max(...h);p=Math.max(0,t-f+1);p>e&&(p=e)}const m=Math.min(p+f+1,C),y=["/Tx BMC q",`1 1 ${c} ${o} re W n`];if(h.length){y.push("0.600006 0.756866 0.854904 rg");for(const e of h)p<=e&&ee.trimEnd()));const{coords:e,bbox:t,matrix:i}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,a);this.data.textPosition=this._transformPoint(e,t,i)}if(this._isOffscreenCanvasSupported){const s=e.dict.get("CA"),r=new FakeUnicodeFont(i,"sans-serif");this.appearance=r.createAppearance(this._contents.str,this.rectangle,this.rotation,a,t,s);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:i,ap:a}){const{color:s,fontSize:r,oldAnnotation:n,rect:g,rotation:o,user:c,value:C}=e,h=n||new Dict(t);h.set("Type",Name.get("Annot"));h.set("Subtype",Name.get("FreeText"));if(n){h.set("M",`D:${getModificationDate()}`);h.delete("RC")}else h.set("CreationDate",`D:${getModificationDate()}`);h.set("Rect",g);const l=`/Helv ${r} Tf ${getPdfColor(s,!0)}`;h.set("DA",l);h.set("Contents",stringToAsciiOrUTF16BE(C));h.set("F",4);h.set("Border",[0,0,0]);h.set("Rotate",o);c&&h.set("T",stringToAsciiOrUTF16BE(c));if(i||a){const e=new Dict(t);h.set("AP",e);i?e.set("N",i):e.set("N",a)}return h}static async createNewAppearanceStream(e,t,i){const{baseFontRef:a,evaluator:r,task:n}=i,{color:g,fontSize:o,rect:c,rotation:C,value:h}=e,l=new Dict(t),Q=new Dict(t);if(a)Q.set("Helv",a);else{const e=new Dict(t);e.set("BaseFont",Name.get("Helvetica"));e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type1"));e.set("Encoding",Name.get("WinAnsiEncoding"));Q.set("Helv",e)}l.set("Font",Q);const E=await WidgetAnnotation._getFontData(r,n,{fontName:"Helv",fontSize:o},l),[u,d,f,p]=c;let m=f-u,y=p-d;C%180!=0&&([m,y]=[y,m]);const w=h.split("\n"),D=o/1e3;let b=-1/0;const F=[];for(let e of w){const t=E.encodeString(e);if(t.length>1)return null;e=t.join("");F.push(e);let i=0;const a=E.charsToGlyphs(e);for(const e of a)i+=e.width*D;b=Math.max(b,i)}let S=1;b>m&&(S=m/b);let k=1;const R=s*o,N=1*o,G=R*w.length;G>y&&(k=y/G);const M=o*Math.min(S,k);let U,x,L;switch(C){case 0:L=[1,0,0,1];x=[c[0],c[1],m,y];U=[c[0],c[3]-N];break;case 90:L=[0,1,-1,0];x=[c[1],-c[2],m,y];U=[c[1],-c[0]-N];break;case 180:L=[-1,0,0,-1];x=[-c[2],-c[3],m,y];U=[-c[2],-c[1]-N];break;case 270:L=[0,-1,1,0];x=[-c[3],c[0],m,y];U=[-c[3],c[2]-N]}const H=["q",`${L.join(" ")} 0 0 cm`,`${x.join(" ")} re W n`,"BT",`${getPdfColor(g,!0)}`,`0 Tc /Helv ${numberToString(M)} Tf`];H.push(`${U.join(" ")} Td (${escapeString(F[0])}) Tj`);const J=numberToString(R);for(let e=1,t=F.length;e{e.push(`${a[0]} ${a[1]} m`,`${a[2]} ${a[3]} l`,"S");return[t[0]-o,t[2]+o,t[7]-o,t[3]+o]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=M;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),s=getRgbColor(t.getArray("IC"),null),r=s?getPdfColorArray(s):null,n=r?a:null;if(0===this.borderStyle.width&&!r)return;this._setDefaultAppearance({xref:i,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:r,strokeAlpha:a,fillAlpha:n,pointsCallback:(e,t)=>{const i=t[4]+this.borderStyle.width/2,a=t[5]+this.borderStyle.width/2,s=t[6]-t[4]-this.borderStyle.width,n=t[3]-t[7]-this.borderStyle.width;e.push(`${i} ${a} ${s} ${n} re`);r?e.push("B"):e.push("S");return[t[0],t[2],t[7],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=U;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),s=getRgbColor(t.getArray("IC"),null),r=s?getPdfColorArray(s):null,n=r?a:null;if(0===this.borderStyle.width&&!r)return;const g=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:i,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:r,strokeAlpha:a,fillAlpha:n,pointsCallback:(e,t)=>{const i=t[0]+this.borderStyle.width/2,a=t[1]-this.borderStyle.width/2,s=t[6]-this.borderStyle.width/2,n=t[7]+this.borderStyle.width/2,o=i+(s-i)/2,c=a+(n-a)/2,C=(s-i)/2*g,h=(n-a)/2*g;e.push(`${o} ${n} m`,`${o+C} ${n} ${s} ${c+h} ${s} ${c} c`,`${s} ${c-h} ${o+C} ${a} ${o} ${a} c`,`${o-C} ${a} ${i} ${c-h} ${i} ${c} c`,`${i} ${c+h} ${o-C} ${n} ${o} ${n} c`,"h");r?e.push("B"):e.push("S");return[t[0],t[2],t[7],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=L;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(t.getArray("LE"));this.data.lineEndings=this.lineEndings}const a=t.getArray("Vertices");if(!isNumberArray(a,null))return;const s=this.data.vertices=Float32Array.from(a);if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),r=this.borderStyle.width||1,n=2*r,g=[1/0,1/0,-1/0,-1/0];for(let e=0,t=s.length;e{for(let t=0,i=s.length;t{for(const t of this.data.inkLists){for(let i=0,a=t.length;ie/255)));Q.set("CA",n);const u=new Dict(t);Q.set("AP",u);i?u.set("N",i):u.set("N",a);return Q}static async createNewAppearanceStream(e,t,i){if(e.outlines)return this.createNewAppearanceStreamForHighlight(e,t,i);const{color:a,rect:s,paths:r,thickness:n,opacity:g}=e,o=[`${n} w 1 J 1 j`,`${getPdfColor(a,!1)}`];1!==g&&o.push("/R0 gs");for(const e of r.lines){o.push(`${numberToString(e[4])} ${numberToString(e[5])} m`);for(let t=6,i=e.length;t{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}static createNewDict(e,t,{apRef:i,ap:a}){const{color:s,oldAnnotation:r,opacity:n,rect:g,rotation:o,user:c,quadPoints:C}=e,h=r||new Dict(t);h.set("Type",Name.get("Annot"));h.set("Subtype",Name.get("Highlight"));h.set(r?"M":"CreationDate",`D:${getModificationDate()}`);h.set("CreationDate",`D:${getModificationDate()}`);h.set("Rect",g);h.set("F",4);h.set("Border",[0,0,0]);h.set("Rotate",o);h.set("QuadPoints",C);h.set("C",Array.from(s,(e=>e/255)));h.set("CA",n);c&&h.set("T",stringToAsciiOrUTF16BE(c));if(i||a){const e=new Dict(t);h.set("AP",e);e.set("N",i||a)}return h}static async createNewAppearanceStream(e,t,i){const{color:a,rect:s,outlines:r,opacity:n}=e,g=[`${getPdfColor(a,!0)}`,"/R0 gs"],o=[];for(const e of r){o.length=0;o.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,i=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=Y;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA");this._setDefaultAppearance({xref:i,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:a,pointsCallback:(e,t)=>{const i=(t[1]-t[5])/6;let a=i,s=t[4];const r=t[5],n=t[6];e.push(`${s} ${r+a} m`);do{s+=2;a=0===a?i:0;e.push(`${s} ${r+a} l`)}while(s{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{#T;constructor(e){super(e);this.data.annotationType=K;this.#T=this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!1;this.#T=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}this.data.hasOwnCanvas=this.#T;return!t?.has(this.data.id)}static async createImage(e,t){const{width:i,height:a}=e,s=new OffscreenCanvas(i,a),r=s.getContext("2d",{alpha:!0});r.drawImage(e,0,0);const n=r.getImageData(0,0,i,a).data,g=new Uint32Array(n.buffer),o=g.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(o){r.fillStyle="white";r.fillRect(0,0,i,a);r.drawImage(e,0,0)}const c=s.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),C=Name.get("XObject"),h=Name.get("Image"),l=new Dict(t);l.set("Type",C);l.set("Subtype",h);l.set("BitsPerComponent",8);l.set("ColorSpace",Name.get("DeviceRGB"));l.set("Filter",Name.get("DCTDecode"));l.set("BBox",[0,0,i,a]);l.set("Width",i);l.set("Height",a);let Q=null;if(o){const e=new Uint8Array(g.length);if(FeatureTest.isLittleEndian)for(let t=0,i=g.length;t>>24;else for(let t=0,i=g.length;t=0&&r<=1?r:null}}class DecryptStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;this.decrypt=i;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.str.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.str.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const i=this.bufferLength,a=i+e.length;this.ensureBuffer(a).set(e,i);this.bufferLength=a}}class ARCFourCipher{constructor(e){this.a=0;this.b=0;const t=new Uint8Array(256),i=e.length;for(let e=0;e<256;++e)t[e]=e;for(let a=0,s=0;a<256;++a){const r=t[a];s=s+r+e[a%i]&255;t[a]=t[s];t[s]=r}this.s=t}encryptBlock(e){let t=this.a,i=this.b;const a=this.s,s=e.length,r=new Uint8Array(s);for(let n=0;n>5&255;C[h++]=s>>13&255;C[h++]=s>>21&255;C[h++]=s>>>29&255;C[h++]=0;C[h++]=0;C[h++]=0;const E=new Int32Array(16);for(h=0;h>>32-g)|0;s=r}r=r+s|0;n=n+c|0;g=g+Q|0;o=o+u|0}return new Uint8Array([255&r,r>>8&255,r>>16&255,r>>>24&255,255&n,n>>8&255,n>>16&255,n>>>24&255,255&g,g>>8&255,g>>16&255,g>>>24&255,255&o,o>>8&255,o>>16&255,o>>>24&255])}}();class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}or(e){this.high|=e.high;this.low|=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}shiftLeft(e){if(e>=32){this.high=this.low<>>32-e;this.low<<=e}}rotateRight(e){let t,i;if(32&e){i=this.low;t=this.high}else{t=this.low;i=this.high}e&=31;this.low=t>>>e|i<<32-e;this.high=i>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let i=(this.high>>>0)+(e.high>>>0);t>4294967295&&(i+=1);this.low=0|t;this.high=0|i}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const Ag=function calculateSHA256Closure(){function rotr(e,t){return e>>>t|e<<32-t}function ch(e,t,i){return e&t^~e&i}function maj(e,t,i){return e&t^e&i^t&i}function sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}const e=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];return function hash(t,i,a){let s=1779033703,r=3144134277,n=1013904242,g=2773480762,o=1359893119,c=2600822924,C=528734635,h=1541459225;const l=64*Math.ceil((a+9)/64),Q=new Uint8Array(l);let E,u;for(E=0;E>>29&255;Q[E++]=a>>21&255;Q[E++]=a>>13&255;Q[E++]=a>>5&255;Q[E++]=a<<3&255;const f=new Uint32Array(64);for(E=0;E>>10)+f[u-7]+littleSigma(f[u-15])+f[u-16]|0;let t,i,a=s,l=r,d=n,m=g,y=o,w=c,D=C,b=h;for(u=0;u<64;++u){t=b+sigmaPrime(y)+ch(y,w,D)+e[u]+f[u];i=sigma(a)+maj(a,l,d);b=D;D=w;w=y;y=m+t|0;m=d;d=l;l=a;a=t+i|0}s=s+a|0;r=r+l|0;n=n+d|0;g=g+m|0;o=o+y|0;c=c+w|0;C=C+D|0;h=h+b|0}var p;return new Uint8Array([s>>24&255,s>>16&255,s>>8&255,255&s,r>>24&255,r>>16&255,r>>8&255,255&r,n>>24&255,n>>16&255,n>>8&255,255&n,g>>24&255,g>>16&255,g>>8&255,255&g,o>>24&255,o>>16&255,o>>8&255,255&o,c>>24&255,c>>16&255,c>>8&255,255&c,C>>24&255,C>>16&255,C>>8&255,255&C,h>>24&255,h>>16&255,h>>8&255,255&h])}}(),eg=function calculateSHA512Closure(){function ch(e,t,i,a,s){e.assign(t);e.and(i);s.assign(t);s.not();s.and(a);e.xor(s)}function maj(e,t,i,a,s){e.assign(t);e.and(i);s.assign(t);s.and(a);e.xor(s);s.assign(i);s.and(a);e.xor(s)}function sigma(e,t,i){e.assign(t);e.rotateRight(28);i.assign(t);i.rotateRight(34);e.xor(i);i.assign(t);i.rotateRight(39);e.xor(i)}function sigmaPrime(e,t,i){e.assign(t);e.rotateRight(14);i.assign(t);i.rotateRight(18);e.xor(i);i.assign(t);i.rotateRight(41);e.xor(i)}function littleSigma(e,t,i){e.assign(t);e.rotateRight(1);i.assign(t);i.rotateRight(8);e.xor(i);i.assign(t);i.shiftRight(7);e.xor(i)}function littleSigmaPrime(e,t,i){e.assign(t);e.rotateRight(19);i.assign(t);i.rotateRight(61);e.xor(i);i.assign(t);i.shiftRight(6);e.xor(i)}const e=[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)];return function hash(t,i,a,s=!1){let r,n,g,o,c,C,h,l;if(s){r=new Word64(3418070365,3238371032);n=new Word64(1654270250,914150663);g=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);c=new Word64(1731405415,4290775857);C=new Word64(2394180231,1750603025);h=new Word64(3675008525,1694076839);l=new Word64(1203062813,3204075428)}else{r=new Word64(1779033703,4089235720);n=new Word64(3144134277,2227873595);g=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);c=new Word64(1359893119,2917565137);C=new Word64(2600822924,725511199);h=new Word64(528734635,4215389547);l=new Word64(1541459225,327033209)}const Q=128*Math.ceil((a+17)/128),E=new Uint8Array(Q);let u,d;for(u=0;u>>29&255;E[u++]=a>>21&255;E[u++]=a>>13&255;E[u++]=a>>5&255;E[u++]=a<<3&255;const p=new Array(80);for(u=0;u<80;u++)p[u]=new Word64(0,0);let m=new Word64(0,0),y=new Word64(0,0),w=new Word64(0,0),D=new Word64(0,0),b=new Word64(0,0),F=new Word64(0,0),S=new Word64(0,0),k=new Word64(0,0);const R=new Word64(0,0),N=new Word64(0,0),G=new Word64(0,0),M=new Word64(0,0);let U,x;for(u=0;u=1;--e){i=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=i;i=r[14];a=r[10];r[14]=r[6];r[10]=r[2];r[6]=i;r[2]=a;i=r[15];a=r[11];s=r[7];r[15]=r[3];r[11]=i;r[7]=a;r[3]=s;for(let e=0;e<16;++e)r[e]=this._inv_s[r[e]];for(let i=0,a=16*e;i<16;++i,++a)r[i]^=t[a];for(let e=0;e<16;e+=4){const t=this._mix[r[e]],a=this._mix[r[e+1]],s=this._mix[r[e+2]],n=this._mix[r[e+3]];i=t^a>>>8^a<<24^s>>>16^s<<16^n>>>24^n<<8;r[e]=i>>>24&255;r[e+1]=i>>16&255;r[e+2]=i>>8&255;r[e+3]=255&i}}i=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=i;i=r[14];a=r[10];r[14]=r[6];r[10]=r[2];r[6]=i;r[2]=a;i=r[15];a=r[11];s=r[7];r[15]=r[3];r[11]=i;r[7]=a;r[3]=s;for(let e=0;e<16;++e){r[e]=this._inv_s[r[e]];r[e]^=t[e]}return r}_encrypt(e,t){const i=this._s;let a,s,r;const n=new Uint8Array(16);n.set(e);for(let e=0;e<16;++e)n[e]^=t[e];for(let e=1;e=a;--i)if(e[i]!==t){t=0;break}g-=t;r[r.length-1]=e.subarray(0,16-t)}}const o=new Uint8Array(g);for(let e=0,t=0,i=r.length;e=256&&(g=255&(27^g))}for(let t=0;t<4;++t){i[e]=a^=i[e-32];e++;i[e]=s^=i[e-32];e++;i[e]=r^=i[e-32];e++;i[e]=n^=i[e-32];e++}}return i}}class PDF17{checkOwnerPassword(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);return isArrayEqual(Ag(s,0,s.length),a)}checkUserPassword(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);return isArrayEqual(Ag(a,0,a.length),i)}getOwnerKey(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);const r=Ag(s,0,s.length);return new AES256Cipher(r).decryptBlock(a,!1,new Uint8Array(16))}getUserKey(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);const s=Ag(a,0,a.length);return new AES256Cipher(s).decryptBlock(i,!1,new Uint8Array(16))}}class PDF20{_hash(e,t,i){let a=Ag(t,0,t.length).subarray(0,32),s=[0],r=0;for(;r<64||s.at(-1)>r-32;){const t=e.length+a.length+i.length,c=new Uint8Array(t);let C=0;c.set(e,C);C+=e.length;c.set(a,C);C+=a.length;c.set(i,C);const h=new Uint8Array(64*t);for(let e=0,i=0;e<64;e++,i+=t)h.set(c,i);s=new AES128Cipher(a.subarray(0,16)).encrypt(h,a.subarray(16,32));const l=s.slice(0,16).reduce(((e,t)=>e+t),0)%3;0===l?a=Ag(s,0,s.length):1===l?a=(n=s,g=0,o=s.length,eg(n,g,o,!0)):2===l&&(a=eg(s,0,s.length));r++}var n,g,o;return a.subarray(0,32)}checkOwnerPassword(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);return isArrayEqual(this._hash(e,s,i),a)}checkUserPassword(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);return isArrayEqual(this._hash(e,a,[]),i)}getOwnerKey(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);const r=this._hash(e,s,i);return new AES256Cipher(r).decryptBlock(a,!1,new Uint8Array(16))}getUserKey(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);const s=this._hash(e,a,[]);return new AES256Cipher(s).decryptBlock(i,!1,new Uint8Array(16))}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const i=new this.StreamCipherConstructor;return new DecryptStream(e,t,(function cipherTransformDecryptStream(e,t){return i.decryptBlock(e,t)}))}decryptString(e){const t=new this.StringCipherConstructor;let i=stringToBytes(e);i=t.decryptBlock(i,!0);return bytesToString(i)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const i=16-e.length%16;e+=String.fromCharCode(i).repeat(i);const a=new Uint8Array(16);if("undefined"!=typeof crypto)crypto.getRandomValues(a);else for(let e=0;e<16;e++)a[e]=Math.floor(256*Math.random());let s=stringToBytes(e);s=t.encrypt(s,a);const r=new Uint8Array(16+s.length);r.set(a);r.set(s,16);return bytesToString(r)}let i=stringToBytes(e);i=t.encrypt(i);return bytesToString(i)}}class CipherTransformFactory{static#q=new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]);#O(e,t,i,a,s,r,n,g,o,c,C,h){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const l=6===e?new PDF20:new PDF17;return l.checkUserPassword(t,g,n)?l.getUserKey(t,o,C):t.length&&l.checkOwnerPassword(t,a,r,i)?l.getOwnerKey(t,s,r,c):null}#P(e,t,i,a,s,r,n,g){const o=40+i.length+e.length,c=new Uint8Array(o);let C,h,l=0;if(t){h=Math.min(32,t.length);for(;l>8&255;c[l++]=s>>16&255;c[l++]=s>>>24&255;for(C=0,h=e.length;C=4&&!g){c[l++]=255;c[l++]=255;c[l++]=255;c[l++]=255}let Q=$n(c,0,l);const E=n>>3;if(r>=3)for(C=0;C<50;++C)Q=$n(Q,0,E);const u=Q.subarray(0,E);let d,f;if(r>=3){for(l=0;l<32;++l)c[l]=CipherTransformFactory.#q[l];for(C=0,h=e.length;C>3;if(i>=3)for(g=0;g<50;++g)o=$n(o,0,o.length);let C,h;if(i>=3){h=t;const e=new Uint8Array(c);for(g=19;g>=0;g--){for(let t=0;t>8&255;s[n++]=e>>16&255;s[n++]=255&t;s[n++]=t>>8&255;if(a){s[n++]=115;s[n++]=65;s[n++]=108;s[n++]=84}return $n(s,0,n).subarray(0,Math.min(i.length+5,16))}#X(e,t,i,a,s){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const r=this,n=e.get(t.name),g=n?.get("CFM");if(!g||"None"===g.name)return function(){return new NullCipher};if("V2"===g.name)return function(){return new ARCFourCipher(r.#j(i,a,s,!1))};if("AESV2"===g.name)return function(){return new AES128Cipher(r.#j(i,a,s,!0))};if("AESV3"===g.name)return function(){return new AES256Cipher(s)};throw new FormatError("Unknown crypto method")}constructor(e,t,i){const a=e.get("Filter");if(!isName(a,"Standard"))throw new FormatError("unknown encryption method");this.filterName=a.name;this.dict=e;const s=e.get("V");if(!Number.isInteger(s)||1!==s&&2!==s&&4!==s&&5!==s)throw new FormatError("unsupported encryption algorithm");this.algorithm=s;let r=e.get("Length");if(!r)if(s<=3)r=40;else{const t=e.get("CF"),i=e.get("StmF");if(t instanceof Dict&&i instanceof Name){t.suppressEncryption=!0;const e=t.get(i.name);r=e?.get("Length")||128;r<40&&(r<<=3)}}if(!Number.isInteger(r)||r<40||r%8!=0)throw new FormatError("invalid key length");const n=stringToBytes(e.get("O")),g=stringToBytes(e.get("U")),o=n.subarray(0,32),c=g.subarray(0,32),C=e.get("P"),h=e.get("R"),l=(4===s||5===s)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=l;const Q=stringToBytes(t);let E,u;if(i){if(6===h)try{i=utf8StringToString(i)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}E=stringToBytes(i)}if(5!==s)u=this.#P(Q,E,o,c,C,h,r,l);else{const t=n.subarray(32,40),i=n.subarray(40,48),a=g.subarray(0,48),s=g.subarray(32,40),r=g.subarray(40,48),C=stringToBytes(e.get("OE")),l=stringToBytes(e.get("UE")),Q=stringToBytes(e.get("Perms"));u=this.#O(h,E,o,t,i,a,c,s,r,C,l,Q)}if(!u&&!i)throw new PasswordException("No password given",rt);if(!u&&i){const e=this.#W(E,o,h,r);u=this.#P(Q,e,o,c,C,h,r,l)}if(!u)throw new PasswordException("Incorrect Password",nt);this.encryptionKey=u;if(s>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#X(this.cf,this.strf,e,t,this.encryptionKey),this.#X(this.cf,this.stmf,e,t,this.encryptionKey));const i=this.#j(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(i)};return new CipherTransform(cipherConstructor,cipherConstructor)}}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{constructor(e){super(e);this.node=null}onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const i=t.firstChild;return"value"===i?.nodeName?t.children.map((e=>decodeString(e.textContent))):decodeString(t.textContent)}}class XRef{#Z=null;constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[n,g]=r;if(!Number.isInteger(n)||!Number.isInteger(g))throw new FormatError(`Invalid XRef range fields: ${n}, ${g}`);if(!Number.isInteger(i)||!Number.isInteger(a)||!Number.isInteger(s))throw new FormatError(`Invalid XRef entry fields length: ${n}, ${g}`);for(let r=t.entryNum;r=e.length);){i+=String.fromCharCode(a);a=e[t]}return i}function skipUntil(e,t,i){const a=i.length,s=e.length;let r=0;for(;t=a)break;t++;r++}return r}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,i=/^(\d+)\s+(\d+)\s+obj\b/,a=new Uint8Array([116,114,97,105,108,101,114]),s=new Uint8Array([115,116,97,114,116,120,114,101,102]),r=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const n=this.stream;n.pos=0;const g=n.getBytes(),o=bytesToString(g),c=g.length;let C=n.start;const h=[],l=[];for(;C=c)break;Q=g[C]}while(10!==Q&&13!==Q);continue}const E=readToken(g,C);let u;if(E.startsWith("xref")&&(4===E.length||/\s/.test(E[4]))){C+=skipUntil(g,C,a);h.push(C);C+=skipUntil(g,C,s)}else if(u=i.exec(E)){const t=0|u[1],i=0|u[2],a=C+E.length;let s,h=!1;if(this.entries[t]){if(this.entries[t].gen===i)try{new Parser({lexer:new Lexer(n.makeSubStream(a))}).getObj();h=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${E}): "${e}".`):h=!0}}else h=!0;h&&(this.entries[t]={offset:C-n.start,gen:i,uncompressed:!0});e.lastIndex=a;const Q=e.exec(o);if(Q){s=e.lastIndex+1-C;if("endobj"!==Q[1]){warn(`indexObjects: Found "${Q[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);s-=Q[1].length+1}}else s=c-C;const d=g.subarray(C,C+s),f=skipUntil(d,0,r);if(f0?Math.max(...this._xrefStms):null)}getEntry(e){const t=this.entries[e];return t&&!t.free&&t.offset?t:null}fetchIfRef(e,t=!1){return e instanceof Ref?this.fetch(e,t):e}fetch(e,t=!1){if(!(e instanceof Ref))throw new Error("ref object is not a reference");const i=e.num,a=this._cacheMap.get(i);if(void 0!==a){a instanceof Dict&&!a.objId&&(a.objId=e.toString());return a}let s=this.getEntry(i);if(null===s){this._cacheMap.set(i,s);return s}if(this._pendingRefs.has(e)){this._pendingRefs.remove(e);warn(`Ignoring circular reference: ${e}.`);return lt}this._pendingRefs.put(e);try{s=s.uncompressed?this.fetchUncompressed(e,s,t):this.fetchCompressed(e,s,t);this._pendingRefs.remove(e)}catch(t){this._pendingRefs.remove(e);throw t}s instanceof Dict?s.objId=e.toString():s instanceof BaseStream&&(s.dict.objId=e.toString());return s}fetchUncompressed(e,t,i=!1){const a=e.gen;let s=e.num;if(t.gen!==a){const r=`Inconsistent generation in XRef: ${e}`;if(this._generationFallback&&t.gen0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this._getBoundingBox("MediaBox")||tg)}get cropBox(){return shadow(this,"cropBox",this._getBoundingBox("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const i=Util.intersect(e,t);if(i&&i[2]-i[0]>0&&i[3]-i[1]>0)return shadow(this,"view",i);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this._getInheritableProperty("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}_onSubStreamError(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}getContentStream(){return this.pdfManager.ensure(this,"content").then((e=>e instanceof BaseStream?e:Array.isArray(e)?new StreamsSequenceStream(e,this._onSubStreamError.bind(this)):new NullStream))}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#V(e,t,i){const a=[];for(const s of e)if(s.id){const e=Ref.fromString(s.id);if(!e){warn(`A non-linked annotation cannot be modified: ${s.id}`);continue}if(s.deleted){t.put(e,e);if(s.popupRef){const e=Ref.fromString(s.popupRef);e&&t.put(e,e)}continue}i?.put(e);s.ref=e;a.push(this.xref.fetchAsync(e).then((e=>{e instanceof Dict&&(s.oldAnnotation=e.clone())}),(()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)})));delete s.id}await Promise.all(a)}async saveNewAnnotations(e,t,i,a,s){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const r=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),n=new RefSetCache,g=new RefSet;await this.#V(i,n,g);const o=this.pageDict,c=this.annotations.filter((e=>!(e instanceof Ref&&n.has(e)))),C=await AnnotationFactory.saveNewAnnotations(r,t,i,a,s);for(const{ref:e}of C.annotations)e instanceof Ref&&!g.has(e)&&c.push(e);const h=o.clone();h.set("Annots",c);s.put(this.ref,{data:h});for(const e of n)s.put(e,{data:null})}save(e,t,i,a){const s=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions});return this._parsedAnnotations.then((function(e){const r=[];for(const n of e)r.push(n.save(s,t,i,a).catch((function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null})));return Promise.all(r)}))}loadResources(e){this.resourcesPromise||=this.pdfManager.ensure(this,"resources");return this.resourcesPromise.then((()=>new ObjectLoader(this.resources,e,this.xref).load()))}getOperatorList({handler:e,sink:t,task:i,intent:a,cacheKey:s,annotationStorage:r=null,modifiedIds:n=null}){const C=this.getContentStream(),E=this.loadResources(["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"]),d=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),f=this.xfaFactory?null:getNewAnnotationsMap(r),p=f?.get(this.pageIndex);let m=Promise.resolve(null),y=null;if(p){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const a=new Set;for(const{bitmapId:e,bitmap:t}of p)!e||t||a.has(e)||a.add(e);const{isOffscreenCanvasSupported:s}=this.evaluatorOptions;if(a.size>0){const e=p.slice();for(const[t,i]of r)t.startsWith(u)&&i.bitmap&&a.has(i.bitmapId)&&e.push(i);t=AnnotationFactory.generateImages(e,this.xref,s)}else t=AnnotationFactory.generateImages(p,this.xref,s);y=new RefSet;m=Promise.all([e,this.#V(p,y,null)]).then((([e])=>e?AnnotationFactory.printNewAnnotations(e,d,i,p,t):null))}const w=Promise.all([C,E]).then((([r])=>{const n=new OperatorList(a,t);e.send("StartRenderPage",{transparency:d.hasBlendModes(this.resources,this.nonBlendModesSet),pageIndex:this.pageIndex,cacheKey:s});return d.getOperatorList({stream:r,task:i,resources:this.resources,operatorList:n}).then((function(){return n}))}));return Promise.all([w,this._parsedAnnotations,m]).then((function([e,t,s]){if(s){t=t.filter((e=>!(e.ref&&y.has(e.ref))));for(let e=0,i=s.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace)));if(r>=0){t.splice(r,1,a);s.splice(e--,1);i--}}}t=t.concat(s)}if(0===t.length||a&l){e.flush(!0);return{length:e.totalLength}}const C=!!(a&h),E=!!(a&Q),u=!!(a&g),f=!!(a&o),p=!!(a&c),m=[];for(const e of t)(u||f&&e.mustBeViewed(r,C)&&e.mustBeViewedWhenEditing(E,n)||p&&e.mustBePrinted(r))&&m.push(e.getOperatorList(d,i,a,r).catch((function(e){warn(`getOperatorList - ignoring annotation data during "${i.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}})));return Promise.all(m).then((function(t){let i=!1,a=!1;for(const{opList:s,separateForm:r,separateCanvas:n}of t){e.addOpList(s);i||=r;a||=n}e.flush(!0,{form:i,canvas:a});return{length:e.totalLength}}))}))}async extractTextContent({handler:e,task:t,includeMarkedContent:i,disableNormalization:a,sink:s}){const r=this.getContentStream(),n=this.loadResources(["ExtGState","Font","Properties","XObject"]),g=this.pdfManager.ensureCatalog("lang"),[o,,c]=await Promise.all([r,n,g]);return new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}).getTextContent({stream:o,task:t,resources:this.resources,includeMarkedContent:i,disableNormalization:a,sink:s,viewBox:this.view,lang:c})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return this.pdfManager.ensure(t,"serializable")}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,i){const a=await this._parsedAnnotations;if(0===a.length)return a;const s=[],r=[];let n;const C=!!(i&g),h=!!(i&o),l=!!(i&c);for(const i of a){const a=C||h&&i.viewable;(a||l&&i.printable)&&s.push(i.data);if(i.hasTextContent&&a){n||=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions});r.push(i.extractTextContent(n,t,[-1/0,-1/0,1/0,1/0]).catch((function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)})))}}await Promise.all(r);return s}get annotations(){const e=this._getInheritableProperty("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then((async e=>{if(0===e.length)return e;const[t,i]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const a=i?.orphanFields,s=[];for(const i of e)s.push(AnnotationFactory.create(this.xref,i,t,this._localIdFactory,!1,a,this.ref).catch((function(e){warn(`_parsedAnnotations: "${e}".`);return null})));const r=[];let n,g;for(const e of await Promise.all(s))e&&(e instanceof WidgetAnnotation?(g||=[]).push(e):e instanceof PopupAnnotation?(n||=[]).push(e):r.push(e));g&&r.push(...g);n&&r.push(...n);return r})))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,pA))}}const ig=new Uint8Array([37,80,68,70,45]),ag=new Uint8Array([115,116,97,114,116,120,114,101,102]),sg=new Uint8Array([101,110,100,111,98,106]);function find(e,t,i=1024,a=!1){const s=t.length,r=e.peekBytes(i),n=r.length-s;if(n<=0)return!1;if(a){const i=s-1;let a=r.length-1;for(;a>=i;){let n=0;for(;n=s){e.pos+=a-i;return!0}a--}}else{let i=0;for(;i<=n;){let a=0;for(;a=s){e.pos+=i;return!0}i++}}return!1}class PDFDocument{constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);this._pagePromises=new Map;this._version=null;const i={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++i.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,sg)){e.skip(6);let i=e.peekByte();for(;isWhiteSpace(i);){e.pos++;i=e.peekByte()}t=e.pos-e.start}}else{const i=1024,a=ag.length;let s=!1,r=e.end;for(;!s&&r>0;){r-=i-a;r<0&&(r=0);e.pos=r;s=find(e,ag,i,!0)}if(s){e.skip(9);let i;do{i=e.getByte()}while(isWhiteSpace(i));let a="";for(;i>=32&&i<=57;){a+=String.fromCharCode(i);i=e.getByte()}t=parseInt(a,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,ig))return;e.moveStart();e.skip(ig.length);let t,i="";for(;(t=e.getByte())>32&&i.length<7;)i+=String.fromCharCode(t);ft.test(i)?this._version=i:warn(`Invalid PDF header version: ${i}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}_hasOnlyDocumentSignatures(e,t=0){return!!Array.isArray(e)&&e.every((e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("_hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this._hasOnlyDocumentSignatures(e.get("Kids"),t)}const i=isName(e.get("FT"),"Sig"),a=e.get("Rect"),s=Array.isArray(a)&&a.every((e=>0===e));return i&&s}))}get _xfaStreams(){const e=this.catalog.acroForm;if(!e)return null;const t=e.get("XFA"),i={"xdp:xdp":"",template:"",datasets:"",config:"",connectionSet:"",localeSet:"",stylesheet:"","/xdp:xdp":""};if(t instanceof BaseStream&&!t.isEmpty){i["xdp:xdp"]=t;return i}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,a=t.length;e0;e.hasFields=a;const s=t.get("XFA");e.hasXfa=Array.isArray(s)&&s.length>0||s instanceof BaseStream&&!s.isEmpty;const r=!!(1&t.get("SigFlags")),n=r&&this._hasOnlyDocumentSignatures(i);e.hasAcroForm=a&&!n;e.hasSignatures=r}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const e={PDFFormatVersion:this.version,Language:this.catalog.lang,EncryptFilterName:this.xref.encrypt?this.xref.encrypt.filterName:null,IsLinearized:!!this.linearization,IsAcroFormPresent:this.formInfo.hasAcroForm,IsXFAPresent:this.formInfo.hasXfa,IsCollectionPresent:!!this.catalog.collection,IsSignaturesPresent:this.formInfo.hasSignatures};let t;try{t=this.xref.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(t instanceof Dict))return shadow(this,"documentInfo",e);for(const i of t.getKeys()){const a=t.get(i);switch(i){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof a){e[i]=stringToPDFString(a);continue}break;case"Trapped":if(a instanceof Name){e[i]=a;continue}break;default:let t;switch(typeof a){case"string":t=stringToPDFString(a);break;case"number":case"boolean":t=a;break;default:a instanceof Name&&(t=a)}if(void 0===t){warn(`Bad value, for custom key "${i}", in Info: ${a}.`);continue}e.Custom||(e.Custom=Object.create(null));e.Custom[i]=t;continue}warn(`Bad value, for key "${i}", in Info: ${a}.`)}return shadow(this,"documentInfo",e)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let i,a;if(Array.isArray(t)&&validate(t[0])){i=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(a=stringToBytes(t[1]))}else i=$n(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[toHexUtil(i),a?toHexUtil(a):null])}async _getLinearizationPage(e){const{catalog:t,linearization:i,xref:a}=this,s=Ref.get(i.objectNumberFirst,0);try{const e=await a.fetchAsync(s);if(e instanceof Dict){let i=e.getRaw("Type");i instanceof Ref&&(i=await a.fetchAsync(i));if(isName(i,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(s)||t.pageKidsCountCache.put(s,1);t.pageIndexCache.has(s)||t.pageIndexCache.put(s,0);return[e,s]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(i){warn(`_getLinearizationPage: "${i.message}".`);return t.getPageDict(e)}}getPage(e){const t=this._pagePromises.get(e);if(t)return t;const{catalog:i,linearization:a,xfaFactory:s}=this;let r;r=s?Promise.resolve([Dict.empty,null]):a?.pageFirst===e?this._getLinearizationPage(e):i.getPageDict(e);r=r.then((([t,a])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:a,globalIdFactory:this._globalIdFactory,fontCache:i.fontCache,builtInCMapCache:i.builtInCMapCache,standardFontDataCache:i.standardFontDataCache,globalImageCache:i.globalImageCache,systemFontCache:i.systemFontCache,nonBlendModesSet:i.nonBlendModesSet,xfaFactory:s})));this._pagePromises.set(e,r);return r}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this._pagePromises.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:i}=this;t.setActualNumPages();let a;try{await Promise.all([i.ensureDoc("xfaFactory"),i.ensureDoc("linearization"),i.ensureCatalog("numPages")]);if(this.xfaFactory)return;a=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(a))throw new FormatError("Page count is not an integer.");if(a<=1)return;await this.getPage(a-1)}catch(s){this._pagePromises.delete(a-1);await this.cleanup();if(s instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${a}.`);let r;try{r=await t.getAllPageDicts(e)}catch(i){if(i instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[a,s]]of r){let r;if(a instanceof Error){r=Promise.reject(a);r.catch((()=>{}))}else r=Promise.resolve(new Page({pdfManager:i,xref:this.xref,pageIndex:e,pageDict:a,ref:s,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this._pagePromises.set(e,r)}t.setActualNumPages(r.size)}}fontFallback(e,t){return this.catalog.fontFallback(e,t)}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#z(e,t,i,a,s,r,n){const{xref:g}=this;if(!(i instanceof Ref)||r.has(i))return;r.put(i);const o=await g.fetchAsync(i);if(!(o instanceof Dict))return;if(o.has("T")){const t=stringToPDFString(await o.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let i=o;for(;;){i=i.getRaw("Parent")||t;if(i instanceof Ref){if(r.has(i))break;i=await g.fetchAsync(i)}if(!(i instanceof Dict))break;if(i.has("T")){const t=stringToPDFString(await i.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!o.has("Parent")&&isName(o.get("Subtype"),"Widget")&&n.put(i,t);a.has(e)||a.set(e,[]);a.get(e).push(AnnotationFactory.create(g,i,s,null,!0,n,null).then((e=>e?.getFieldObject())).catch((function(e){warn(`#collectFieldObjects: "${e}".`);return null})));if(!o.has("Kids"))return;const c=await o.getAsync("Kids");if(Array.isArray(c))for(const t of c)await this.#z(e,i,t,a,s,r,n)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then((async e=>{if(!e.hasFields)return null;const[t,i]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureCatalog("acroForm")]);if(!t)return null;const a=new RefSet,s=Object.create(null),r=new Map,n=new RefSetCache;for(const e of await i.getAsync("Fields"))await this.#z("",null,e,r,t,a,n);const g=[];for(const[e,t]of r)g.push(Promise.all(t).then((t=>{(t=t.filter((e=>!!e))).length>0&&(s[e]=t)})));await Promise.all(g);return{allFields:s,orphanFields:n}})))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t&&Object.values(t.allFields).some((e=>e.some((e=>null!==e.actions))))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const i of e)i instanceof Ref&&t.push(i.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}}class BasePdfManager{constructor(e){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e.docBaseUrl);this._docId=e.docId;this._password=e.password;this.enableXfa=e.enableXfa;e.evaluatorOptions.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;e.evaluatorOptions.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(e.evaluatorOptions)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}get catalog(){return this.pdfDocument.catalog}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}loadXfaFonts(e,t){return this.pdfDocument.loadXfaFonts(e,t)}loadXfaImages(){return this.pdfDocument.loadXfaImages()}serializeXfaData(e){return this.pdfDocument.serializeXfaData(e)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,i){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,i){const a=e[t];return"function"==typeof a?a.apply(e,i):a}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,i){try{const a=e[t];return"function"==typeof a?a.apply(e,i):a}catch(a){if(!(a instanceof MissingDataException))throw a;await this.requestRange(a.begin,a.end);return this.ensure(e,t,i)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const rg=1,ng=2,gg=1,og=2,Ig=3,cg=4,Cg=5,hg=6,lg=7,Bg=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof MissingPDFException||e instanceof PasswordException||e instanceof UnexpectedResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"MissingPDFException":return new MissingPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"UnexpectedResponseException":return new UnexpectedResponseException(e.message,e.status);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#_=new AbortController;constructor(e,t,i){this.sourceName=e;this.targetName=t;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#$.bind(this),{signal:this.#_.signal})}#$({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#AA(e);return}if(e.callback){const t=e.callbackId,i=this.callbackCapabilities[t];if(!i)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===rg)i.resolve(e.data);else{if(e.callback!==ng)throw new Error("Unexpected callback case");i.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const i=this.sourceName,a=e.sourceName,s=this.comObj;Promise.try(t,e.data).then((function(t){s.postMessage({sourceName:i,targetName:a,callback:rg,callbackId:e.callbackId,data:t})}),(function(t){s.postMessage({sourceName:i,targetName:a,callback:ng,callbackId:e.callbackId,reason:wrapReason(t)})}))}else e.streamId?this.#eA(e):t(e.data)}on(e,t){const i=this.actionHandler;if(i[e])throw new Error(`There is already an actionName called "${e}"`);i[e]=t}send(e,t,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},i)}sendWithPromise(e,t,i){const a=this.callbackId++,s=Promise.withResolvers();this.callbackCapabilities[a]=s;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:a,data:t},i)}catch(e){s.reject(e)}return s.promise}sendWithStream(e,t,i,a){const s=this.streamId++,r=this.sourceName,n=this.targetName,g=this.comObj;return new ReadableStream({start:i=>{const o=Promise.withResolvers();this.streamControllers[s]={controller:i,startCall:o,pullCall:null,cancelCall:null,isClosed:!1};g.postMessage({sourceName:r,targetName:n,action:e,streamId:s,data:t,desiredSize:i.desiredSize},a);return o.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[s].pullCall=t;g.postMessage({sourceName:r,targetName:n,stream:hg,streamId:s,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[s].cancelCall=t;this.streamControllers[s].isClosed=!0;g.postMessage({sourceName:r,targetName:n,stream:gg,streamId:s,reason:wrapReason(e)});return t.promise}},i)}#eA(e){const t=e.streamId,i=this.sourceName,a=e.sourceName,s=this.comObj,r=this,n=this.actionHandler[e.action],g={enqueue(e,r=1,n){if(this.isCancelled)return;const g=this.desiredSize;this.desiredSize-=r;if(g>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}s.postMessage({sourceName:i,targetName:a,stream:cg,streamId:t,chunk:e},n)},close(){if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:i,targetName:a,stream:Ig,streamId:t});delete r.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:i,targetName:a,stream:Cg,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};g.sinkCapability.resolve();g.ready=g.sinkCapability.promise;this.streamSinks[t]=g;Promise.try(n,e.data,g).then((function(){s.postMessage({sourceName:i,targetName:a,stream:Bg,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:Bg,streamId:t,reason:wrapReason(e)})}))}#AA(e){const t=e.streamId,i=this.sourceName,a=e.sourceName,s=this.comObj,r=this.streamControllers[t],n=this.streamSinks[t];switch(e.stream){case Bg:e.success?r.startCall.resolve():r.startCall.reject(wrapReason(e.reason));break;case lg:e.success?r.pullCall.resolve():r.pullCall.reject(wrapReason(e.reason));break;case hg:if(!n){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,success:!0});break}n.desiredSize<=0&&e.desiredSize>0&&n.sinkCapability.resolve();n.desiredSize=e.desiredSize;Promise.try(n.onPull||onFn).then((function(){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,reason:wrapReason(e)})}));break;case cg:assert(r,"enqueue should have stream controller");if(r.isClosed)break;r.controller.enqueue(e.chunk);break;case Ig:assert(r,"close should have stream controller");if(r.isClosed)break;r.isClosed=!0;r.controller.close();this.#tA(r,t);break;case Cg:assert(r,"error should have stream controller");r.controller.error(wrapReason(e.reason));this.#tA(r,t);break;case og:e.success?r.cancelCall.resolve():r.cancelCall.reject(wrapReason(e.reason));this.#tA(r,t);break;case gg:if(!n)break;const g=wrapReason(e.reason);Promise.try(n.onCancel||onFn,g).then((function(){s.postMessage({sourceName:i,targetName:a,stream:og,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:og,streamId:t,reason:wrapReason(e)})}));n.sinkCapability.reject(g);n.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#tA(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#_?.abort();this.#_=null}}async function writeObject(e,t,i,{encrypt:a=null}){const s=a?.createCipherTransform(e.num,e.gen);i.push(`${e.num} ${e.gen} obj\n`);t instanceof Dict?await writeDict(t,i,s):t instanceof BaseStream?await writeStream(t,i,s):(Array.isArray(t)||ArrayBuffer.isView(t))&&await writeArray(t,i,s);i.push("\nendobj\n")}async function writeDict(e,t,i){t.push("<<");for(const a of e.getKeys()){t.push(` /${escapePDFName(a)} `);await writeValue(e.getRaw(a),t,i)}t.push(">>")}async function writeStream(e,t,i){let a=e.getBytes();const{dict:s}=e,[r,n]=await Promise.all([s.getAsync("Filter"),s.getAsync("DecodeParms")]),g=isName(Array.isArray(r)?await s.xref.fetchIfRefAsync(r[0]):r,"FlateDecode");if(a.length>=256||g)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(a).then((async()=>{await t.ready;await t.close()})).catch((()=>{}));const i=await new Response(e.readable).arrayBuffer();a=new Uint8Array(i);let o,c;if(r){if(!g){o=Array.isArray(r)?[Name.get("FlateDecode"),...r]:[Name.get("FlateDecode"),r];n&&(c=Array.isArray(n)?[null,...n]:[null,n])}}else o=Name.get("FlateDecode");o&&s.set("Filter",o);c&&s.set("DecodeParms",c)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let o=bytesToString(a);i&&(o=i.encryptString(o));s.set("Length",o.length);await writeDict(s,t,i);t.push(" stream\n",o,"\nendstream")}async function writeArray(e,t,i){t.push("[");let a=!0;for(const s of e){a?a=!1:t.push(" ");await writeValue(s,t,i)}t.push("]")}async function writeValue(e,t,i){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await writeArray(e,t,i);else if("string"==typeof e){i&&(e=i.encryptString(e));t.push(`(${escapeString(e)})`)}else"number"==typeof e?t.push(numberToString(e)):"boolean"==typeof e?t.push(e.toString()):e instanceof Dict?await writeDict(e,t,i):e instanceof BaseStream?await writeStream(e,t,i):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,i,a){for(let s=t+i-1;s>i-1;s--){a[s]=255&e;e>>=8}return i+t}function writeString(e,t,i){for(let a=0,s=e.length;a1&&(r=i.documentElement.searchNode([s.at(-1)],0));r?r.childNodes=Array.isArray(a)?a.map((e=>new SimpleDOMNode("value",e))):[new SimpleDOMNode("#text",a)]:warn(`Node not found for path: ${t}`)}const a=[];i.documentElement.dump(a);return a.join("")}(a.fetchIfRef(t).getString(),i)}const s=new StringStream(e);s.dict=new Dict(a);s.dict.set("Type",Name.get("EmbeddedFile"));i.put(t,{data:s})}function getIndexes(e){const t=[];for(const{ref:i}of e)i.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(i.num,1);return t}function computeIDs(e,t,i){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const a=function computeMD5(e,t){const i=Math.floor(Date.now()/1e3),a=t.filename||"",s=[i.toString(),a,e.toString()];let r=s.reduce(((e,t)=>e+t.length),0);for(const e of Object.values(t.info)){s.push(e);r+=e.length}const n=new Uint8Array(r);let g=0;for(const e of s){writeString(e,g,n);g+=e.length}return bytesToString($n(n))}(e,t);i.set("ID",[t.fileIds[0],a])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:i,xref:a=null,hasXfa:s=!1,xfaDatasetsRef:r=null,hasXfaDatasetsEntry:n=!1,needAppearances:g,acroFormRef:o=null,acroForm:c=null,xfaData:C=null,useXrefStream:h=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:i,hasXfa:a,hasXfaDatasetsEntry:s,xfaDatasetsRef:r,needAppearances:n,changes:g}){!a||s||r||warn("XFA - Cannot save it");if(!n&&(!a||!r||s))return;const o=t.clone();if(a&&!s){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,r);o.set("XFA",e)}n&&o.set("NeedAppearances",!0);g.put(i,{data:o})}({xref:a,acroForm:c,acroFormRef:o,hasXfa:s,hasXfaDatasetsEntry:n,xfaDatasetsRef:r,needAppearances:g,changes:i});s&&updateXFA({xfaData:C,xfaDatasetsRef:r,changes:i,xref:a});const l=function getTrailerDict(e,t,i){const a=new Dict(null);a.set("Prev",e.startXRef);const s=e.newRef;if(i){t.put(s,{data:""});a.set("Size",s.num+1);a.set("Type",Name.get("XRef"))}else a.set("Size",s.num);null!==e.rootRef&&a.set("Root",e.rootRef);null!==e.infoRef&&a.set("Info",e.infoRef);null!==e.encryptRef&&a.set("Encrypt",e.encryptRef);return a}(t,i,h),Q=[],E=await async function writeChanges(e,t,i=[]){const a=[];for(const[s,{data:r}]of e.items())if(null!==r&&"string"!=typeof r){await writeObject(s,r,i,t);a.push({ref:s,data:i.join("")});i.length=0}else a.push({ref:s,data:r});return a.sort(((e,t)=>e.ref.num-t.ref.num))}(i,a,Q);let u=e.length;const d=e.at(-1);if(10!==d&&13!==d){Q.push("\n");u+=1}for(const{data:e}of E)null!==e&&Q.push(e);await(h?async function getXRefStreamTable(e,t,i,a,s){const r=[];let n=0,g=0;for(const{ref:e,data:a}of i){let i;n=Math.max(n,t);if(null!==a){i=Math.min(e.gen,65535);r.push([1,t,i]);t+=a.length}else{i=Math.min(e.gen+1,65535);r.push([0,0,i])}g=Math.max(g,i)}a.set("Index",getIndexes(i));const o=[1,getSizeInBytes(n),getSizeInBytes(g)];a.set("W",o);computeIDs(t,e,a);const c=o.reduce(((e,t)=>e+t),0),C=new Uint8Array(c*r.length),h=new Stream(C);h.dict=a;let l=0;for(const[e,t,i]of r){l=writeInt(e,o[0],l,C);l=writeInt(t,o[1],l,C);l=writeInt(i,o[2],l,C)}await writeObject(e.newRef,h,s,{});s.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,u,E,l,Q):async function getXRefTable(e,t,i,a,s){s.push("xref\n");const r=getIndexes(i);let n=0;for(const{ref:e,data:a}of i){if(e.num===r[n]){s.push(`${r[n]} ${r[n+1]}\n`);n+=2}if(null!==a){s.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=a.length}else s.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,a);s.push("trailer\n");await writeDict(a,s);s.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,u,E,l,Q));const f=Q.reduce(((e,t)=>e+t.length),e.length),p=new Uint8Array(f);p.set(e);let m=e.length;for(const e of Q){writeString(e,m,p);m+=e.length}return p}class PDFWorkerStream{constructor(e){this._msgHandler=e;this._contentLength=null;this._fullRequestReader=null;this._rangeRequestReaders=[]}getFullReader(){assert(!this._fullRequestReader,"PDFWorkerStream.getFullReader can only be called once.");this._fullRequestReader=new PDFWorkerStreamReader(this._msgHandler);return this._fullRequestReader}getRangeReader(e,t){const i=new PDFWorkerStreamRangeReader(e,t,this._msgHandler);this._rangeRequestReaders.push(i);return i}cancelAllRequests(e){this._fullRequestReader?.cancel(e);for(const t of this._rangeRequestReaders.slice(0))t.cancel(e)}}class PDFWorkerStreamReader{constructor(e){this._msgHandler=e;this.onProgress=null;this._contentLength=null;this._isRangeSupported=!1;this._isStreamingSupported=!1;const t=this._msgHandler.sendWithStream("GetReader");this._reader=t.getReader();this._headersReady=this._msgHandler.sendWithPromise("ReaderHeadersReady").then((e=>{this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._contentLength=e.contentLength}))}get headersReady(){return this._headersReady}get contentLength(){return this._contentLength}get isStreamingSupported(){return this._isStreamingSupported}get isRangeSupported(){return this._isRangeSupported}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader{constructor(e,t,i){this._msgHandler=i;this.onProgress=null;const a=this._msgHandler.sendWithStream("GetRangeReader",{begin:e,end:t});this._reader=a.getReader()}get isStreamingSupported(){return!1}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!t&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let i=!1;e.on("test",(t=>{if(!i){i=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(gt=e)}(e.verbosity)}));e.on("GetDocRequest",(e=>this.createDocumentHandler(e,t)))}static createDocumentHandler(e,t){let i,a=!1,s=null;const r=new Set,n=getVerbosityLevel(),{docId:g,apiVersion:o}=e,c="4.10.38";if(o!==c)throw new Error(`The API version "${o}" does not match the Worker version "${c}".`);const C=[];for(const e in[])C.push(e);if(C.length)throw new Error("The `Array.prototype` contains unexpected enumerable properties: "+C.join(", ")+"; thus breaking e.g. `for...in` iteration of `Array`s.");const h=g+"_worker";let l=new MessageHandler(h,g,t);function ensureNotTerminated(){if(a)throw new Error("Worker was terminated")}function startWorkerTask(e){r.add(e)}function finishWorkerTask(e){e.finish();r.delete(e)}async function loadDocument(e){await i.ensureDoc("checkHeader");await i.ensureDoc("parseStartXRef");await i.ensureDoc("parse",[e]);await i.ensureDoc("checkFirstPage",[e]);await i.ensureDoc("checkLastPage",[e]);const t=await i.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaFonts");startWorkerTask(e);await Promise.all([i.loadXfaFonts(l,e).catch((e=>{})).then((()=>finishWorkerTask(e))),i.loadXfaImages()])}const[a,s]=await Promise.all([i.ensureDoc("numPages"),i.ensureDoc("fingerprints")]);return{numPages:a,fingerprints:s,htmlForXfa:t?await i.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();l.send("GetDoc",{pdfInfo:e})}function onFailure(e){ensureNotTerminated();if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);l.sendWithPromise("PasswordRequest",e).then((function({password:e}){finishWorkerTask(t);i.updatePassword(e);pdfManagerReady()})).catch((function(){finishWorkerTask(t);l.send("DocException",e)}))}else l.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,(function(e){ensureNotTerminated();e instanceof XRefParseException?i.requestLoadedStream().then((function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)})):onFailure(e)}))}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:i,rangeChunkSize:a,length:r,docBaseUrl:n,enableXfa:o,evaluatorOptions:c}){const C={source:null,disableAutoFetch:i,docBaseUrl:n,docId:g,enableXfa:o,evaluatorOptions:c,handler:l,length:r,password:t,rangeChunkSize:a};if(e){C.source=e;return new LocalPdfManager(C)}const h=new PDFWorkerStream(l),Q=h.getFullReader(),E=Promise.withResolvers();let u,d=[],f=0;Q.headersReady.then((function(){if(Q.isRangeSupported){C.source=h;C.length=Q.contentLength;C.disableAutoFetch||=Q.isStreamingSupported;u=new NetworkPdfManager(C);for(const e of d)u.sendProgressiveData(e);d=[];E.resolve(u);s=null}})).catch((function(e){E.reject(e);s=null}));new Promise((function(e,t){const readChunk=function({value:e,done:i}){try{ensureNotTerminated();if(i){if(!u){const e=arrayBuffersToBytes(d);d=[];r&&e.length!==r&&warn("reported HTTP length is different from actual");C.source=e;u=new LocalPdfManager(C);E.resolve(u)}s=null;return}f+=e.byteLength;Q.isStreamingSupported||l.send("DocProgress",{loaded:f,total:Math.max(f,Q.contentLength||0)});u?u.sendProgressiveData(e):d.push(e);Q.read().then(readChunk,t)}catch(e){t(e)}};Q.read().then(readChunk,t)})).catch((function(e){E.reject(e);s=null}));s=e=>{h.cancelAllRequests(e)};return E.promise})(e).then((function(e){if(a){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}i=e;i.requestLoadedStream(!0).then((e=>{l.send("DataLoaded",{length:e.bytes.byteLength})}))})).then(pdfManagerReady,onFailure)}l.on("GetPage",(function(e){return i.getPage(e.pageIndex).then((function(e){return Promise.all([i.ensure(e,"rotate"),i.ensure(e,"ref"),i.ensure(e,"userUnit"),i.ensure(e,"view")]).then((function([e,t,i,a]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:i,view:a}}))}))}));l.on("GetPageIndex",(function(e){const t=Ref.get(e.num,e.gen);return i.ensureCatalog("getPageIndex",[t])}));l.on("GetDestinations",(function(e){return i.ensureCatalog("destinations")}));l.on("GetDestination",(function(e){return i.ensureCatalog("getDestination",[e.id])}));l.on("GetPageLabels",(function(e){return i.ensureCatalog("pageLabels")}));l.on("GetPageLayout",(function(e){return i.ensureCatalog("pageLayout")}));l.on("GetPageMode",(function(e){return i.ensureCatalog("pageMode")}));l.on("GetViewerPreferences",(function(e){return i.ensureCatalog("viewerPreferences")}));l.on("GetOpenAction",(function(e){return i.ensureCatalog("openAction")}));l.on("GetAttachments",(function(e){return i.ensureCatalog("attachments")}));l.on("GetDocJSActions",(function(e){return i.ensureCatalog("jsActions")}));l.on("GetPageJSActions",(function({pageIndex:e}){return i.getPage(e).then((function(e){return i.ensure(e,"jsActions")}))}));l.on("GetOutline",(function(e){return i.ensureCatalog("documentOutline")}));l.on("GetOptionalContentConfig",(function(e){return i.ensureCatalog("optionalContentConfig")}));l.on("GetPermissions",(function(e){return i.ensureCatalog("permissions")}));l.on("GetMetadata",(function(e){return Promise.all([i.ensureDoc("documentInfo"),i.ensureCatalog("metadata")])}));l.on("GetMarkInfo",(function(e){return i.ensureCatalog("markInfo")}));l.on("GetData",(function(e){return i.requestLoadedStream().then((function(e){return e.bytes}))}));l.on("GetAnnotations",(function({pageIndex:e,intent:t}){return i.getPage(e).then((function(i){const a=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(a);return i.getAnnotationsData(l,a,t).then((e=>{finishWorkerTask(a);return e}),(e=>{finishWorkerTask(a);throw e}))}))}));l.on("GetFieldObjects",(function(e){return i.ensureDoc("fieldObjects").then((e=>e?.allFields||null))}));l.on("HasJSActions",(function(e){return i.ensureDoc("hasJSActions")}));l.on("GetCalculationOrderIds",(function(e){return i.ensureDoc("calculationOrderIds")}));l.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:a,filename:s}){const r=[i.requestLoadedStream(),i.ensureCatalog("acroForm"),i.ensureCatalog("acroFormRef"),i.ensureDoc("startXRef"),i.ensureDoc("xref"),i.ensureDoc("linearization"),i.ensureCatalog("structTreeRoot")],n=new RefSetCache,g=[],o=e?null:getNewAnnotationsMap(a),[c,C,h,Q,E,u,d]=await Promise.all(r),f=E.trailer.getRaw("Root")||null;let p;if(o){d?await d.canUpdateStructTree({pdfManager:i,xref:E,newAnnotationsByPage:o})&&(p=d):await StructTreeRoot.canCreateStructureTree({catalogRef:f,pdfManager:i,newAnnotationsByPage:o})&&(p=null);const e=AnnotationFactory.generateImages(a.values(),E,i.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===p?g:[];for(const[a,s]of o)t.push(i.getPage(a).then((t=>{const i=new WorkerTask(`Save (editor): page ${a}`);startWorkerTask(i);return t.saveNewAnnotations(l,i,s,e,n).finally((function(){finishWorkerTask(i)}))})));null===p?g.push(Promise.all(t).then((async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:o,xref:E,catalogRef:f,pdfManager:i,changes:n})}))):p&&g.push(Promise.all(t).then((async()=>{await p.updateStructureTree({newAnnotationsByPage:o,pdfManager:i,changes:n})})))}if(e)g.push(i.serializeXfaData(a));else for(let e=0;ee.needAppearances)),D=C instanceof Dict&&C.get("XFA")||null;let b=null,F=!1;if(Array.isArray(D)){for(let e=0,t=D.length;e{E.resetNewTemporaryRef()}))}));l.on("GetOperatorList",(function(e,t){const a=e.pageIndex;i.getPage(a).then((function(i){const s=new WorkerTask(`GetOperatorList: page ${a}`);startWorkerTask(s);const r=n>=yA?Date.now():0;i.getOperatorList({handler:l,sink:t,task:s,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds}).then((function(e){finishWorkerTask(s);r&&info(`page=${a+1} - getOperatorList: time=${Date.now()-r}ms, len=${e.length}`);t.close()}),(function(e){finishWorkerTask(s);s.terminated||t.error(e)}))}))}));l.on("GetTextContent",(function(e,t){const{pageIndex:a,includeMarkedContent:s,disableNormalization:r}=e;i.getPage(a).then((function(e){const i=new WorkerTask("GetTextContent: page "+a);startWorkerTask(i);const g=n>=yA?Date.now():0;e.extractTextContent({handler:l,task:i,sink:t,includeMarkedContent:s,disableNormalization:r}).then((function(){finishWorkerTask(i);g&&info(`page=${a+1} - getTextContent: time=`+(Date.now()-g)+"ms");t.close()}),(function(e){finishWorkerTask(i);i.terminated||t.error(e)}))}))}));l.on("GetStructTree",(function(e){return i.getPage(e.pageIndex).then((function(e){return i.ensure(e,"getStructTree")}))}));l.on("FontFallback",(function(e){return i.fontFallback(e.id,l)}));l.on("Cleanup",(function(e){return i.cleanup(!0)}));l.on("Terminate",(function(e){a=!0;const t=[];if(i){i.terminate(new AbortException("Worker was terminated."));const e=i.cleanup();t.push(e);i=null}else clearGlobalCaches();s?.(new AbortException("Worker was terminated."));for(const e of r){t.push(e.finished);e.terminate()}return Promise.all(t).then((function(){l.destroy();l=null}))}));l.on("Ready",(function(t){setupDoc(e);e=null}));return h}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}var Qg=__webpack_exports__.WorkerMessageHandler;export{Qg as WorkerMessageHandler}; \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c72ebb554be018511ae972c3f2361dff02dce02 GIT binary patch literal 2545 zcma*pX;2es8VB%~zPr=ibVMCx-JQ^BhLDAsK)^**h(ZDp9YGuzZ%~j!}+w%FI;|aC7){7CdVvG)P{bng1y9Te*f}~*`1kQl$jwb z$tlW~rRS!X?#xfm_&6tTdp_`cjgYwbRFLNdoJCN$S-yhg`ZnC-yvedRSmOh%;Y`Gl6bY$Z-}#C=#F4%9!I1b zWQ~f+9P?;vhCxWwlwl=lrWG|7IYo;{jjmzJ5R9?f>n%-d@>kLINUc z4wM5dAO;kq<$}Dk{2-u0$I6@2N}&cUx9nmV1dYc8jfC}%=F9WCg^OQK9C6poh#2!A z3^EU*UFZvS^)?bu3T?J;@Ahb~%I?+@4!l5!*TjC}GIslNan-RCrrd~PdHYnNLJk+m&`$Y+NV(e>CCu%R#_8GqY4cv#j`#uRWdsg9DxWy(?oOvgCU}&@jy%c!H&-Q zqXJxajAtmQRoRa9V-RFXXh-bK*;Fum{BjpkYQGX~i@OZ^Dx0n&H}kvGKqQ?w(6iGXu_g08T|_hp#ZvFzIwKF*a=oMJ~3UGAjZ?g}GOxm44td zXoyYrU*I=y*vHv89hkYH(v5R#wc)BC3dZJKb3K)f>zaM3%JP(mpecViP0eKKYf3zy z->jx_mc?mCtPEvCQ?uppk?eLJt}_IR7giW%Jr)RyI!+E-voIs*lXI*z`GQc_&D#X( z{6G};HPYj6O|$lXxBJeDaweqa{4L=tOZCjTI^&UOxXg})LRG_cr^B9Rqt(i5ORbQX zq`_xCRsH>xEYY%&*Nyi#{S_JZNlTm#K56`RI%7^amom;*h90Si&g1CfaFV3D|a!`3Y-GKKbL*KSbl z>I96`TR@CqPJl(>QqB~RvK~-U)`e`l4LIqj+IU^~yyIe*|BRVB>4Bup%j{tLdKz4j zY^<8P8m~GRGz*yv0&-RJE+-keJ+%m3wNeopzsltWd->eWmBVwUr)pX` zK~CD<;~Z*Uy3W`3+MrEYxm5qYQ!z%YI;y7DTG`UVH0;@{M{!B&id_}3DBQ?zsotuR zEGLdRx25nLm%-wjlnEi;-aN_1S7???rO~WgA67jjr&(vRa3y$u#kqJbeKnw z{!T!1li9>M+sJ6AUe+*9d}2uGjhzd z|L1Rtp8uTGYyZoQ*`DS^m2dw-X{a)l+3m?ncvn^+O>)hdd3(hMtlhkRGns{<8c0I! zDDjpmwtj?@!6kA|iu3q+Ai;@JR+ zfk+ln&YFC{4bhK6IxVgLs4W%^8Lk`qzWU*L>yq0A3;l}{!wKZ!ue)C)SKI)9dl1hl zhIRLV@8E}rwvE{gX(}$f6x*k)_`*Ijt1=EU-Ls6-(phomeQBgtUs z5Xz~Cd*nE)Ac!0i4ep}Z1AugMB(&F?)#CU{Qc{Sp^vKsdL}vRB30H+Bbzrn`M##H3 z{W8dc_mDroEE+p8_}mnJtzZ4!RNe)zhB)Ds;S57nYSJxtek>^~&(7B+N5MPf2+2xx z5Dl&4X|c@f{Kd|z1r+N|$DmsoVp*3yOdxT^J^-VAk)Z@$4^XrPrFP-Co+MXZ+KJ(W z{JNYvraLLWA;&tRhIKOvhW|HC|L-dLvAUF(MG0(Nl?4tB{RzN7I(}Cb%hwN{crFC8 zji#aJElKvDFV+&VI1V?oUMA>*kto0^;3W8FQBSZ|{ z$v~TqE=(8DZa^i$^oht&h};P1N&wMXorKh*Z68gPV&ouy>%f36Oqkwemyeas$Qbz# zV?7Jy%o7KY6^I=P@eCji%W`o5sf(5hySYo9$l4e2`(hIV_?=H-#R6}0$WVA|*(K@3 z=5?@RlcLh(meW%A4)hGzcvEpm(_w?>zhL*i&s9$2>r zAtk{8Cia|+Y+V!uX9BtpXoF%lswuRKsM!pSs!?yhlCy!269K0|b M?FSZn2B>%I-}ej|s{jB1 literal 0 HcmV?d00001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css new file mode 100644 index 00000000000..86a3716c501 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css @@ -0,0 +1,3274 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.messageBar{ + --closing-button-icon:url(images/messageBar_closingButton.svg); + --message-bar-close-button-color:var(--text-primary-color); + --message-bar-close-button-color-hover:var(--text-primary-color); + --message-bar-close-button-border-radius:4px; + --message-bar-close-button-border:none; + --message-bar-close-button-hover-bg-color:rgb(21 20 26 / 0.14); + --message-bar-close-button-active-bg-color:rgb(21 20 26 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(21 20 26 / 0.07); +} + +@media (prefers-color-scheme: dark){ + +.messageBar{ + --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); +} + } + +@media screen and (forced-colors: active){ + +.messageBar{ + --message-bar-close-button-color:ButtonText; + --message-bar-close-button-border:1px solid ButtonText; + --message-bar-close-button-hover-bg-color:ButtonText; + --message-bar-close-button-active-bg-color:ButtonText; + --message-bar-close-button-focus-bg-color:ButtonText; + --message-bar-close-button-color-hover:HighlightText; +} + } + +.messageBar{ + + display:flex; + position:relative; + padding:8px 8px 8px 16px; + flex-direction:column; + justify-content:center; + align-items:center; + gap:8px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + + border-radius:4px; + + border:1px solid var(--message-bar-border-color); + background:var(--message-bar-bg-color); + color:var(--message-bar-fg-color); +} + +.messageBar > div{ + display:flex; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +:is(.messageBar > div)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--message-bar-icon); + mask-image:var(--message-bar-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-icon-color); + flex-shrink:0; + } + +.messageBar button{ + cursor:pointer; + } + +:is(.messageBar button):focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; + } + +.messageBar .closeButton{ + width:32px; + height:32px; + background:none; + border-radius:var(--message-bar-close-button-border-radius); + border:var(--message-bar-close-button-border); + + display:flex; + align-items:center; + justify-content:center; + } + +:is(.messageBar .closeButton)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--closing-button-icon); + mask-image:var(--closing-button-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-close-button-color); + } + +:is(.messageBar .closeButton):is(:hover,:active,:focus)::before{ + background-color:var(--message-bar-close-button-color-hover); + } + +:is(.messageBar .closeButton):hover{ + background-color:var(--message-bar-close-button-hover-bg-color); + } + +:is(.messageBar .closeButton):active{ + background-color:var(--message-bar-close-button-active-bg-color); + } + +:is(.messageBar .closeButton):focus{ + background-color:var(--message-bar-close-button-focus-bg-color); + } + +:is(.messageBar .closeButton) > span{ + display:inline-block; + width:0; + height:0; + overflow:hidden; + } + +#editorUndoBar{ + --text-primary-color:#15141a; + + --message-bar-icon:url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color:#0060df; + --message-bar-bg-color:#deeafc; + --message-bar-fg-color:var(--text-primary-color); + --message-bar-border-color:rgb(0 0 0 / 0.08); + + --undo-button-bg-color:rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover:rgb(21 20 26 / 0.14); + --undo-button-bg-color-active:rgb(21 20 26 / 0.21); + + --undo-button-fg-color:var(--message-bar-fg-color); + --undo-button-fg-color-hover:var(--undo-button-fg-color); + --undo-button-fg-color-active:var(--undo-button-fg-color); + + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); +} + +@media (prefers-color-scheme: dark){ + +#editorUndoBar{ + --text-primary-color:#fbfbfe; + + --message-bar-icon-color:#73a7f3; + --message-bar-bg-color:#003070; + --message-bar-border-color:rgb(255 255 255 / 0.08); + + --undo-button-bg-color:rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); + --undo-button-bg-color-active:rgb(255 255 255 / 0.21); +} + } + +@media screen and (forced-colors: active){ + +#editorUndoBar{ + --text-primary-color:CanvasText; + + --message-bar-icon-color:CanvasText; + --message-bar-bg-color:Canvas; + --message-bar-border-color:CanvasText; + + --undo-button-bg-color:ButtonText; + --undo-button-bg-color-hover:SelectedItem; + --undo-button-bg-color-active:SelectedItem; + + --undo-button-fg-color:ButtonFace; + --undo-button-fg-color-hover:SelectedItemText; + --undo-button-fg-color-active:SelectedItemText; + + --focus-ring-color:CanvasText; +} + } + +#editorUndoBar{ + + position:fixed; + top:50px; + left:50%; + transform:translateX(-50%); + z-index:10; + + padding-block:8px; + padding-inline:16px 8px; + + font:menu; + font-size:15px; + + cursor:default; +} + +#editorUndoBar button{ + cursor:pointer; + } + +#editorUndoBar #editorUndoBarUndoButton{ + border-radius:4px; + font-weight:590; + line-height:19.5px; + color:var(--undo-button-fg-color); + border:none; + padding:4px 16px; + margin-inline-start:8px; + height:32px; + + background-color:var(--undo-button-bg-color); + } + +:is(#editorUndoBar #editorUndoBarUndoButton):hover{ + background-color:var(--undo-button-bg-color-hover); + color:var(--undo-button-fg-color-hover); + } + +:is(#editorUndoBar #editorUndoBarUndoButton):active{ + background-color:var(--undo-button-bg-color-active); + color:var(--undo-button-fg-color-active); + } + +#editorUndoBar > div{ + align-items:center; + } + +.dialog{ + --dialog-bg-color:white; + --dialog-border-color:white; + --dialog-shadow:0 2px 14px 0 rgb(58 57 68 / 0.2); + --text-primary-color:#15141a; + --text-secondary-color:#5b5b66; + --hover-filter:brightness(0.9); + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); + --link-fg-color:#0060df; + --link-hover-fg-color:#0250bb; + --separator-color:#f0f0f4; + + --textarea-border-color:#8f8f9d; + --textarea-bg-color:white; + --textarea-fg-color:var(--text-secondary-color); + + --radio-bg-color:#f0f0f4; + --radio-checked-bg-color:#fbfbfe; + --radio-border-color:#8f8f9d; + --radio-checked-border-color:#0060df; + + --button-secondary-bg-color:#f0f0f4; + --button-secondary-fg-color:var(--text-primary-color); + --button-secondary-border-color:var(--button-secondary-bg-color); + --button-secondary-hover-bg-color:var(--button-secondary-bg-color); + --button-secondary-hover-fg-color:var(--button-secondary-fg-color); + --button-secondary-hover-border-color:var(--button-secondary-hover-bg-color); + + --button-primary-bg-color:#0060df; + --button-primary-fg-color:#fbfbfe; + --button-primary-border-color:var(--button-primary-bg-color); + --button-primary-hover-bg-color:var(--button-primary-bg-color); + --button-primary-hover-fg-color:var(--button-primary-fg-color); + --button-primary-hover-border-color:var(--button-primary-hover-bg-color); +} + +@media (prefers-color-scheme: dark){ + +.dialog{ + --dialog-bg-color:#1c1b22; + --dialog-border-color:#1c1b22; + --dialog-shadow:0 2px 14px 0 #15141a; + --text-primary-color:#fbfbfe; + --text-secondary-color:#cfcfd8; + --focus-ring-color:#0df; + --hover-filter:brightness(1.4); + --link-fg-color:#0df; + --link-hover-fg-color:#80ebff; + --separator-color:#52525e; + + --textarea-bg-color:#42414d; + + --radio-bg-color:#2b2a33; + --radio-checked-bg-color:#15141a; + --radio-checked-border-color:#0df; + + --button-secondary-bg-color:#2b2a33; + --button-primary-bg-color:#0df; + --button-primary-fg-color:#15141a; +} + } + +@media screen and (forced-colors: active){ + +.dialog{ + --dialog-bg-color:Canvas; + --dialog-border-color:CanvasText; + --dialog-shadow:none; + --text-primary-color:CanvasText; + --text-secondary-color:CanvasText; + --hover-filter:none; + --focus-ring-color:ButtonBorder; + --link-fg-color:LinkText; + --link-hover-fg-color:LinkText; + --separator-color:CanvasText; + + --textarea-border-color:ButtonBorder; + --textarea-bg-color:Field; + --textarea-fg-color:ButtonText; + + --radio-bg-color:ButtonFace; + --radio-checked-bg-color:ButtonFace; + --radio-border-color:ButtonText; + --radio-checked-border-color:ButtonText; + + --button-secondary-bg-color:ButtonFace; + --button-secondary-fg-color:ButtonText; + --button-secondary-border-color:ButtonText; + --button-secondary-hover-bg-color:AccentColor; + --button-secondary-hover-fg-color:AccentColorText; + + --button-primary-bg-color:ButtonText; + --button-primary-fg-color:ButtonFace; + --button-primary-hover-bg-color:AccentColor; + --button-primary-hover-fg-color:AccentColorText; +} + } + +.dialog{ + + font:message-box; + font-size:13px; + font-weight:400; + line-height:150%; + border-radius:4px; + padding:12px 16px; + border:1px solid var(--dialog-border-color); + background:var(--dialog-bg-color); + color:var(--text-primary-color); + box-shadow:var(--dialog-shadow); +} + +:is(.dialog .mainContainer) *:focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; + } + +:is(.dialog .mainContainer) .title{ + display:flex; + width:auto; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + } + +:is(:is(.dialog .mainContainer) .title) > span{ + font-size:13px; + font-style:normal; + font-weight:590; + line-height:150%; + } + +:is(.dialog .mainContainer) .dialogSeparator{ + width:100%; + height:0; + margin-block:4px; + border-top:1px solid var(--separator-color); + border-bottom:none; + } + +:is(.dialog .mainContainer) .dialogButtonsGroup{ + display:flex; + gap:12px; + align-self:flex-end; + } + +:is(.dialog .mainContainer) .radio{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + } + +:is(:is(.dialog .mainContainer) .radio) > .radioButton{ + display:flex; + gap:8px; + align-self:stretch; + align-items:center; + } + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + box-sizing:border-box; + width:16px; + height:16px; + border-radius:50%; + background-color:var(--radio-bg-color); + border:1px solid var(--radio-border-color); + } + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):hover{ + filter:var(--hover-filter); + } + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):checked{ + background-color:var(--radio-checked-bg-color); + border:4px solid var(--radio-checked-border-color); + } + +:is(:is(.dialog .mainContainer) .radio) > .radioLabel{ + display:flex; + padding-inline-start:24px; + align-items:flex-start; + gap:10px; + align-self:stretch; + } + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioLabel) > span{ + flex:1 0 0; + font-size:11px; + color:var(--text-secondary-color); + } + +:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton)){ + border-radius:4px; + border:1px solid; + font:menu; + font-weight:600; + padding:4px 16px; + width:auto; + height:32px; + } + +:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + cursor:pointer; + filter:var(--hover-filter); + } + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ + color:var(--button-secondary-fg-color); + background-color:var(--button-secondary-bg-color); + border-color:var(--button-secondary-border-color); + } + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + color:var(--button-secondary-hover-fg-color); + background-color:var(--button-secondary-hover-bg-color); + border-color:var(--button-secondary-hover-border-color); + } + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ + color:var(--button-primary-fg-color); + background-color:var(--button-primary-bg-color); + border-color:var(--button-primary-border-color); + opacity:1; + } + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + color:var(--button-primary-hover-fg-color); + background-color:var(--button-primary-hover-bg-color); + border-color:var(--button-primary-hover-border-color); + } + +:is(.dialog .mainContainer) a{ + color:var(--link-fg-color); + } + +:is(:is(.dialog .mainContainer) a):hover{ + color:var(--link-hover-fg-color); + } + +:is(.dialog .mainContainer) textarea{ + font:inherit; + padding:8px; + resize:none; + margin:0; + box-sizing:border-box; + border-radius:4px; + border:1px solid var(--textarea-border-color); + background:var(--textarea-bg-color); + color:var(--textarea-fg-color); + } + +:is(:is(.dialog .mainContainer) textarea):focus{ + outline-offset:0; + border-color:transparent; + } + +:is(:is(.dialog .mainContainer) textarea):disabled{ + pointer-events:none; + opacity:0.4; + } + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#ffebcd; + --message-bar-fg-color:#15141a; + --message-bar-border-color:rgb(0 0 0 / 0.08); + --message-bar-icon:url(images/messageBar_warning.svg); + --message-bar-icon-color:#cd411e; + } + +@media (prefers-color-scheme: dark){ + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#5a3100; + --message-bar-fg-color:#fbfbfe; + --message-bar-border-color:rgb(255 255 255 / 0.08); + --message-bar-icon-color:#e49c49; + } + } + +@media screen and (forced-colors: active){ + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:HighlightText; + --message-bar-fg-color:CanvasText; + --message-bar-border-color:CanvasText; + --message-bar-icon-color:CanvasText; + } + } + +:is(.dialog .mainContainer) .messageBar{ + + align-self:stretch; + } + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div)::before,:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + margin-block:4px; + } + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + flex:1 0 0; + } + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .title{ + font-size:13px; + font-weight:590; + } + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .description{ + font-size:13px; + } + +:is(.dialog .mainContainer) .toggler{ + display:flex; + align-items:center; + gap:8px; + align-self:stretch; + } + +:is(:is(.dialog .mainContainer) .toggler) > .togglerLabel{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.textLayer{ + position:absolute; + text-align:initial; + inset:0; + overflow:clip; + opacity:1; + line-height:1; + -webkit-text-size-adjust:none; + -moz-text-size-adjust:none; + text-size-adjust:none; + forced-color-adjust:none; + transform-origin:0 0; + caret-color:CanvasText; + z-index:0; +} + +.textLayer.highlighting{ + touch-action:none; + } + +.textLayer :is(span,br){ + color:transparent; + position:absolute; + white-space:pre; + cursor:text; + transform-origin:0% 0%; + } + +.textLayer > :not(.markedContent),.textLayer .markedContent span:not(.markedContent){ + z-index:1; + } + +.textLayer span.markedContent{ + top:0; + height:0; + } + +.textLayer span[role="img"]{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; + } + +.textLayer .highlight{ + --highlight-bg-color:rgb(180 0 170 / 0.25); + --highlight-selected-bg-color:rgb(0 100 0 / 0.25); + --highlight-backdrop-filter:none; + --highlight-selected-backdrop-filter:none; + } + +@media screen and (forced-colors: active){ + +.textLayer .highlight{ + --highlight-bg-color:transparent; + --highlight-selected-bg-color:transparent; + --highlight-backdrop-filter:var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter:var( + --hcm-highlight-selected-filter + ); + } + } + +.textLayer .highlight{ + + margin:-1px; + padding:1px; + background-color:var(--highlight-bg-color); + -webkit-backdrop-filter:var(--highlight-backdrop-filter); + backdrop-filter:var(--highlight-backdrop-filter); + border-radius:4px; + } + +.appended:is(.textLayer .highlight){ + position:initial; + } + +.begin:is(.textLayer .highlight){ + border-radius:4px 0 0 4px; + } + +.end:is(.textLayer .highlight){ + border-radius:0 4px 4px 0; + } + +.middle:is(.textLayer .highlight){ + border-radius:0; + } + +.selected:is(.textLayer .highlight){ + background-color:var(--highlight-selected-bg-color); + -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); + backdrop-filter:var(--highlight-selected-backdrop-filter); + } + +.textLayer ::-moz-selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); + } + +.textLayer ::selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); + } + +.textLayer br::-moz-selection{ + background:transparent; + } + +.textLayer br::selection{ + background:transparent; + } + +.textLayer .endOfContent{ + display:block; + position:absolute; + inset:100% 0 0; + z-index:0; + cursor:default; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.textLayer.selecting .endOfContent{ + top:0; + } + +.annotationLayer{ + --annotation-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color:Highlight; + --input-focus-outline:1px solid Canvas; + --input-unfocused-border-color:transparent; + --input-disabled-border-color:transparent; + --input-hover-border-color:black; + --link-outline:none; +} + +@media screen and (forced-colors: active){ + +.annotationLayer{ + --input-focus-border-color:CanvasText; + --input-unfocused-border-color:ActiveText; + --input-disabled-border-color:GrayText; + --input-hover-border-color:Highlight; + --link-outline:1.5px solid LinkText; +} + + .annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid selectedItem; + } + + .annotationLayer .linkAnnotation{ + outline:var(--link-outline); + } + + :is(.annotationLayer .linkAnnotation):hover{ + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + } + + :is(.annotationLayer .linkAnnotation) > a:hover{ + opacity:0 !important; + background:none !important; + box-shadow:none; + } + + .annotationLayer .popupAnnotation .popup{ + outline:calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color:ButtonFace !important; + color:ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + content:""; + pointer-events:none; + } + + .annotationLayer .popupAnnotation.focused .popup{ + outline:calc(3px * var(--scale-factor)) solid Highlight !important; + } + } + +.annotationLayer{ + + position:absolute; + top:0; + left:0; + pointer-events:none; + transform-origin:0 0; +} + +.annotationLayer[data-main-rotation="90"] .norotate{ + transform:rotate(270deg) translateX(-100%); + } + +.annotationLayer[data-main-rotation="180"] .norotate{ + transform:rotate(180deg) translate(-100%, -100%); + } + +.annotationLayer[data-main-rotation="270"] .norotate{ + transform:rotate(90deg) translateY(-100%); + } + +.annotationLayer.disabled section,.annotationLayer.disabled .popup{ + pointer-events:none; + } + +.annotationLayer .annotationContent{ + position:absolute; + width:100%; + height:100%; + pointer-events:none; + } + +.freetext:is(.annotationLayer .annotationContent){ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:1.35; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.annotationLayer section{ + position:absolute; + text-align:initial; + pointer-events:auto; + box-sizing:border-box; + transform-origin:0 0; + } + +:is(.annotationLayer section):has(div.annotationContent) canvas.annotationContent{ + display:none; + } + +.textLayer.selecting ~ .annotationLayer section{ + pointer-events:none; + } + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton) > a{ + position:absolute; + font-size:1em; + top:0; + left:0; + width:100%; + height:100%; + } + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover{ + opacity:0.2; + background-color:rgb(255 255 0); + box-shadow:0 2px 10px rgb(255 255 0); + } + +.annotationLayer .linkAnnotation.hasBorder:hover{ + background-color:rgb(255 255 0 / 0.2); + } + +.annotationLayer .hasBorder{ + background-size:100% 100%; + } + +.annotationLayer .textAnnotation img{ + position:absolute; + cursor:pointer; + width:100%; + height:100%; + top:0; + left:0; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea),.annotationLayer .choiceWidgetAnnotation select,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + background-image:var(--annotation-unfocused-field-background); + border:2px solid var(--input-unfocused-border-color); + box-sizing:border-box; + font:calc(9px * var(--scale-factor)) sans-serif; + height:100%; + margin:0; + vertical-align:top; + width:100%; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid red; + } + +.annotationLayer .choiceWidgetAnnotation select option{ + padding:0; + } + +.annotationLayer .buttonWidgetAnnotation.radioButton input{ + border-radius:50%; + } + +.annotationLayer .textWidgetAnnotation textarea{ + resize:none; + } + +.annotationLayer .textWidgetAnnotation [disabled]:is(input,textarea),.annotationLayer .choiceWidgetAnnotation select[disabled],.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input[disabled]{ + background:none; + border:2px solid var(--input-disabled-border-color); + cursor:not-allowed; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:hover{ + border:2px solid var(--input-hover-border-color); + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation.checkBox input:hover{ + border-radius:2px; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):focus,.annotationLayer .choiceWidgetAnnotation select:focus{ + background:none; + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) :focus{ + background-image:none; + background-color:transparent; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox :focus{ + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation.radioButton :focus{ + border:2px solid var(--input-focus-border-color); + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after,.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + background-color:CanvasText; + content:""; + display:block; + position:absolute; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + height:80%; + left:45%; + width:1px; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before{ + transform:rotate(45deg); + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + transform:rotate(-45deg); + } + +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + border-radius:50%; + height:50%; + left:25%; + top:25%; + width:50%; + } + +.annotationLayer .textWidgetAnnotation input.comb{ + font-family:monospace; + padding-left:2px; + padding-right:0; + } + +.annotationLayer .textWidgetAnnotation input.comb:focus{ + width:103%; + } + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + } + +.annotationLayer .fileAttachmentAnnotation .popupTriggerArea{ + height:100%; + width:100%; + } + +.annotationLayer .popupAnnotation{ + position:absolute; + font-size:calc(9px * var(--scale-factor)); + pointer-events:none; + width:-moz-max-content; + width:max-content; + max-width:45%; + height:auto; + } + +.annotationLayer .popup{ + background-color:rgb(255 255 153); + box-shadow:0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) rgb(136 136 136); + border-radius:calc(2px * var(--scale-factor)); + outline:1.5px solid rgb(255 255 74); + padding:calc(6px * var(--scale-factor)); + cursor:pointer; + font:message-box; + white-space:normal; + word-wrap:break-word; + pointer-events:auto; + } + +.annotationLayer .popupAnnotation.focused .popup{ + outline-width:3px; + } + +.annotationLayer .popup *{ + font-size:calc(9px * var(--scale-factor)); + } + +.annotationLayer .popup > .header{ + display:inline-block; + } + +.annotationLayer .popup > .header h1{ + display:inline; + } + +.annotationLayer .popup > .header .popupDate{ + display:inline-block; + margin-left:calc(5px * var(--scale-factor)); + width:-moz-fit-content; + width:fit-content; + } + +.annotationLayer .popupContent{ + border-top:1px solid rgb(51 51 51); + margin-top:calc(2px * var(--scale-factor)); + padding-top:calc(2px * var(--scale-factor)); + } + +.annotationLayer .richText > *{ + white-space:pre-wrap; + font-size:calc(9px * var(--scale-factor)); + } + +.annotationLayer .popupTriggerArea{ + cursor:pointer; + } + +.annotationLayer section svg{ + position:absolute; + width:100%; + height:100%; + top:0; + left:0; + } + +.annotationLayer .annotationTextContent{ + position:absolute; + width:100%; + height:100%; + opacity:0; + color:transparent; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + pointer-events:none; + } + +:is(.annotationLayer .annotationTextContent) span{ + width:100%; + display:inline-block; + } + +.annotationLayer svg.quadrilateralsContainer{ + contain:strict; + width:0; + height:0; + position:absolute; + top:0; + left:0; + z-index:-1; + } + +:root{ + --xfa-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --xfa-focus-outline:auto; +} + +@media screen and (forced-colors: active){ + :root{ + --xfa-focus-outline:2px solid CanvasText; + } + .xfaLayer *:required{ + outline:1.5px solid selectedItem; + } +} + +.xfaLayer{ + background-color:transparent; +} + +.xfaLayer .highlight{ + margin:-1px; + padding:1px; + background-color:rgb(239 203 237); + border-radius:4px; +} + +.xfaLayer .highlight.appended{ + position:initial; +} + +.xfaLayer .highlight.begin{ + border-radius:4px 0 0 4px; +} + +.xfaLayer .highlight.end{ + border-radius:0 4px 4px 0; +} + +.xfaLayer .highlight.middle{ + border-radius:0; +} + +.xfaLayer .highlight.selected{ + background-color:rgb(203 223 203); +} + +.xfaPage{ + overflow:hidden; + position:relative; +} + +.xfaContentarea{ + position:absolute; +} + +.xfaPrintOnly{ + display:none; +} + +.xfaLayer{ + position:absolute; + text-align:initial; + top:0; + left:0; + transform-origin:0 0; + line-height:1.2; +} + +.xfaLayer *{ + color:inherit; + font:inherit; + font-style:inherit; + font-weight:inherit; + font-kerning:inherit; + letter-spacing:-0.01px; + text-align:inherit; + text-decoration:inherit; + box-sizing:border-box; + background-color:transparent; + padding:0; + margin:0; + pointer-events:auto; + line-height:inherit; +} + +.xfaLayer *:required{ + outline:1.5px solid red; +} + +.xfaLayer div, +.xfaLayer svg, +.xfaLayer svg *{ + pointer-events:none; +} + +.xfaLayer a{ + color:blue; +} + +.xfaRich li{ + margin-left:3em; +} + +.xfaFont{ + color:black; + font-weight:normal; + font-kerning:none; + font-size:10px; + font-style:normal; + letter-spacing:0; + text-decoration:none; + vertical-align:0; +} + +.xfaCaption{ + overflow:hidden; + flex:0 0 auto; +} + +.xfaCaptionForCheckButton{ + overflow:hidden; + flex:1 1 auto; +} + +.xfaLabel{ + height:100%; + width:100%; +} + +.xfaLeft{ + display:flex; + flex-direction:row; + align-items:center; +} + +.xfaRight{ + display:flex; + flex-direction:row-reverse; + align-items:center; +} + +:is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + max-height:100%; +} + +.xfaTop{ + display:flex; + flex-direction:column; + align-items:flex-start; +} + +.xfaBottom{ + display:flex; + flex-direction:column-reverse; + align-items:flex-start; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + width:100%; +} + +.xfaBorder{ + background-color:transparent; + position:absolute; + pointer-events:none; +} + +.xfaWrapped{ + width:100%; + height:100%; +} + +:is(.xfaTextfield, .xfaSelect):focus{ + background-image:none; + background-color:transparent; + outline:var(--xfa-focus-outline); + outline-offset:-1px; +} + +:is(.xfaCheckbox, .xfaRadio):focus{ + outline:var(--xfa-focus-outline); +} + +.xfaTextfield, +.xfaSelect{ + height:100%; + width:100%; + flex:1 1 auto; + border:none; + resize:none; + background-image:var(--xfa-unfocused-field-background); +} + +.xfaSelect{ + padding-inline:2px; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect){ + flex:0 1 auto; +} + +.xfaButton{ + cursor:pointer; + width:100%; + height:100%; + border:none; + text-align:center; +} + +.xfaLink{ + width:100%; + height:100%; + position:absolute; + top:0; + left:0; +} + +.xfaCheckbox, +.xfaRadio{ + width:100%; + height:100%; + flex:0 0 auto; + border:none; +} + +.xfaRich{ + white-space:pre-wrap; + width:100%; + height:100%; +} + +.xfaImage{ + -o-object-position:left top; + object-position:left top; + -o-object-fit:contain; + object-fit:contain; + width:100%; + height:100%; +} + +.xfaLrTb, +.xfaRlTb, +.xfaTb{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaLr{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaRl{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; +} + +.xfaTb > div{ + justify-content:left; +} + +.xfaPosition{ + position:relative; +} + +.xfaArea{ + position:relative; +} + +.xfaValignMiddle{ + display:flex; + align-items:center; +} + +.xfaTable{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaTable .xfaRow{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaTable .xfaRlRow{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; + flex:1; +} + +.xfaTable .xfaRlRow > div{ + flex:1; +} + +:is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea){ + background:initial; +} + +@media print{ + .xfaTextfield, + .xfaSelect{ + background:transparent; + } + + .xfaSelect{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + text-indent:1px; + text-overflow:""; + } +} + +.canvasWrapper svg{ + transform:none; + } + +.moving:is(.canvasWrapper svg){ + z-index:100000; + } + +[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, 1, -1, 0, 1, 0); + } + +[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(-1, 0, 0, -1, 1, 1); + } + +[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, -1, 1, 0, 0, 1); + } + +.draw:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; + } + +.draw[data-draw-rotation="90"]:is(.canvasWrapper svg){ + transform:rotate(90deg); + } + +.draw[data-draw-rotation="180"]:is(.canvasWrapper svg){ + transform:rotate(180deg); + } + +.draw[data-draw-rotation="270"]:is(.canvasWrapper svg){ + transform:rotate(270deg); + } + +.highlight:is(.canvasWrapper svg){ + --blend-mode:multiply; + } + +@media screen and (forced-colors: active){ + +.highlight:is(.canvasWrapper svg){ + --blend-mode:difference; + } + } + +.highlight:is(.canvasWrapper svg){ + + position:absolute; + mix-blend-mode:var(--blend-mode); + } + +.highlight:is(.canvasWrapper svg):not(.free){ + fill-rule:evenodd; + } + +.highlightOutline:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; + fill-rule:evenodd; + fill:none; + } + +.highlightOutline.hovered:is(.canvasWrapper svg):not(.free):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:var(--outline-width); + } + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + var(--outline-width) + 2 * var(--outline-around-width) + ); + } + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:var(--outline-width); + } + +.highlightOutline.free.hovered:is(.canvasWrapper svg):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:calc(2 * var(--outline-width)); + } + +.highlightOutline.free.selected:is(.canvasWrapper svg) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + 2 * (var(--outline-width) + var(--outline-around-width)) + ); + } + +.highlightOutline.free.selected:is(.canvasWrapper svg) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:calc(2 * var(--outline-width)); + } + +.toggle-button{ + --button-background-color:#f0f0f4; + --button-background-color-hover:#e0e0e6; + --button-background-color-active:#cfcfd8; + --color-accent-primary:#0060df; + --color-accent-primary-hover:#0250bb; + --color-accent-primary-active:#054096; + --border-interactive-color:#8f8f9d; + --border-radius-circle:9999px; + --border-width:1px; + --size-item-small:16px; + --size-item-large:32px; + --color-canvas:white; +} + +@media (prefers-color-scheme: dark){ + +.toggle-button{ + --button-background-color:color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover:color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active:color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary:#0df; + --color-accent-primary-hover:#80ebff; + --color-accent-primary-active:#aaf2ff; + --border-interactive-color:#bfbfc9; + --color-canvas:#1c1b22; +} + } + +@media (forced-colors: active){ + +.toggle-button{ + --color-accent-primary:ButtonText; + --color-accent-primary-hover:SelectedItem; + --color-accent-primary-active:SelectedItem; + --border-interactive-color:ButtonText; + --button-background-color:ButtonFace; + --border-interactive-color-hover:SelectedItem; + --border-interactive-color-active:SelectedItem; + --border-interactive-color-disabled:GrayText; + --color-canvas:ButtonText; +} + } + +.toggle-button{ + + --toggle-background-color:var(--button-background-color); + --toggle-background-color-hover:var(--button-background-color-hover); + --toggle-background-color-active:var(--button-background-color-active); + --toggle-background-color-pressed:var(--color-accent-primary); + --toggle-background-color-pressed-hover:var(--color-accent-primary-hover); + --toggle-background-color-pressed-active:var(--color-accent-primary-active); + --toggle-border-color:var(--border-interactive-color); + --toggle-border-color-hover:var(--toggle-border-color); + --toggle-border-color-active:var(--toggle-border-color); + --toggle-border-radius:var(--border-radius-circle); + --toggle-border-width:var(--border-width); + --toggle-height:var(--size-item-small); + --toggle-width:var(--size-item-large); + --toggle-dot-background-color:var(--toggle-border-color); + --toggle-dot-background-color-hover:var(--toggle-dot-background-color); + --toggle-dot-background-color-active:var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed:var(--color-canvas); + --toggle-dot-margin:1px; + --toggle-dot-height:calc( + var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * + var(--toggle-border-width) + ); + --toggle-dot-width:var(--toggle-dot-height); + --toggle-dot-transform-x:calc( + var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) + ); + + -webkit-appearance:none; + + -moz-appearance:none; + + appearance:none; + padding:0; + margin:0; + border:var(--toggle-border-width) solid var(--toggle-border-color); + height:var(--toggle-height); + width:var(--toggle-width); + border-radius:var(--toggle-border-radius); + background:var(--toggle-background-color); + box-sizing:border-box; + flex-shrink:0; +} + +.toggle-button:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); + } + +.toggle-button:enabled:hover{ + background:var(--toggle-background-color-hover); + border-color:var(--toggle-border-color); + } + +.toggle-button:enabled:active{ + background:var(--toggle-background-color-active); + border-color:var(--toggle-border-color); + } + +.toggle-button[aria-pressed="true"]{ + background:var(--toggle-background-color-pressed); + border-color:transparent; + } + +.toggle-button[aria-pressed="true"]:enabled:hover{ + background:var(--toggle-background-color-pressed-hover); + border-color:transparent; + } + +.toggle-button[aria-pressed="true"]:enabled:active{ + background:var(--toggle-background-color-pressed-active); + border-color:transparent; + } + +.toggle-button::before{ + display:block; + content:""; + background-color:var(--toggle-dot-background-color); + height:var(--toggle-dot-height); + width:var(--toggle-dot-width); + margin:var(--toggle-dot-margin); + border-radius:var(--toggle-border-radius); + translate:0; + } + +.toggle-button[aria-pressed="true"]::before{ + translate:var(--toggle-dot-transform-x); + background-color:var(--toggle-dot-background-color-on-pressed); + } + +.toggle-button[aria-pressed="true"]:enabled:hover::before,.toggle-button[aria-pressed="true"]:enabled:active::before{ + background-color:var(--toggle-dot-background-color-on-pressed); + } + +[dir="rtl"] .toggle-button[aria-pressed="true"]::before{ + translate:calc(-1 * var(--toggle-dot-transform-x)); + } + +@media (prefers-reduced-motion: no-preference){ + .toggle-button::before{ + transition:translate 100ms; + } + } + +@media (prefers-contrast){ + .toggle-button:enabled:hover{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button:enabled:active{ + border-color:var(--toggle-border-color-active); + } + + .toggle-button[aria-pressed="true"]:enabled{ + border-color:var(--toggle-border-color); + position:relative; + } + + .toggle-button[aria-pressed="true"]:enabled:hover,.toggle-button[aria-pressed="true"]:enabled:hover:active{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button[aria-pressed="true"]:enabled:active{ + background-color:var(--toggle-dot-background-color-active); + border-color:var(--toggle-dot-background-color-hover); + } + + .toggle-button:hover::before,.toggle-button:active::before{ + background-color:var(--toggle-dot-background-color-hover); + } + } + +@media (forced-colors){ + +.toggle-button{ + --toggle-dot-background-color:var(--color-accent-primary); + --toggle-dot-background-color-hover:var(--color-accent-primary-hover); + --toggle-dot-background-color-active:var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed:var(--button-background-color); + --toggle-background-color-disabled:var(--button-background-color-disabled); + --toggle-border-color-hover:var(--border-interactive-color-hover); + --toggle-border-color-active:var(--border-interactive-color-active); + --toggle-border-color-disabled:var(--border-interactive-color-disabled); +} + + .toggle-button[aria-pressed="true"]:enabled::after{ + border:1px solid var(--button-background-color); + content:""; + position:absolute; + height:var(--toggle-height); + width:var(--toggle-width); + display:block; + border-radius:var(--toggle-border-radius); + inset:-2px; + } + + .toggle-button[aria-pressed="true"]:enabled:active::after{ + border-color:var(--toggle-border-color-active); + } + } + +:root{ + --outline-width:2px; + --outline-color:#0060df; + --outline-around-width:1px; + --outline-around-color:#f0f0f4; + --hover-outline-around-color:var(--outline-around-color); + --focus-outline:solid var(--outline-width) var(--outline-color); + --unfocus-outline:solid var(--outline-width) transparent; + --focus-outline-around:solid var(--outline-around-width) var(--outline-around-color); + --hover-outline-color:#8f8f9d; + --hover-outline:solid var(--outline-width) var(--hover-outline-color); + --hover-outline-around:solid var(--outline-around-width) var(--hover-outline-around-color); + --freetext-line-height:1.35; + --freetext-padding:2px; + --resizer-bg-color:var(--outline-color); + --resizer-size:6px; + --resizer-shift:calc( + 0px - (var(--outline-width) + var(--resizer-size)) / 2 - + var(--outline-around-width) + ); + --editorFreeText-editing-cursor:text; + --editorInk-editing-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; + --editorHighlight-editing-cursor:url(images/cursor-editorTextHighlight.svg) 24 24, text; + --editorFreeHighlight-editing-cursor:url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; + + --new-alt-text-warning-image:url(images/altText_warning.svg); +} +.visuallyHidden{ + position:absolute; + top:0; + left:0; + border:0; + margin:0; + padding:0; + width:0; + height:0; + overflow:hidden; + white-space:nowrap; + font-size:0; +} + +.textLayer.highlighting{ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +.textLayer.highlighting:not(.free) span{ + cursor:var(--editorHighlight-editing-cursor); + } + +[role="img"]:is(.textLayer.highlighting:not(.free) span){ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +.textLayer.highlighting.free span{ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +:is(#viewerContainer.pdfPresentationMode:fullscreen,.annotationEditorLayer.disabled) .noAltTextBadge{ + display:none !important; + } + +@media (min-resolution: 1.1dppx){ + :root{ + --editorFreeText-editing-cursor:url(images/cursor-editorFreeText.svg) 0 16, text; + } +} + +@media screen and (forced-colors: active){ + :root{ + --outline-color:CanvasText; + --outline-around-color:ButtonFace; + --resizer-bg-color:ButtonText; + --hover-outline-color:Highlight; + --hover-outline-around-color:SelectedItemText; + } +} + +[data-editor-rotation="90"]{ + transform:rotate(90deg); +} + +[data-editor-rotation="180"]{ + transform:rotate(180deg); +} + +[data-editor-rotation="270"]{ + transform:rotate(270deg); +} + +.annotationEditorLayer{ + background:transparent; + position:absolute; + inset:0; + font-size:calc(100px * var(--scale-factor)); + transform-origin:0 0; + cursor:auto; +} + +.annotationEditorLayer .selectedEditor{ + z-index:100000 !important; + } + +.annotationEditorLayer.drawing *{ + pointer-events:none !important; + } + +.annotationEditorLayer.waiting{ + content:""; + cursor:wait; + position:absolute; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer.disabled{ + pointer-events:none; +} + +.annotationEditorLayer.freetextEditing{ + cursor:var(--editorFreeText-editing-cursor); +} + +.annotationEditorLayer.inkEditing{ + cursor:var(--editorInk-editing-cursor); +} + +.annotationEditorLayer .draw{ + box-sizing:border-box; +} + +.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor){ + position:absolute; + background:transparent; + z-index:1; + transform-origin:0 0; + cursor:auto; + max-width:100%; + max-height:100%; + border:var(--unfocus-outline); +} + +.draggable.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ + cursor:move; + } + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ + border:var(--focus-outline); + outline:var(--focus-outline-around); + } + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor))::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + pointer-events:none; + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor){ + border:var(--hover-outline); + outline:var(--hover-outline-around); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor)::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-delete-image:url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color:#f0f0f4; + --editor-toolbar-highlight-image:url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-fg-color:#2e2e56; + --editor-toolbar-border-color:#8f8f9d; + --editor-toolbar-hover-border-color:var(--editor-toolbar-border-color); + --editor-toolbar-hover-bg-color:#e0e0e6; + --editor-toolbar-hover-fg-color:var(--editor-toolbar-fg-color); + --editor-toolbar-hover-outline:none; + --editor-toolbar-focus-outline-color:#0060df; + --editor-toolbar-shadow:0 2px 6px 0 rgb(58 57 68 / 0.2); + --editor-toolbar-vert-offset:6px; + --editor-toolbar-height:28px; + --editor-toolbar-padding:2px; + --alt-text-done-color:#2ac3a2; + --alt-text-warning-color:#0090ed; + --alt-text-hover-done-color:var(--alt-text-done-color); + --alt-text-hover-warning-color:var(--alt-text-warning-color); + } + +@media (prefers-color-scheme: dark){ + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:#2b2a33; + --editor-toolbar-fg-color:#fbfbfe; + --editor-toolbar-hover-bg-color:#52525e; + --editor-toolbar-focus-outline-color:#0df; + --alt-text-done-color:#54ffbd; + --alt-text-warning-color:#80ebff; + } + } + +@media screen and (forced-colors: active){ + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:ButtonFace; + --editor-toolbar-fg-color:ButtonText; + --editor-toolbar-border-color:ButtonText; + --editor-toolbar-hover-border-color:AccentColor; + --editor-toolbar-hover-bg-color:ButtonFace; + --editor-toolbar-hover-fg-color:AccentColor; + --editor-toolbar-hover-outline:2px solid var(--editor-toolbar-hover-border-color); + --editor-toolbar-focus-outline-color:ButtonBorder; + --editor-toolbar-shadow:none; + --alt-text-done-color:var(--editor-toolbar-fg-color); + --alt-text-warning-color:var(--editor-toolbar-fg-color); + --alt-text-hover-done-color:var(--editor-toolbar-hover-fg-color); + --alt-text-hover-warning-color:var(--editor-toolbar-hover-fg-color); + } + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + + display:flex; + width:-moz-fit-content; + width:fit-content; + height:var(--editor-toolbar-height); + flex-direction:column; + justify-content:center; + align-items:center; + cursor:default; + pointer-events:auto; + box-sizing:content-box; + padding:var(--editor-toolbar-padding); + + position:absolute; + inset-inline-end:0; + inset-block-start:calc(100% + var(--editor-toolbar-vert-offset)); + + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + } + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + display:none; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar):has(:focus-visible){ + border-color:transparent; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + transform-origin:100% 0; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + transform-origin:0 0; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons{ + display:flex; + justify-content:center; + align-items:center; + gap:0; + height:100%; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) button{ + padding:0; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .divider{ + width:0; + height:calc( + 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) + ); + border-left:1px solid var(--editor-toolbar-border-color); + border-right:none; + display:inline-block; + margin-inline:2px; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton{ + width:var(--editor-toolbar-height); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-highlight-image); + mask-image:var(--editor-toolbar-highlight-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete{ + width:var(--editor-toolbar-height); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-delete-image); + mask-image:var(--editor-toolbar-delete-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > *{ + height:var(--editor-toolbar-height); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider){ + border:none; + background-color:transparent; + cursor:pointer; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover{ + border-radius:2px; + background-color:var(--editor-toolbar-hover-bg-color); + color:var(--editor-toolbar-hover-fg-color); + outline:var(--editor-toolbar-hover-outline); + outline-offset:1px; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover:active{ + outline:none; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):focus-visible{ + border-radius:2px; + outline:2px solid var(--editor-toolbar-focus-outline-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText{ + --alt-text-add-image:url(images/altText_add.svg); + --alt-text-done-image:url(images/altText_done.svg); + + display:flex; + align-items:center; + justify-content:center; + width:-moz-max-content; + width:max-content; + padding-inline:8px; + pointer-events:all; + font:menu; + font-weight:590; + font-size:12px; + color:var(--editor-toolbar-fg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):disabled{ + pointer-events:none; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + content:""; + -webkit-mask-image:var(--alt-text-add-image); + mask-image:var(--alt-text-add-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + width:12px; + height:13px; + background-color:var(--editor-toolbar-fg-color); + margin-inline-end:4px; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); + } + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + background-color:var(--alt-text-warning-color); + -webkit-mask-size:cover; + mask-size:cover; + } + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-warning-color); + } + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); + background-color:var(--alt-text-done-color); + } + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-done-color); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip{ + display:none; + word-wrap:anywhere; + } + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#f0f0f4; + --alt-text-tooltip-fg:#15141a; + --alt-text-tooltip-border:#8f8f9d; + --alt-text-tooltip-shadow:0px 2px 6px 0px rgb(58 57 68 / 0.2); + } + +@media (prefers-color-scheme: dark){ + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#1c1b22; + --alt-text-tooltip-fg:#fbfbfe; + --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; + } + } + +@media screen and (forced-colors: active){ + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:Canvas; + --alt-text-tooltip-fg:CanvasText; + --alt-text-tooltip-border:CanvasText; + --alt-text-tooltip-shadow:none; + } + } + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + + display:inline-flex; + flex-direction:column; + align-items:center; + justify-content:center; + position:absolute; + top:calc(100% + 2px); + inset-inline-start:0; + padding-block:2px 3px; + padding-inline:3px; + max-width:300px; + width:-moz-max-content; + width:max-content; + height:auto; + font-size:12px; + + border:0.5px solid var(--alt-text-tooltip-border); + background:var(--alt-text-tooltip-bg); + box-shadow:var(--alt-text-tooltip-shadow); + color:var(--alt-text-tooltip-fg); + + pointer-events:none; + } + +.annotationEditorLayer .freeTextEditor{ + padding:calc(var(--freetext-padding) * var(--scale-factor)); + width:auto; + height:auto; + touch-action:none; +} + +.annotationEditorLayer .freeTextEditor .internal{ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:var(--freetext-line-height); + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.annotationEditorLayer .freeTextEditor .overlay{ + position:absolute; + display:none; + background:transparent; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer freeTextEditor .overlay.enabled{ + display:block; +} + +.annotationEditorLayer .freeTextEditor .internal:empty::before{ + content:attr(default-content); + color:gray; +} + +.annotationEditorLayer .freeTextEditor .internal:focus{ + outline:none; + -webkit-user-select:auto; + -moz-user-select:auto; + user-select:auto; +} + +.annotationEditorLayer .inkEditor{ + width:100%; + height:100%; +} + +.annotationEditorLayer .inkEditor.editing{ + cursor:inherit; +} + +.annotationEditorLayer .inkEditor .inkEditorCanvas{ + position:absolute; + inset:0; + width:100%; + height:100%; + touch-action:none; +} + +.annotationEditorLayer .stampEditor{ + width:auto; + height:auto; +} + +:is(.annotationEditorLayer .stampEditor) canvas{ + position:absolute; + width:100%; + height:100%; + margin:0; + top:0; + left:0; + } + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#f0f0f4; + --no-alt-text-badge-bg-color:#cfcfd8; + --no-alt-text-badge-fg-color:#5b5b66; + } + +@media (prefers-color-scheme: dark){ + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#52525e; + --no-alt-text-badge-bg-color:#fbfbfe; + --no-alt-text-badge-fg-color:#15141a; + } + } + +@media screen and (forced-colors: active){ + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:ButtonText; + --no-alt-text-badge-bg-color:ButtonFace; + --no-alt-text-badge-fg-color:ButtonText; + } + } + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + + position:absolute; + inset-inline-end:5px; + inset-block-end:5px; + display:inline-flex; + width:32px; + height:32px; + padding:3px; + justify-content:center; + align-items:center; + pointer-events:none; + z-index:1; + + border-radius:2px; + border:1px solid var(--no-alt-text-badge-border-color); + background:var(--no-alt-text-badge-bg-color); + } + +:is(:is(.annotationEditorLayer .stampEditor) .noAltTextBadge)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--no-alt-text-badge-fg-color); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers{ + position:absolute; + inset:0; + } + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers){ + display:none; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer{ + width:var(--resizer-size); + height:var(--resizer-size); + background:content-box var(--resizer-bg-color); + border:var(--focus-outline-around); + border-radius:2px; + position:absolute; + } + +.topLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:var(--resizer-shift); + } + +.topMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); + } + +.topRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + right:var(--resizer-shift); + } + +.middleRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + right:var(--resizer-shift); + } + +.bottomRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + right:var(--resizer-shift); + } + +.bottomMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); + } + +.bottomLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:var(--resizer-shift); + } + +.middleLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + left:var(--resizer-shift); + } + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nwse-resize; + } + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ns-resize; + } + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nesw-resize; + } + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ew-resize; + } + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nesw-resize; + } + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ew-resize; + } + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nwse-resize; + } + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ns-resize; + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar{ + rotate:270deg; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="180"],[data-main-rotation="90"] [data-editor-rotation="90"],[data-main-rotation="180"] [data-editor-rotation="0"],[data-main-rotation="270"] [data-editor-rotation="270"])) .editToolbar{ + rotate:180deg; + inset-inline-end:100%; + inset-block-start:calc(0pc - var(--editor-toolbar-vert-offset)); + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar{ + rotate:90deg; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:100%; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-start:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +.dialog.altText::backdrop{ + -webkit-mask:url(#alttext-manager-mask); + mask:url(#alttext-manager-mask); + } + +.dialog.altText.positioned{ + margin:0; + } + +.dialog.altText #altTextContainer{ + width:300px; + height:-moz-fit-content; + height:fit-content; + display:inline-flex; + flex-direction:column; + align-items:flex-start; + gap:16px; + } + +:is(.dialog.altText #altTextContainer) #overallDescription{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + align-self:stretch; + } + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) span{ + align-self:stretch; + } + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) .title{ + font-size:13px; + font-style:normal; + font-weight:590; + } + +:is(.dialog.altText #altTextContainer) #addDescription{ + display:flex; + flex-direction:column; + align-items:stretch; + gap:8px; + } + +:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea{ + flex:1; + padding-inline:24px 10px; + } + +:is(:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea) textarea{ + width:100%; + min-height:75px; + } + +:is(.dialog.altText #altTextContainer) #buttons{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +.dialog.newAltText{ + --new-alt-text-ai-disclaimer-icon:url(images/altText_disclaimer.svg); + --new-alt-text-spinner-icon:url(images/altText_spinner.svg); + --preview-image-bg-color:#f0f0f4; + --preview-image-border:none; +} + +@media (prefers-color-scheme: dark){ + +.dialog.newAltText{ + --preview-image-bg-color:#2b2a33; +} + } + +@media screen and (forced-colors: active){ + +.dialog.newAltText{ + --preview-image-bg-color:ButtonFace; + --preview-image-border:1px solid ButtonText; +} + } + +.dialog.newAltText{ + + width:80%; + max-width:570px; + min-width:300px; + padding:0; +} + +.dialog.newAltText.noAi #newAltTextDisclaimer,.dialog.newAltText.noAi #newAltTextCreateAutomatically{ + display:none !important; + } + +.dialog.newAltText.aiInstalling #newAltTextCreateAutomatically{ + display:none !important; + } + +.dialog.newAltText.aiInstalling #newAltTextDownloadModel{ + display:flex !important; + } + +.dialog.newAltText.error #newAltTextNotNow{ + display:none !important; + } + +.dialog.newAltText.error #newAltTextCancel{ + display:inline-block !important; + } + +.dialog.newAltText:not(.error) #newAltTextError{ + display:none !important; + } + +.dialog.newAltText #newAltTextContainer{ + display:flex; + width:auto; + padding:16px; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + flex:0 1 auto; + line-height:normal; + } + +:is(.dialog.newAltText #newAltTextContainer) #mainContent{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + align-self:stretch; + flex:1 1 auto; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionAndSettings{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:16px; + flex:1 0 0; + align-self:stretch; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; + flex:1 1 auto; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer{ + width:100%; + height:70px; + position:relative; + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea{ + width:100%; + height:100%; + padding:8px; + } + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::-moz-placeholder{ + color:var(--text-secondary-color); + } + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::placeholder{ + color:var(--text-secondary-color); + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:none; + position:absolute; + width:16px; + height:16px; + inset-inline-start:8px; + inset-block-start:8px; + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + pointer-events:none; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::-moz-placeholder{ + color:transparent; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::placeholder{ + color:transparent; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:inline-block; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescription{ + font-size:11px; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer{ + display:flex; + flex-direction:row; + align-items:flex-start; + gap:4px; + font-size:11px; + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer)::before{ + content:""; + display:inline-block; + width:17px; + height:16px; + -webkit-mask-image:var(--new-alt-text-ai-disclaimer-icon); + mask-image:var(--new-alt-text-ai-disclaimer-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + flex:1 0 auto; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel{ + display:flex; + align-items:center; + gap:4px; + align-self:stretch; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview{ + width:180px; + aspect-ratio:1; + display:flex; + justify-content:center; + align-items:center; + flex:0 0 auto; + background-color:var(--preview-image-bg-color); + border:var(--preview-image-border); + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview) > canvas{ + max-width:100%; + max-height:100%; + } + +.colorPicker{ + --hover-outline-color:#0250bb; + --selected-outline-color:#0060df; + --swatch-border-color:#cfcfd8; +} + +@media (prefers-color-scheme: dark){ + +.colorPicker{ + --hover-outline-color:#80ebff; + --selected-outline-color:#aaf2ff; + --swatch-border-color:#52525e; +} + } + +@media screen and (forced-colors: active){ + +.colorPicker{ + --hover-outline-color:Highlight; + --selected-outline-color:var(--hover-outline-color); + --swatch-border-color:ButtonText; +} + } + +.colorPicker .swatch{ + width:16px; + height:16px; + border:1px solid var(--swatch-border-color); + border-radius:100%; + outline-offset:2px; + box-sizing:border-box; + forced-color-adjust:none; + } + +.colorPicker button:is(:hover,.selected) > .swatch{ + border:none; + } + +.annotationEditorLayer[data-main-rotation="0"] .highlightEditor:not(.free) > .editToolbar{ + rotate:0deg; + } + +.annotationEditorLayer[data-main-rotation="90"] .highlightEditor:not(.free) > .editToolbar{ + rotate:270deg; + } + +.annotationEditorLayer[data-main-rotation="180"] .highlightEditor:not(.free) > .editToolbar{ + rotate:180deg; + } + +.annotationEditorLayer[data-main-rotation="270"] .highlightEditor:not(.free) > .editToolbar{ + rotate:90deg; + } + +.annotationEditorLayer .highlightEditor{ + position:absolute; + background:transparent; + z-index:1; + cursor:auto; + max-width:100%; + max-height:100%; + border:none; + outline:none; + pointer-events:none; + transform-origin:0 0; + } + +:is(.annotationEditorLayer .highlightEditor):not(.free){ + transform:none; + } + +:is(.annotationEditorLayer .highlightEditor) .internal{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + pointer-events:auto; + } + +.disabled:is(.annotationEditorLayer .highlightEditor) .internal{ + pointer-events:none; + } + +.selectedEditor:is(.annotationEditorLayer .highlightEditor) .internal{ + cursor:pointer; + } + +:is(.annotationEditorLayer .highlightEditor) .editToolbar{ + --editor-toolbar-colorpicker-arrow-image:url(images/toolbarButton-menuArrow.svg); + + transform-origin:center !important; + } + +:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker{ + position:relative; + width:auto; + display:flex; + justify-content:center; + align-items:center; + gap:4px; + padding:4px; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker)::after{ + content:""; + -webkit-mask-image:var(--editor-toolbar-colorpicker-arrow-image); + mask-image:var(--editor-toolbar-colorpicker-arrow-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:12px; + height:12px; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):hover::after{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden)){ + background-color:var(--editor-toolbar-hover-bg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden))::after{ + scale:-1; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown{ + position:absolute; + display:flex; + justify-content:center; + align-items:center; + flex-direction:column; + gap:11px; + padding-block:8px; + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + inset-block-start:calc(100% + 4px); + width:calc(100% + 2 * var(--editor-toolbar-padding)); + } + +:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button{ + width:100%; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline-offset:2px; + } + +[aria-selected="true"]:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); + } + +.editorParamsToolbar:has(#highlightParamsToolbarContainer){ + padding:unset; +} + +#highlightParamsToolbarContainer{ + gap:16px; + padding-inline:10px; + padding-block-end:12px; +} + +#highlightParamsToolbarContainer .colorPicker{ + display:flex; + flex-direction:column; + gap:8px; + } + +:is(#highlightParamsToolbarContainer .colorPicker) .dropdown{ + display:flex; + justify-content:space-between; + align-items:center; + flex-direction:row; + height:auto; + } + +:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button{ + width:auto; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + flex:0 0 auto; + padding:0; + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) .swatch{ + width:24px; + height:24px; + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; + } + +[aria-selected="true"]:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); + } + +#highlightParamsToolbarContainer #editorHighlightThickness{ + display:flex; + flex-direction:column; + align-items:center; + gap:4px; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .editorParamsLabel{ + height:auto; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + + --example-color:#bfbfc9; + } + +@media (prefers-color-scheme: dark){ + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:#80808e; + } + } + +@media screen and (forced-colors: active){ + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:CanvasText; + } + } + +:is(:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) > .editorParamsSlider[disabled]){ + opacity:0.4; + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::before,:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + content:""; + width:8px; + aspect-ratio:1; + display:block; + border-radius:100%; + background-color:var(--example-color); + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + width:24px; + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) .editorParamsSlider{ + width:unset; + height:14px; + } + +#highlightParamsToolbarContainer #editorHighlightVisibility{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#d7d7db; + } + +@media (prefers-color-scheme: dark){ + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#8f8f9d; + } + } + +@media screen and (forced-colors: active){ + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:CanvasText; + } + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + + margin-block:4px; + width:100%; + height:1px; + background-color:var(--divider-color); + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .toggler{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + } + +#altTextSettingsDialog{ + padding:16px; +} + +#altTextSettingsDialog #altTextSettingsContainer{ + display:flex; + width:573px; + flex-direction:column; + gap:16px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) .mainContainer{ + gap:16px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) .description{ + color:var(--text-secondary-color); + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings{ + display:flex; + flex-direction:column; + gap:12px; + } + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) button{ + width:-moz-fit-content; + width:fit-content; + } + +.download:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) #deleteModelButton{ + display:none; + } + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings):not(.download) #downloadModelButton{ + display:none; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticAltText,:is(#altTextSettingsDialog #altTextSettingsContainer) #altTextEditor{ + display:flex; + flex-direction:column; + gap:8px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #createModelDescription,:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings,:is(#altTextSettingsDialog #altTextSettingsContainer) #showAltTextDialogDescription{ + padding-inline-start:40px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticSettings{ + display:flex; + flex-direction:column; + gap:16px; + } + +:root{ + --viewer-container-height:0; + --pdfViewer-padding-bottom:0; + --page-margin:1px auto -8px; + --page-border:9px solid transparent; + --spreadHorizontalWrapped-margin-LR:-3.5px; + --loading-icon-delay:400ms; +} + +@media screen and (forced-colors: active){ + :root{ + --pdfViewer-padding-bottom:9px; + --page-margin:8px auto -1px; + --page-border:1px solid CanvasText; + --spreadHorizontalWrapped-margin-LR:3.5px; + } +} + +[data-main-rotation="90"]{ + transform:rotate(90deg) translateY(-100%); +} +[data-main-rotation="180"]{ + transform:rotate(180deg) translate(-100%, -100%); +} +[data-main-rotation="270"]{ + transform:rotate(270deg) translateX(-100%); +} + +#hiddenCopyElement, +.hiddenCanvasElement{ + position:absolute; + top:0; + left:0; + width:0; + height:0; + display:none; +} + +.pdfViewer{ + --scale-factor:1; + --page-bg-color:unset; + + padding-bottom:var(--pdfViewer-padding-bottom); + + --hcm-highlight-filter:none; + --hcm-highlight-selected-filter:none; +} + +@media screen and (forced-colors: active){ + +.pdfViewer{ + --hcm-highlight-filter:invert(100%); +} + } + +.pdfViewer.copyAll{ + cursor:wait; + } + +.pdfViewer .canvasWrapper{ + overflow:hidden; + width:100%; + height:100%; + } + +:is(.pdfViewer .canvasWrapper) canvas{ + position:absolute; + top:0; + left:0; + margin:0; + display:block; + width:100%; + height:100%; + contain:content; + } + +:is(:is(.pdfViewer .canvasWrapper) canvas) .structTree{ + contain:strict; + } + +.pdfViewer .page{ + --scale-round-x:1px; + --scale-round-y:1px; + + direction:ltr; + width:816px; + height:1056px; + margin:var(--page-margin); + position:relative; + overflow:visible; + border:var(--page-border); + background-clip:content-box; + background-color:var(--page-bg-color, rgb(255 255 255)); +} + +.pdfViewer .dummyPage{ + position:relative; + width:0; + height:var(--viewer-container-height); +} + +.pdfViewer.noUserSelect{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.pdfViewer.removePageBorders .page{ + margin:0 auto 10px; + border:none; +} + +.pdfViewer.singlePageView{ + display:inline-block; +} + +.pdfViewer.singlePageView .page{ + margin:0; + border:none; +} + +.pdfViewer:is(.scrollHorizontal, .scrollWrapped), +.spread{ + margin-inline:3.5px; + text-align:center; +} + +.pdfViewer.scrollHorizontal, +.spread{ + white-space:nowrap; +} + +.pdfViewer.removePageBorders, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread{ + margin-inline:0; +} + +.spread :is(.page, .dummyPage), +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread){ + display:inline-block; + vertical-align:middle; +} + +.spread .page, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:var(--spreadHorizontalWrapped-margin-LR); +} + +.pdfViewer.removePageBorders .spread .page, +.pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:5px; +} + +.pdfViewer .page.loadingIcon::after{ + position:absolute; + top:0; + left:0; + content:""; + width:100%; + height:100%; + background:url("images/loading-icon.gif") center no-repeat; + display:none; + transition-property:display; + transition-delay:var(--loading-icon-delay); + z-index:5; + contain:strict; +} + +.pdfViewer .page.loading::after{ + display:block; +} + +.pdfViewer .page:not(.loading)::after{ + transition-property:none; + display:none; +} + +.pdfPresentationMode .pdfViewer{ + padding-bottom:0; +} + +.pdfPresentationMode .spread{ + margin:0; +} + +.pdfPresentationMode .pdfViewer .page{ + margin:0 auto; + border:2px solid transparent; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs new file mode 100644 index 00000000000..9b2c200c99e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs @@ -0,0 +1,8435 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ + +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = globalThis.pdfjsViewer = {}; + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + AnnotationLayerBuilder: () => (/* reexport */ AnnotationLayerBuilder), + DownloadManager: () => (/* reexport */ DownloadManager), + EventBus: () => (/* reexport */ EventBus), + FindState: () => (/* reexport */ FindState), + GenericL10n: () => (/* reexport */ genericl10n_GenericL10n), + LinkTarget: () => (/* reexport */ LinkTarget), + PDFFindController: () => (/* reexport */ PDFFindController), + PDFHistory: () => (/* reexport */ PDFHistory), + PDFLinkService: () => (/* reexport */ PDFLinkService), + PDFPageView: () => (/* reexport */ PDFPageView), + PDFScriptingManager: () => (/* reexport */ PDFScriptingManagerComponents), + PDFSinglePageViewer: () => (/* reexport */ PDFSinglePageViewer), + PDFViewer: () => (/* reexport */ PDFViewer), + ProgressBar: () => (/* reexport */ ProgressBar), + RenderingStates: () => (/* reexport */ RenderingStates), + ScrollMode: () => (/* reexport */ ScrollMode), + SimpleLinkService: () => (/* reexport */ SimpleLinkService), + SpreadMode: () => (/* reexport */ SpreadMode), + StructTreeLayerBuilder: () => (/* reexport */ StructTreeLayerBuilder), + TextLayerBuilder: () => (/* reexport */ TextLayerBuilder), + XfaLayerBuilder: () => (/* reexport */ XfaLayerBuilder), + parseQueryString: () => (/* reexport */ parseQueryString) +}); + +;// ./web/ui_utils.js +const DEFAULT_SCALE_VALUE = "auto"; +const DEFAULT_SCALE = 1.0; +const DEFAULT_SCALE_DELTA = 1.1; +const MIN_SCALE = 0.1; +const MAX_SCALE = 10.0; +const UNKNOWN_SCALE = 0; +const MAX_AUTO_SCALE = 1.25; +const SCROLLBAR_PADDING = 40; +const VERTICAL_PADDING = 5; +const RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; +const PresentationModeState = { + UNKNOWN: 0, + NORMAL: 1, + CHANGING: 2, + FULLSCREEN: 3 +}; +const SidebarView = { + UNKNOWN: -1, + NONE: 0, + THUMBS: 1, + OUTLINE: 2, + ATTACHMENTS: 3, + LAYERS: 4 +}; +const TextLayerMode = { + DISABLE: 0, + ENABLE: 1, + ENABLE_PERMISSIONS: 2 +}; +const ScrollMode = { + UNKNOWN: -1, + VERTICAL: 0, + HORIZONTAL: 1, + WRAPPED: 2, + PAGE: 3 +}; +const SpreadMode = { + UNKNOWN: -1, + NONE: 0, + ODD: 1, + EVEN: 2 +}; +const CursorTool = { + SELECT: 0, + HAND: 1, + ZOOM: 2 +}; +const AutoPrintRegExp = /\bprint\s*\(/; +function scrollIntoView(element, spot, scrollMatches = false) { + let parent = element.offsetParent; + if (!parent) { + console.error("offsetParent is not set -- cannot scroll"); + return; + } + let offsetY = element.offsetTop + element.clientTop; + let offsetX = element.offsetLeft + element.clientLeft; + while (parent.clientHeight === parent.scrollHeight && parent.clientWidth === parent.scrollWidth || scrollMatches && (parent.classList.contains("markedContent") || getComputedStyle(parent).overflow === "hidden")) { + offsetY += parent.offsetTop; + offsetX += parent.offsetLeft; + parent = parent.offsetParent; + if (!parent) { + return; + } + } + if (spot) { + if (spot.top !== undefined) { + offsetY += spot.top; + } + if (spot.left !== undefined) { + offsetX += spot.left; + parent.scrollLeft = offsetX; + } + } + parent.scrollTop = offsetY; +} +function watchScroll(viewAreaElement, callback, abortSignal = undefined) { + const debounceScroll = function (evt) { + if (rAF) { + return; + } + rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { + rAF = null; + const currentX = viewAreaElement.scrollLeft; + const lastX = state.lastX; + if (currentX !== lastX) { + state.right = currentX > lastX; + } + state.lastX = currentX; + const currentY = viewAreaElement.scrollTop; + const lastY = state.lastY; + if (currentY !== lastY) { + state.down = currentY > lastY; + } + state.lastY = currentY; + callback(state); + }); + }; + const state = { + right: true, + down: true, + lastX: viewAreaElement.scrollLeft, + lastY: viewAreaElement.scrollTop, + _eventHandler: debounceScroll + }; + let rAF = null; + viewAreaElement.addEventListener("scroll", debounceScroll, { + useCapture: true, + signal: abortSignal + }); + abortSignal?.addEventListener("abort", () => window.cancelAnimationFrame(rAF), { + once: true + }); + return state; +} +function parseQueryString(query) { + const params = new Map(); + for (const [key, value] of new URLSearchParams(query)) { + params.set(key.toLowerCase(), value); + } + return params; +} +const InvisibleCharsRegExp = /[\x00-\x1F]/g; +function removeNullCharacters(str, replaceInvisible = false) { + if (!InvisibleCharsRegExp.test(str)) { + return str; + } + if (replaceInvisible) { + return str.replaceAll(InvisibleCharsRegExp, m => m === "\x00" ? "" : " "); + } + return str.replaceAll("\x00", ""); +} +function binarySearchFirstItem(items, condition, start = 0) { + let minIndex = start; + let maxIndex = items.length - 1; + if (maxIndex < 0 || !condition(items[maxIndex])) { + return items.length; + } + if (condition(items[minIndex])) { + return minIndex; + } + while (minIndex < maxIndex) { + const currentIndex = minIndex + maxIndex >> 1; + const currentItem = items[currentIndex]; + if (condition(currentItem)) { + maxIndex = currentIndex; + } else { + minIndex = currentIndex + 1; + } + } + return minIndex; +} +function approximateFraction(x) { + if (Math.floor(x) === x) { + return [x, 1]; + } + const xinv = 1 / x; + const limit = 8; + if (xinv > limit) { + return [1, limit]; + } else if (Math.floor(xinv) === xinv) { + return [1, xinv]; + } + const x_ = x > 1 ? xinv : x; + let a = 0, + b = 1, + c = 1, + d = 1; + while (true) { + const p = a + c, + q = b + d; + if (q > limit) { + break; + } + if (x_ <= p / q) { + c = p; + d = q; + } else { + a = p; + b = q; + } + } + let result; + if (x_ - a / b < c / d - x_) { + result = x_ === x ? [a, b] : [b, a]; + } else { + result = x_ === x ? [c, d] : [d, c]; + } + return result; +} +function floorToDivide(x, div) { + return x - x % div; +} +function getPageSizeInches({ + view, + userUnit, + rotate +}) { + const [x1, y1, x2, y2] = view; + const changeOrientation = rotate % 180 !== 0; + const width = (x2 - x1) / 72 * userUnit; + const height = (y2 - y1) / 72 * userUnit; + return { + width: changeOrientation ? height : width, + height: changeOrientation ? width : height + }; +} +function backtrackBeforeAllVisibleElements(index, views, top) { + if (index < 2) { + return index; + } + let elt = views[index].div; + let pageTop = elt.offsetTop + elt.clientTop; + if (pageTop >= top) { + elt = views[index - 1].div; + pageTop = elt.offsetTop + elt.clientTop; + } + for (let i = index - 2; i >= 0; --i) { + elt = views[i].div; + if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) { + break; + } + index = i; + } + return index; +} +function getVisibleElements({ + scrollEl, + views, + sortByVisibility = false, + horizontal = false, + rtl = false +}) { + const top = scrollEl.scrollTop, + bottom = top + scrollEl.clientHeight; + const left = scrollEl.scrollLeft, + right = left + scrollEl.clientWidth; + function isElementBottomAfterViewTop(view) { + const element = view.div; + const elementBottom = element.offsetTop + element.clientTop + element.clientHeight; + return elementBottom > top; + } + function isElementNextAfterViewHorizontally(view) { + const element = view.div; + const elementLeft = element.offsetLeft + element.clientLeft; + const elementRight = elementLeft + element.clientWidth; + return rtl ? elementLeft < right : elementRight > left; + } + const visible = [], + ids = new Set(), + numViews = views.length; + let firstVisibleElementInd = binarySearchFirstItem(views, horizontal ? isElementNextAfterViewHorizontally : isElementBottomAfterViewTop); + if (firstVisibleElementInd > 0 && firstVisibleElementInd < numViews && !horizontal) { + firstVisibleElementInd = backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top); + } + let lastEdge = horizontal ? right : -1; + for (let i = firstVisibleElementInd; i < numViews; i++) { + const view = views[i], + element = view.div; + const currentWidth = element.offsetLeft + element.clientLeft; + const currentHeight = element.offsetTop + element.clientTop; + const viewWidth = element.clientWidth, + viewHeight = element.clientHeight; + const viewRight = currentWidth + viewWidth; + const viewBottom = currentHeight + viewHeight; + if (lastEdge === -1) { + if (viewBottom >= bottom) { + lastEdge = viewBottom; + } + } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) { + break; + } + if (viewBottom <= top || currentHeight >= bottom || viewRight <= left || currentWidth >= right) { + continue; + } + const hiddenHeight = Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom); + const hiddenWidth = Math.max(0, left - currentWidth) + Math.max(0, viewRight - right); + const fractionHeight = (viewHeight - hiddenHeight) / viewHeight, + fractionWidth = (viewWidth - hiddenWidth) / viewWidth; + const percent = fractionHeight * fractionWidth * 100 | 0; + visible.push({ + id: view.id, + x: currentWidth, + y: currentHeight, + view, + percent, + widthPercent: fractionWidth * 100 | 0 + }); + ids.add(view.id); + } + const first = visible[0], + last = visible.at(-1); + if (sortByVisibility) { + visible.sort(function (a, b) { + const pc = a.percent - b.percent; + if (Math.abs(pc) > 0.001) { + return -pc; + } + return a.id - b.id; + }); + } + return { + first, + last, + views: visible, + ids + }; +} +function normalizeWheelEventDirection(evt) { + let delta = Math.hypot(evt.deltaX, evt.deltaY); + const angle = Math.atan2(evt.deltaY, evt.deltaX); + if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) { + delta = -delta; + } + return delta; +} +function normalizeWheelEventDelta(evt) { + const deltaMode = evt.deltaMode; + let delta = normalizeWheelEventDirection(evt); + const MOUSE_PIXELS_PER_LINE = 30; + const MOUSE_LINES_PER_PAGE = 30; + if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE; + } else if (deltaMode === WheelEvent.DOM_DELTA_LINE) { + delta /= MOUSE_LINES_PER_PAGE; + } + return delta; +} +function isValidRotation(angle) { + return Number.isInteger(angle) && angle % 90 === 0; +} +function isValidScrollMode(mode) { + return Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) && mode !== ScrollMode.UNKNOWN; +} +function isValidSpreadMode(mode) { + return Number.isInteger(mode) && Object.values(SpreadMode).includes(mode) && mode !== SpreadMode.UNKNOWN; +} +function isPortraitOrientation(size) { + return size.width <= size.height; +} +const animationStarted = new Promise(function (resolve) { + window.requestAnimationFrame(resolve); +}); +const docStyle = document.documentElement.style; +function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); +} +class ProgressBar { + #classList = null; + #disableAutoFetchTimeout = null; + #percent = 0; + #style = null; + #visible = true; + constructor(bar) { + this.#classList = bar.classList; + this.#style = bar.style; + } + get percent() { + return this.#percent; + } + set percent(val) { + this.#percent = clamp(val, 0, 100); + if (isNaN(val)) { + this.#classList.add("indeterminate"); + return; + } + this.#classList.remove("indeterminate"); + this.#style.setProperty("--progressBar-percent", `${this.#percent}%`); + } + setWidth(viewer) { + if (!viewer) { + return; + } + const container = viewer.parentNode; + const scrollbarWidth = container.offsetWidth - viewer.offsetWidth; + if (scrollbarWidth > 0) { + this.#style.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`); + } + } + setDisableAutoFetch(delay = 5000) { + if (this.#percent === 100 || isNaN(this.#percent)) { + return; + } + if (this.#disableAutoFetchTimeout) { + clearTimeout(this.#disableAutoFetchTimeout); + } + this.show(); + this.#disableAutoFetchTimeout = setTimeout(() => { + this.#disableAutoFetchTimeout = null; + this.hide(); + }, delay); + } + hide() { + if (!this.#visible) { + return; + } + this.#visible = false; + this.#classList.add("hidden"); + } + show() { + if (this.#visible) { + return; + } + this.#visible = true; + this.#classList.remove("hidden"); + } +} +function getActiveOrFocusedElement() { + let curRoot = document; + let curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus"); + while (curActiveOrFocused?.shadowRoot) { + curRoot = curActiveOrFocused.shadowRoot; + curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus"); + } + return curActiveOrFocused; +} +function apiPageLayoutToViewerModes(layout) { + let scrollMode = ScrollMode.VERTICAL, + spreadMode = SpreadMode.NONE; + switch (layout) { + case "SinglePage": + scrollMode = ScrollMode.PAGE; + break; + case "OneColumn": + break; + case "TwoPageLeft": + scrollMode = ScrollMode.PAGE; + case "TwoColumnLeft": + spreadMode = SpreadMode.ODD; + break; + case "TwoPageRight": + scrollMode = ScrollMode.PAGE; + case "TwoColumnRight": + spreadMode = SpreadMode.EVEN; + break; + } + return { + scrollMode, + spreadMode + }; +} +function apiPageModeToSidebarView(mode) { + switch (mode) { + case "UseNone": + return SidebarView.NONE; + case "UseThumbs": + return SidebarView.THUMBS; + case "UseOutlines": + return SidebarView.OUTLINE; + case "UseAttachments": + return SidebarView.ATTACHMENTS; + case "UseOC": + return SidebarView.LAYERS; + } + return SidebarView.NONE; +} +function toggleCheckedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-checked", toggle); + view?.classList.toggle("hidden", !toggle); +} +function toggleExpandedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-expanded", toggle); + view?.classList.toggle("hidden", !toggle); +} +const calcRound = function () { + const e = document.createElement("div"); + e.style.width = "round(down, calc(1.6666666666666665 * 792px), 1px)"; + return e.style.width === "calc(1320px)" ? Math.fround : x => x; +}(); + +;// ./web/pdf_find_utils.js +const CharacterType = { + SPACE: 0, + ALPHA_LETTER: 1, + PUNCT: 2, + HAN_LETTER: 3, + KATAKANA_LETTER: 4, + HIRAGANA_LETTER: 5, + HALFWIDTH_KATAKANA_LETTER: 6, + THAI_LETTER: 7 +}; +function isAlphabeticalScript(charCode) { + return charCode < 0x2e80; +} +function isAscii(charCode) { + return (charCode & 0xff80) === 0; +} +function isAsciiAlpha(charCode) { + return charCode >= 0x61 && charCode <= 0x7a || charCode >= 0x41 && charCode <= 0x5a; +} +function isAsciiDigit(charCode) { + return charCode >= 0x30 && charCode <= 0x39; +} +function isAsciiSpace(charCode) { + return charCode === 0x20 || charCode === 0x09 || charCode === 0x0d || charCode === 0x0a; +} +function isHan(charCode) { + return charCode >= 0x3400 && charCode <= 0x9fff || charCode >= 0xf900 && charCode <= 0xfaff; +} +function isKatakana(charCode) { + return charCode >= 0x30a0 && charCode <= 0x30ff; +} +function isHiragana(charCode) { + return charCode >= 0x3040 && charCode <= 0x309f; +} +function isHalfwidthKatakana(charCode) { + return charCode >= 0xff60 && charCode <= 0xff9f; +} +function isThai(charCode) { + return (charCode & 0xff80) === 0x0e00; +} +function getCharacterType(charCode) { + if (isAlphabeticalScript(charCode)) { + if (isAscii(charCode)) { + if (isAsciiSpace(charCode)) { + return CharacterType.SPACE; + } else if (isAsciiAlpha(charCode) || isAsciiDigit(charCode) || charCode === 0x5f) { + return CharacterType.ALPHA_LETTER; + } + return CharacterType.PUNCT; + } else if (isThai(charCode)) { + return CharacterType.THAI_LETTER; + } else if (charCode === 0xa0) { + return CharacterType.SPACE; + } + return CharacterType.ALPHA_LETTER; + } + if (isHan(charCode)) { + return CharacterType.HAN_LETTER; + } else if (isKatakana(charCode)) { + return CharacterType.KATAKANA_LETTER; + } else if (isHiragana(charCode)) { + return CharacterType.HIRAGANA_LETTER; + } else if (isHalfwidthKatakana(charCode)) { + return CharacterType.HALFWIDTH_KATAKANA_LETTER; + } + return CharacterType.ALPHA_LETTER; +} +let NormalizeWithNFKC; +function getNormalizeWithNFKC() { + NormalizeWithNFKC ||= ` ¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰꟲ-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`; + return NormalizeWithNFKC; +} + +;// ./web/pdf_find_controller.js + + +const FindState = { + FOUND: 0, + NOT_FOUND: 1, + WRAPPED: 2, + PENDING: 3 +}; +const FIND_TIMEOUT = 250; +const MATCH_SCROLL_OFFSET_TOP = -50; +const MATCH_SCROLL_OFFSET_LEFT = -400; +const CHARACTERS_TO_NORMALIZE = { + "\u2010": "-", + "\u2018": "'", + "\u2019": "'", + "\u201A": "'", + "\u201B": "'", + "\u201C": '"', + "\u201D": '"', + "\u201E": '"', + "\u201F": '"', + "\u00BC": "1/4", + "\u00BD": "1/2", + "\u00BE": "3/4" +}; +const DIACRITICS_EXCEPTION = new Set([0x3099, 0x309a, 0x094d, 0x09cd, 0x0a4d, 0x0acd, 0x0b4d, 0x0bcd, 0x0c4d, 0x0ccd, 0x0d3b, 0x0d3c, 0x0d4d, 0x0dca, 0x0e3a, 0x0eba, 0x0f84, 0x1039, 0x103a, 0x1714, 0x1734, 0x17d2, 0x1a60, 0x1b44, 0x1baa, 0x1bab, 0x1bf2, 0x1bf3, 0x2d7f, 0xa806, 0xa82c, 0xa8c4, 0xa953, 0xa9c0, 0xaaf6, 0xabed, 0x0c56, 0x0f71, 0x0f72, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f80, 0x0f74]); +let DIACRITICS_EXCEPTION_STR; +const DIACRITICS_REG_EXP = /\p{M}+/gu; +const SPECIAL_CHARS_REG_EXP = /([.*+?^${}()|[\]\\])|(\p{P})|(\s+)|(\p{M})|(\p{L})/gu; +const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u; +const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u; +const SYLLABLES_REG_EXP = /[\uAC00-\uD7AF\uFA6C\uFACF-\uFAD1\uFAD5-\uFAD7]+/g; +const SYLLABLES_LENGTHS = new Map(); +const FIRST_CHAR_SYLLABLES_REG_EXP = "[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]"; +const NFKC_CHARS_TO_NORMALIZE = new Map(); +let noSyllablesRegExp = null; +let withSyllablesRegExp = null; +function normalize(text) { + const syllablePositions = []; + let m; + while ((m = SYLLABLES_REG_EXP.exec(text)) !== null) { + let { + index + } = m; + for (const char of m[0]) { + let len = SYLLABLES_LENGTHS.get(char); + if (!len) { + len = char.normalize("NFD").length; + SYLLABLES_LENGTHS.set(char, len); + } + syllablePositions.push([len, index++]); + } + } + let normalizationRegex; + if (syllablePositions.length === 0 && noSyllablesRegExp) { + normalizationRegex = noSyllablesRegExp; + } else if (syllablePositions.length > 0 && withSyllablesRegExp) { + normalizationRegex = withSyllablesRegExp; + } else { + const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); + const toNormalizeWithNFKC = getNormalizeWithNFKC(); + const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])"; + const HKDiacritics = "(?:\u3099|\u309A)"; + const CompoundWord = "\\p{Ll}-\\n\\p{Lu}"; + const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(${CompoundWord})|(\\S-\\n)|(${CJK}\\n)|(\\n)`; + if (syllablePositions.length === 0) { + normalizationRegex = noSyllablesRegExp = new RegExp(regexp + "|(\\u0000)", "gum"); + } else { + normalizationRegex = withSyllablesRegExp = new RegExp(regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`, "gum"); + } + } + const rawDiacriticsPositions = []; + while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) { + rawDiacriticsPositions.push([m[0].length, m.index]); + } + let normalized = text.normalize("NFD"); + const positions = [0, 0]; + let rawDiacriticsIndex = 0; + let syllableIndex = 0; + let shift = 0; + let shiftOrigin = 0; + let eol = 0; + let hasDiacritics = false; + normalized = normalized.replace(normalizationRegex, (match, p1, p2, p3, p4, p5, p6, p7, p8, p9, i) => { + i -= shiftOrigin; + if (p1) { + const replacement = CHARACTERS_TO_NORMALIZE[p1]; + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p2) { + let replacement = NFKC_CHARS_TO_NORMALIZE.get(p2); + if (!replacement) { + replacement = p2.normalize("NFKC"); + NFKC_CHARS_TO_NORMALIZE.set(p2, replacement); + } + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p3) { + hasDiacritics = true; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + ++rawDiacriticsIndex; + } else { + positions.push(i - 1 - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + } + positions.push(i - shift + 1, shift); + shiftOrigin += 1; + eol += 1; + return p3.charAt(0); + } + if (p4) { + const hasTrailingDashEOL = p4.endsWith("\n"); + const len = hasTrailingDashEOL ? p4.length - 2 : p4.length; + hasDiacritics = true; + let jj = len; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + jj -= rawDiacriticsPositions[rawDiacriticsIndex][0]; + ++rawDiacriticsIndex; + } + for (let j = 1; j <= jj; j++) { + positions.push(i - 1 - shift + j, shift - j); + } + shift -= jj; + shiftOrigin += jj; + if (hasTrailingDashEOL) { + i += len - 1; + positions.push(i - shift + 1, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p4.slice(0, len); + } + return p4; + } + if (p5) { + shiftOrigin += 1; + eol += 1; + return p5.replace("\n", ""); + } + if (p6) { + const len = p6.length - 2; + positions.push(i - shift + len, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p6.slice(0, -2); + } + if (p7) { + const len = p7.length - 1; + positions.push(i - shift + len, shift); + shiftOrigin += 1; + eol += 1; + return p7.slice(0, -1); + } + if (p8) { + positions.push(i - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + eol += 1; + return " "; + } + if (i + eol === syllablePositions[syllableIndex]?.[1]) { + const newCharLen = syllablePositions[syllableIndex][0] - 1; + ++syllableIndex; + for (let j = 1; j <= newCharLen; j++) { + positions.push(i - (shift - j), shift - j); + } + shift -= newCharLen; + shiftOrigin += newCharLen; + } + return p9; + }); + positions.push(normalized.length, shift); + const starts = new Uint32Array(positions.length >> 1); + const shifts = new Int32Array(positions.length >> 1); + for (let i = 0, ii = positions.length; i < ii; i += 2) { + starts[i >> 1] = positions[i]; + shifts[i >> 1] = positions[i + 1]; + } + return [normalized, [starts, shifts], hasDiacritics]; +} +function getOriginalIndex(diffs, pos, len) { + if (!diffs) { + return [pos, len]; + } + const [starts, shifts] = diffs; + const start = pos; + const end = pos + len - 1; + let i = binarySearchFirstItem(starts, x => x >= start); + if (starts[i] > start) { + --i; + } + let j = binarySearchFirstItem(starts, x => x >= end, i); + if (starts[j] > end) { + --j; + } + const oldStart = start + shifts[i]; + const oldEnd = end + shifts[j]; + const oldLen = oldEnd + 1 - oldStart; + return [oldStart, oldLen]; +} +class PDFFindController { + #state = null; + #updateMatchesCountOnProgress = true; + #visitedPagesCount = 0; + constructor({ + linkService, + eventBus, + updateMatchesCountOnProgress = true + }) { + this._linkService = linkService; + this._eventBus = eventBus; + this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress; + this.onIsPageVisible = null; + this.#reset(); + eventBus._on("find", this.#onFind.bind(this)); + eventBus._on("findbarclose", this.#onFindBarClose.bind(this)); + } + get highlightMatches() { + return this._highlightMatches; + } + get pageMatches() { + return this._pageMatches; + } + get pageMatchesLength() { + return this._pageMatchesLength; + } + get selected() { + return this._selected; + } + get state() { + return this.#state; + } + setDocument(pdfDocument) { + if (this._pdfDocument) { + this.#reset(); + } + if (!pdfDocument) { + return; + } + this._pdfDocument = pdfDocument; + this._firstPageCapability.resolve(); + } + #onFind(state) { + if (!state) { + return; + } + const pdfDocument = this._pdfDocument; + const { + type + } = state; + if (this.#state === null || this.#shouldDirtyMatch(state)) { + this._dirtyMatch = true; + } + this.#state = state; + if (type !== "highlightallchange") { + this.#updateUIState(FindState.PENDING); + } + this._firstPageCapability.promise.then(() => { + if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { + return; + } + this.#extractText(); + const findbarClosed = !this._highlightMatches; + const pendingTimeout = !!this._findTimeout; + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (!type) { + this._findTimeout = setTimeout(() => { + this.#nextMatch(); + this._findTimeout = null; + }, FIND_TIMEOUT); + } else if (this._dirtyMatch) { + this.#nextMatch(); + } else if (type === "again") { + this.#nextMatch(); + if (findbarClosed && this.#state.highlightAll) { + this.#updateAllPages(); + } + } else if (type === "highlightallchange") { + if (pendingTimeout) { + this.#nextMatch(); + } else { + this._highlightMatches = true; + } + this.#updateAllPages(); + } else { + this.#nextMatch(); + } + }); + } + scrollMatchIntoView({ + element = null, + selectedLeft = 0, + pageIndex = -1, + matchIndex = -1 + }) { + if (!this._scrollMatches || !element) { + return; + } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { + return; + } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) { + return; + } + this._scrollMatches = false; + const spot = { + top: MATCH_SCROLL_OFFSET_TOP, + left: selectedLeft + MATCH_SCROLL_OFFSET_LEFT + }; + scrollIntoView(element, spot, true); + } + #reset() { + this._highlightMatches = false; + this._scrollMatches = false; + this._pdfDocument = null; + this._pageMatches = []; + this._pageMatchesLength = []; + this.#visitedPagesCount = 0; + this.#state = null; + this._selected = { + pageIdx: -1, + matchIdx: -1 + }; + this._offset = { + pageIdx: null, + matchIdx: null, + wrapped: false + }; + this._extractTextPromises = []; + this._pageContents = []; + this._pageDiffs = []; + this._hasDiacritics = []; + this._matchesCountTotal = 0; + this._pagesToSearch = null; + this._pendingFindMatches = new Set(); + this._resumePageIdx = null; + this._dirtyMatch = false; + clearTimeout(this._findTimeout); + this._findTimeout = null; + this._firstPageCapability = Promise.withResolvers(); + } + get #query() { + const { + query + } = this.#state; + if (typeof query === "string") { + if (query !== this._rawQuery) { + this._rawQuery = query; + [this._normalizedQuery] = normalize(query); + } + return this._normalizedQuery; + } + return (query || []).filter(q => !!q).map(q => normalize(q)[0]); + } + #shouldDirtyMatch(state) { + const newQuery = state.query, + prevQuery = this.#state.query; + const newType = typeof newQuery, + prevType = typeof prevQuery; + if (newType !== prevType) { + return true; + } + if (newType === "string") { + if (newQuery !== prevQuery) { + return true; + } + } else if (JSON.stringify(newQuery) !== JSON.stringify(prevQuery)) { + return true; + } + switch (state.type) { + case "again": + const pageNumber = this._selected.pageIdx + 1; + const linkService = this._linkService; + return pageNumber >= 1 && pageNumber <= linkService.pagesCount && pageNumber !== linkService.page && !(this.onIsPageVisible?.(pageNumber) ?? true); + case "highlightallchange": + return false; + } + return true; + } + #isEntireWord(content, startIdx, length) { + let match = content.slice(0, startIdx).match(NOT_DIACRITIC_FROM_END_REG_EXP); + if (match) { + const first = content.charCodeAt(startIdx); + const limit = match[1].charCodeAt(0); + if (getCharacterType(first) === getCharacterType(limit)) { + return false; + } + } + match = content.slice(startIdx + length).match(NOT_DIACRITIC_FROM_START_REG_EXP); + if (match) { + const last = content.charCodeAt(startIdx + length - 1); + const limit = match[1].charCodeAt(0); + if (getCharacterType(last) === getCharacterType(limit)) { + return false; + } + } + return true; + } + #convertToRegExpString(query, hasDiacritics) { + const { + matchDiacritics + } = this.#state; + let isUnicode = false; + query = query.replaceAll(SPECIAL_CHARS_REG_EXP, (match, p1, p2, p3, p4, p5) => { + if (p1) { + return `[ ]*\\${p1}[ ]*`; + } + if (p2) { + return `[ ]*${p2}[ ]*`; + } + if (p3) { + return "[ ]+"; + } + if (matchDiacritics) { + return p4 || p5; + } + if (p4) { + return DIACRITICS_EXCEPTION.has(p4.charCodeAt(0)) ? p4 : ""; + } + if (hasDiacritics) { + isUnicode = true; + return `${p5}\\p{M}*`; + } + return p5; + }); + const trailingSpaces = "[ ]*"; + if (query.endsWith(trailingSpaces)) { + query = query.slice(0, query.length - trailingSpaces.length); + } + if (matchDiacritics) { + if (hasDiacritics) { + DIACRITICS_EXCEPTION_STR ||= String.fromCharCode(...DIACRITICS_EXCEPTION); + isUnicode = true; + query = `${query}(?=[${DIACRITICS_EXCEPTION_STR}]|[^\\p{M}]|$)`; + } + } + return [isUnicode, query]; + } + #calculateMatch(pageIndex) { + const query = this.#query; + if (query.length === 0) { + return; + } + const pageContent = this._pageContents[pageIndex]; + const matcherResult = this.match(query, pageContent, pageIndex); + const matches = this._pageMatches[pageIndex] = []; + const matchesLength = this._pageMatchesLength[pageIndex] = []; + const diffs = this._pageDiffs[pageIndex]; + matcherResult?.forEach(({ + index, + length + }) => { + const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); + if (matchLen) { + matches.push(matchPos); + matchesLength.push(matchLen); + } + }); + if (this.#state.highlightAll) { + this.#updatePage(pageIndex); + } + if (this._resumePageIdx === pageIndex) { + this._resumePageIdx = null; + this.#nextPageMatch(); + } + const pageMatchesCount = matches.length; + this._matchesCountTotal += pageMatchesCount; + if (this.#updateMatchesCountOnProgress) { + if (pageMatchesCount > 0) { + this.#updateUIResultsCount(); + } + } else if (++this.#visitedPagesCount === this._linkService.pagesCount) { + this.#updateUIResultsCount(); + } + } + match(query, pageContent, pageIndex) { + const hasDiacritics = this._hasDiacritics[pageIndex]; + let isUnicode = false; + if (typeof query === "string") { + [isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics); + } else { + query = query.sort().reverse().map(q => { + const [isUnicodePart, queryPart] = this.#convertToRegExpString(q, hasDiacritics); + isUnicode ||= isUnicodePart; + return `(${queryPart})`; + }).join("|"); + } + if (!query) { + return undefined; + } + const { + caseSensitive, + entireWord + } = this.#state; + const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`; + query = new RegExp(query, flags); + const matches = []; + let match; + while ((match = query.exec(pageContent)) !== null) { + if (entireWord && !this.#isEntireWord(pageContent, match.index, match[0].length)) { + continue; + } + matches.push({ + index: match.index, + length: match[0].length + }); + } + return matches; + } + #extractText() { + if (this._extractTextPromises.length > 0) { + return; + } + let deferred = Promise.resolve(); + const textOptions = { + disableNormalization: true + }; + for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { + const { + promise, + resolve + } = Promise.withResolvers(); + this._extractTextPromises[i] = promise; + deferred = deferred.then(() => { + return this._pdfDocument.getPage(i + 1).then(pdfPage => pdfPage.getTextContent(textOptions)).then(textContent => { + const strBuf = []; + for (const textItem of textContent.items) { + strBuf.push(textItem.str); + if (textItem.hasEOL) { + strBuf.push("\n"); + } + } + [this._pageContents[i], this._pageDiffs[i], this._hasDiacritics[i]] = normalize(strBuf.join("")); + resolve(); + }, reason => { + console.error(`Unable to get text content for page ${i + 1}`, reason); + this._pageContents[i] = ""; + this._pageDiffs[i] = null; + this._hasDiacritics[i] = false; + resolve(); + }); + }); + } + } + #updatePage(index) { + if (this._scrollMatches && this._selected.pageIdx === index) { + this._linkService.page = index + 1; + } + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: index + }); + } + #updateAllPages() { + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: -1 + }); + } + #nextMatch() { + const previous = this.#state.findPrevious; + const currentPageIndex = this._linkService.page - 1; + const numPages = this._linkService.pagesCount; + this._highlightMatches = true; + if (this._dirtyMatch) { + this._dirtyMatch = false; + this._selected.pageIdx = this._selected.matchIdx = -1; + this._offset.pageIdx = currentPageIndex; + this._offset.matchIdx = null; + this._offset.wrapped = false; + this._resumePageIdx = null; + this._pageMatches.length = 0; + this._pageMatchesLength.length = 0; + this.#visitedPagesCount = 0; + this._matchesCountTotal = 0; + this.#updateAllPages(); + for (let i = 0; i < numPages; i++) { + if (this._pendingFindMatches.has(i)) { + continue; + } + this._pendingFindMatches.add(i); + this._extractTextPromises[i].then(() => { + this._pendingFindMatches.delete(i); + this.#calculateMatch(i); + }); + } + } + const query = this.#query; + if (query.length === 0) { + this.#updateUIState(FindState.FOUND); + return; + } + if (this._resumePageIdx) { + return; + } + const offset = this._offset; + this._pagesToSearch = numPages; + if (offset.matchIdx !== null) { + const numPageMatches = this._pageMatches[offset.pageIdx].length; + if (!previous && offset.matchIdx + 1 < numPageMatches || previous && offset.matchIdx > 0) { + offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; + this.#updateMatch(true); + return; + } + this.#advanceOffsetPage(previous); + } + this.#nextPageMatch(); + } + #matchesReady(matches) { + const offset = this._offset; + const numMatches = matches.length; + const previous = this.#state.findPrevious; + if (numMatches) { + offset.matchIdx = previous ? numMatches - 1 : 0; + this.#updateMatch(true); + return true; + } + this.#advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (this._pagesToSearch < 0) { + this.#updateMatch(false); + return true; + } + } + return false; + } + #nextPageMatch() { + if (this._resumePageIdx !== null) { + console.error("There can only be one pending page."); + } + let matches = null; + do { + const pageIdx = this._offset.pageIdx; + matches = this._pageMatches[pageIdx]; + if (!matches) { + this._resumePageIdx = pageIdx; + break; + } + } while (!this.#matchesReady(matches)); + } + #advanceOffsetPage(previous) { + const offset = this._offset; + const numPages = this._linkService.pagesCount; + offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; + offset.matchIdx = null; + this._pagesToSearch--; + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = previous ? numPages - 1 : 0; + offset.wrapped = true; + } + } + #updateMatch(found = false) { + let state = FindState.NOT_FOUND; + const wrapped = this._offset.wrapped; + this._offset.wrapped = false; + if (found) { + const previousPage = this._selected.pageIdx; + this._selected.pageIdx = this._offset.pageIdx; + this._selected.matchIdx = this._offset.matchIdx; + state = wrapped ? FindState.WRAPPED : FindState.FOUND; + if (previousPage !== -1 && previousPage !== this._selected.pageIdx) { + this.#updatePage(previousPage); + } + } + this.#updateUIState(state, this.#state.findPrevious); + if (this._selected.pageIdx !== -1) { + this._scrollMatches = true; + this.#updatePage(this._selected.pageIdx); + } + } + #onFindBarClose(evt) { + const pdfDocument = this._pdfDocument; + this._firstPageCapability.promise.then(() => { + if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { + return; + } + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (this._resumePageIdx) { + this._resumePageIdx = null; + this._dirtyMatch = true; + } + this.#updateUIState(FindState.FOUND); + this._highlightMatches = false; + this.#updateAllPages(); + }); + } + #requestMatchesCount() { + const { + pageIdx, + matchIdx + } = this._selected; + let current = 0, + total = this._matchesCountTotal; + if (matchIdx !== -1) { + for (let i = 0; i < pageIdx; i++) { + current += this._pageMatches[i]?.length || 0; + } + current += matchIdx + 1; + } + if (current < 1 || current > total) { + current = total = 0; + } + return { + current, + total + }; + } + #updateUIResultsCount() { + this._eventBus.dispatch("updatefindmatchescount", { + source: this, + matchesCount: this.#requestMatchesCount() + }); + } + #updateUIState(state, previous = false) { + if (!this.#updateMatchesCountOnProgress && (this.#visitedPagesCount !== this._linkService.pagesCount || state === FindState.PENDING)) { + return; + } + this._eventBus.dispatch("updatefindcontrolstate", { + source: this, + state, + previous, + entireWord: this.#state?.entireWord ?? null, + matchesCount: this.#requestMatchesCount(), + rawQuery: this.#state?.query ?? null + }); + } +} + +;// ./web/pdf_link_service.js + +const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; +const LinkTarget = { + NONE: 0, + SELF: 1, + BLANK: 2, + PARENT: 3, + TOP: 4 +}; +class PDFLinkService { + externalLinkEnabled = true; + constructor({ + eventBus, + externalLinkTarget = null, + externalLinkRel = null, + ignoreDestinationZoom = false + } = {}) { + this.eventBus = eventBus; + this.externalLinkTarget = externalLinkTarget; + this.externalLinkRel = externalLinkRel; + this._ignoreDestinationZoom = ignoreDestinationZoom; + this.baseUrl = null; + this.pdfDocument = null; + this.pdfViewer = null; + this.pdfHistory = null; + } + setDocument(pdfDocument, baseUrl = null) { + this.baseUrl = baseUrl; + this.pdfDocument = pdfDocument; + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setHistory(pdfHistory) { + this.pdfHistory = pdfHistory; + } + get pagesCount() { + return this.pdfDocument ? this.pdfDocument.numPages : 0; + } + get page() { + return this.pdfDocument ? this.pdfViewer.currentPageNumber : 1; + } + set page(value) { + if (this.pdfDocument) { + this.pdfViewer.currentPageNumber = value; + } + } + get rotation() { + return this.pdfDocument ? this.pdfViewer.pagesRotation : 0; + } + set rotation(value) { + if (this.pdfDocument) { + this.pdfViewer.pagesRotation = value; + } + } + get isInPresentationMode() { + return this.pdfDocument ? this.pdfViewer.isInPresentationMode : false; + } + async goToDestination(dest) { + if (!this.pdfDocument) { + return; + } + let namedDest, explicitDest, pageNumber; + if (typeof dest === "string") { + namedDest = dest; + explicitDest = await this.pdfDocument.getDestination(dest); + } else { + namedDest = null; + explicitDest = await dest; + } + if (!Array.isArray(explicitDest)) { + console.error(`goToDestination: "${explicitDest}" is not a valid destination array, for dest="${dest}".`); + return; + } + const [destRef] = explicitDest; + if (destRef && typeof destRef === "object") { + pageNumber = this.pdfDocument.cachedPageNumber(destRef); + if (!pageNumber) { + try { + pageNumber = (await this.pdfDocument.getPageIndex(destRef)) + 1; + } catch { + console.error(`goToDestination: "${destRef}" is not a valid page reference, for dest="${dest}".`); + return; + } + } + } else if (Number.isInteger(destRef)) { + pageNumber = destRef + 1; + } + if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) { + console.error(`goToDestination: "${pageNumber}" is not a valid page number, for dest="${dest}".`); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.push({ + namedDest, + explicitDest, + pageNumber + }); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber, + destArray: explicitDest, + ignoreDestinationZoom: this._ignoreDestinationZoom + }); + } + goToPage(val) { + if (!this.pdfDocument) { + return; + } + const pageNumber = typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val) || val | 0; + if (!(Number.isInteger(pageNumber) && pageNumber > 0 && pageNumber <= this.pagesCount)) { + console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.pushPage(pageNumber); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber + }); + } + addLinkAttributes(link, url, newWindow = false) { + if (!url || typeof url !== "string") { + throw new Error('A valid "url" parameter must provided.'); + } + const target = newWindow ? LinkTarget.BLANK : this.externalLinkTarget, + rel = this.externalLinkRel; + if (this.externalLinkEnabled) { + link.href = link.title = url; + } else { + link.href = ""; + link.title = `Disabled: ${url}`; + link.onclick = () => false; + } + let targetStr = ""; + switch (target) { + case LinkTarget.NONE: + break; + case LinkTarget.SELF: + targetStr = "_self"; + break; + case LinkTarget.BLANK: + targetStr = "_blank"; + break; + case LinkTarget.PARENT: + targetStr = "_parent"; + break; + case LinkTarget.TOP: + targetStr = "_top"; + break; + } + link.target = targetStr; + link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL; + } + getDestinationHash(dest) { + if (typeof dest === "string") { + if (dest.length > 0) { + return this.getAnchorUrl("#" + escape(dest)); + } + } else if (Array.isArray(dest)) { + const str = JSON.stringify(dest); + if (str.length > 0) { + return this.getAnchorUrl("#" + escape(str)); + } + } + return this.getAnchorUrl(""); + } + getAnchorUrl(anchor) { + return this.baseUrl ? this.baseUrl + anchor : anchor; + } + setHash(hash) { + if (!this.pdfDocument) { + return; + } + let pageNumber, dest; + if (hash.includes("=")) { + const params = parseQueryString(hash); + if (params.has("search")) { + const query = params.get("search").replaceAll('"', ""), + phrase = params.get("phrase") === "true"; + this.eventBus.dispatch("findfromurlhash", { + source: this, + query: phrase ? query : query.match(/\S+/g) + }); + } + if (params.has("page")) { + pageNumber = params.get("page") | 0 || 1; + } + if (params.has("zoom")) { + const zoomArgs = params.get("zoom").split(","); + const zoomArg = zoomArgs[0]; + const zoomArgNumber = parseFloat(zoomArg); + if (!zoomArg.includes("Fit")) { + dest = [null, { + name: "XYZ" + }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null, zoomArgs.length > 2 ? zoomArgs[2] | 0 : null, zoomArgNumber ? zoomArgNumber / 100 : zoomArg]; + } else if (zoomArg === "Fit" || zoomArg === "FitB") { + dest = [null, { + name: zoomArg + }]; + } else if (zoomArg === "FitH" || zoomArg === "FitBH" || zoomArg === "FitV" || zoomArg === "FitBV") { + dest = [null, { + name: zoomArg + }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null]; + } else if (zoomArg === "FitR") { + if (zoomArgs.length !== 5) { + console.error('PDFLinkService.setHash: Not enough parameters for "FitR".'); + } else { + dest = [null, { + name: zoomArg + }, zoomArgs[1] | 0, zoomArgs[2] | 0, zoomArgs[3] | 0, zoomArgs[4] | 0]; + } + } else { + console.error(`PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`); + } + } + if (dest) { + this.pdfViewer.scrollPageIntoView({ + pageNumber: pageNumber || this.page, + destArray: dest, + allowNegativeOffset: true + }); + } else if (pageNumber) { + this.page = pageNumber; + } + if (params.has("pagemode")) { + this.eventBus.dispatch("pagemode", { + source: this, + mode: params.get("pagemode") + }); + } + if (params.has("nameddest")) { + this.goToDestination(params.get("nameddest")); + } + return; + } + dest = unescape(hash); + try { + dest = JSON.parse(dest); + if (!Array.isArray(dest)) { + dest = dest.toString(); + } + } catch {} + if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) { + this.goToDestination(dest); + return; + } + console.error(`PDFLinkService.setHash: "${unescape(hash)}" is not a valid destination.`); + } + executeNamedAction(action) { + if (!this.pdfDocument) { + return; + } + switch (action) { + case "GoBack": + this.pdfHistory?.back(); + break; + case "GoForward": + this.pdfHistory?.forward(); + break; + case "NextPage": + this.pdfViewer.nextPage(); + break; + case "PrevPage": + this.pdfViewer.previousPage(); + break; + case "LastPage": + this.page = this.pagesCount; + break; + case "FirstPage": + this.page = 1; + break; + default: + break; + } + this.eventBus.dispatch("namedaction", { + source: this, + action + }); + } + async executeSetOCGState(action) { + if (!this.pdfDocument) { + return; + } + const pdfDocument = this.pdfDocument, + optionalContentConfig = await this.pdfViewer.optionalContentConfigPromise; + if (pdfDocument !== this.pdfDocument) { + return; + } + optionalContentConfig.setOCGState(action); + this.pdfViewer.optionalContentConfigPromise = Promise.resolve(optionalContentConfig); + } + static #isValidExplicitDest(dest) { + if (!Array.isArray(dest) || dest.length < 2) { + return false; + } + const [page, zoom, ...args] = dest; + if (!(typeof page === "object" && Number.isInteger(page?.num) && Number.isInteger(page?.gen)) && !Number.isInteger(page)) { + return false; + } + if (!(typeof zoom === "object" && typeof zoom?.name === "string")) { + return false; + } + const argsLen = args.length; + let allowNull = true; + switch (zoom.name) { + case "XYZ": + if (argsLen < 2 || argsLen > 3) { + return false; + } + break; + case "Fit": + case "FitB": + return argsLen === 0; + case "FitH": + case "FitBH": + case "FitV": + case "FitBV": + if (argsLen > 1) { + return false; + } + break; + case "FitR": + if (argsLen !== 4) { + return false; + } + allowNull = false; + break; + default: + return false; + } + for (const arg of args) { + if (!(typeof arg === "number" || allowNull && arg === null)) { + return false; + } + } + return true; + } +} +class SimpleLinkService extends PDFLinkService { + setDocument(pdfDocument, baseUrl = null) {} +} + +;// ./web/pdfjs.js +const { + AbortException, + AnnotationEditorLayer, + AnnotationEditorParamsType, + AnnotationEditorType, + AnnotationEditorUIManager, + AnnotationLayer, + AnnotationMode, + build, + ColorPicker, + createValidAbsoluteUrl, + DOMSVGFactory, + DrawLayer, + FeatureTest, + fetchData, + getDocument, + getFilenameFromUrl, + getPdfFilenameFromUrl, + getXfaPageViewport, + GlobalWorkerOptions, + ImageKind, + InvalidPDFException, + isDataScheme, + isPdfFile, + MissingPDFException, + noContextMenu, + normalizeUnicode, + OPS, + OutputScale, + PasswordResponses, + PDFDataRangeTransport, + PDFDateString, + PDFWorker, + PermissionFlag, + PixelsPerInch, + RenderingCancelledException, + setLayerDimensions, + shadow, + stopEvent, + TextLayer, + TouchManager, + UnexpectedResponseException, + Util, + VerbosityLevel, + version, + XfaLayer +} = globalThis.pdfjsLib; + +;// ./web/annotation_layer_builder.js + + +class AnnotationLayerBuilder { + #onAppend = null; + #eventAbortController = null; + constructor({ + pdfPage, + linkService, + downloadManager, + annotationStorage = null, + imageResourcesPath = "", + renderForms = true, + enableScripting = false, + hasJSActionsPromise = null, + fieldObjectsPromise = null, + annotationCanvasMap = null, + accessibilityManager = null, + annotationEditorUIManager = null, + onAppend = null + }) { + this.pdfPage = pdfPage; + this.linkService = linkService; + this.downloadManager = downloadManager; + this.imageResourcesPath = imageResourcesPath; + this.renderForms = renderForms; + this.annotationStorage = annotationStorage; + this.enableScripting = enableScripting; + this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false); + this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null); + this._annotationCanvasMap = annotationCanvasMap; + this._accessibilityManager = accessibilityManager; + this._annotationEditorUIManager = annotationEditorUIManager; + this.#onAppend = onAppend; + this.annotationLayer = null; + this.div = null; + this._cancelled = false; + this._eventBus = linkService.eventBus; + } + async render(viewport, options, intent = "display") { + if (this.div) { + if (this._cancelled || !this.annotationLayer) { + return; + } + this.annotationLayer.update({ + viewport: viewport.clone({ + dontFlip: true + }) + }); + return; + } + const [annotations, hasJSActions, fieldObjects] = await Promise.all([this.pdfPage.getAnnotations({ + intent + }), this._hasJSActionsPromise, this._fieldObjectsPromise]); + if (this._cancelled) { + return; + } + const div = this.div = document.createElement("div"); + div.className = "annotationLayer"; + this.#onAppend?.(div); + if (annotations.length === 0) { + this.hide(); + return; + } + this.annotationLayer = new AnnotationLayer({ + div, + accessibilityManager: this._accessibilityManager, + annotationCanvasMap: this._annotationCanvasMap, + annotationEditorUIManager: this._annotationEditorUIManager, + page: this.pdfPage, + viewport: viewport.clone({ + dontFlip: true + }), + structTreeLayer: options?.structTreeLayer || null + }); + await this.annotationLayer.render({ + annotations, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.renderForms, + linkService: this.linkService, + downloadManager: this.downloadManager, + annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, + hasJSActions, + fieldObjects + }); + if (this.linkService.isInPresentationMode) { + this.#updatePresentationModeState(PresentationModeState.FULLSCREEN); + } + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this._eventBus?._on("presentationmodechanged", evt => { + this.#updatePresentationModeState(evt.state); + }, { + signal: this.#eventAbortController.signal + }); + } + } + cancel() { + this._cancelled = true; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { + if (!this.div) { + return; + } + let disableFormElements = false; + switch (state) { + case PresentationModeState.FULLSCREEN: + disableFormElements = true; + break; + case PresentationModeState.NORMAL: + break; + default: + return; + } + for (const section of this.div.childNodes) { + if (section.hasAttribute("data-internal-link")) { + continue; + } + section.inert = disableFormElements; + } + } +} + +;// ./web/download_manager.js + +function download(blobUrl, filename) { + const a = document.createElement("a"); + if (!a.click) { + throw new Error('DownloadManager: "a.click()" is not supported.'); + } + a.href = blobUrl; + a.target = "_parent"; + if ("download" in a) { + a.download = filename; + } + (document.body || document.documentElement).append(a); + a.click(); + a.remove(); +} +class DownloadManager { + #openBlobUrls = new WeakMap(); + downloadData(data, filename, contentType) { + const blobUrl = URL.createObjectURL(new Blob([data], { + type: contentType + })); + download(blobUrl, filename); + } + openOrDownloadData(data, filename, dest = null) { + const isPdfData = isPdfFile(filename); + const contentType = isPdfData ? "application/pdf" : ""; + this.downloadData(data, filename, contentType); + return false; + } + download(data, url, filename) { + let blobUrl; + if (data) { + blobUrl = URL.createObjectURL(new Blob([data], { + type: "application/pdf" + })); + } else { + if (!createValidAbsoluteUrl(url, "http://example.com")) { + console.error(`download - not a valid URL: ${url}`); + return; + } + blobUrl = url + "#pdfjs.action=download"; + } + download(blobUrl, filename); + } +} + +;// ./web/event_utils.js +const WaitOnType = { + EVENT: "event", + TIMEOUT: "timeout" +}; +async function waitOnEventOrTimeout({ + target, + name, + delay = 0 +}) { + if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { + throw new Error("waitOnEventOrTimeout - invalid parameters."); + } + const { + promise, + resolve + } = Promise.withResolvers(); + const ac = new AbortController(); + function handler(type) { + ac.abort(); + clearTimeout(timeout); + resolve(type); + } + const evtMethod = target instanceof EventBus ? "_on" : "addEventListener"; + target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), { + signal: ac.signal + }); + const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay); + return promise; +} +class EventBus { + #listeners = Object.create(null); + on(eventName, listener, options = null) { + this._on(eventName, listener, { + external: true, + once: options?.once, + signal: options?.signal + }); + } + off(eventName, listener, options = null) { + this._off(eventName, listener); + } + dispatch(eventName, data) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; + } + let externalListeners; + for (const { + listener, + external, + once + } of eventListeners.slice(0)) { + if (once) { + this._off(eventName, listener); + } + if (external) { + (externalListeners ||= []).push(listener); + continue; + } + listener(data); + } + if (externalListeners) { + for (const listener of externalListeners) { + listener(data); + } + externalListeners = null; + } + } + _on(eventName, listener, options = null) { + let rmAbort = null; + if (options?.signal instanceof AbortSignal) { + const { + signal + } = options; + if (signal.aborted) { + console.error("Cannot use an `aborted` signal."); + return; + } + const onAbort = () => this._off(eventName, listener); + rmAbort = () => signal.removeEventListener("abort", onAbort); + signal.addEventListener("abort", onAbort); + } + const eventListeners = this.#listeners[eventName] ||= []; + eventListeners.push({ + listener, + external: options?.external === true, + once: options?.once === true, + rmAbort + }); + } + _off(eventName, listener, options = null) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners) { + return; + } + for (let i = 0, ii = eventListeners.length; i < ii; i++) { + const evt = eventListeners[i]; + if (evt.listener === listener) { + evt.rmAbort?.(); + eventListeners.splice(i, 1); + return; + } + } + } +} +class FirefoxEventBus extends EventBus { + #externalServices; + #globalEventNames; + #isInAutomation; + constructor(globalEventNames, externalServices, isInAutomation) { + super(); + this.#globalEventNames = globalEventNames; + this.#externalServices = externalServices; + this.#isInAutomation = isInAutomation; + } + dispatch(eventName, data) { + throw new Error("Not implemented: FirefoxEventBus.dispatch"); + } +} + +;// ./node_modules/@fluent/bundle/esm/types.js +class FluentType { + constructor(value) { + this.value = value; + } + valueOf() { + return this.value; + } +} +class FluentNone extends FluentType { + constructor(value = "???") { + super(value); + } + toString(scope) { + return `{${this.value}}`; + } +} +class FluentNumber extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); + return nf.format(this.value); + } catch (err) { + scope.reportError(err); + return this.value.toString(10); + } + } +} +class FluentDateTime extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts); + return dtf.format(this.value); + } catch (err) { + scope.reportError(err); + return new Date(this.value).toISOString(); + } + } +} +;// ./node_modules/@fluent/bundle/esm/resolver.js + +const MAX_PLACEABLES = 100; +const FSI = "\u2068"; +const PDI = "\u2069"; +function match(scope, selector, key) { + if (key === selector) { + return true; + } + if (key instanceof FluentNumber && selector instanceof FluentNumber && key.value === selector.value) { + return true; + } + if (selector instanceof FluentNumber && typeof key === "string") { + let category = scope.memoizeIntlObject(Intl.PluralRules, selector.opts).select(selector.value); + if (key === category) { + return true; + } + } + return false; +} +function getDefault(scope, variants, star) { + if (variants[star]) { + return resolvePattern(scope, variants[star].value); + } + scope.reportError(new RangeError("No default")); + return new FluentNone(); +} +function getArguments(scope, args) { + const positional = []; + const named = Object.create(null); + for (const arg of args) { + if (arg.type === "narg") { + named[arg.name] = resolveExpression(scope, arg.value); + } else { + positional.push(resolveExpression(scope, arg)); + } + } + return { + positional, + named + }; +} +function resolveExpression(scope, expr) { + switch (expr.type) { + case "str": + return expr.value; + case "num": + return new FluentNumber(expr.value, { + minimumFractionDigits: expr.precision + }); + case "var": + return resolveVariableReference(scope, expr); + case "mesg": + return resolveMessageReference(scope, expr); + case "term": + return resolveTermReference(scope, expr); + case "func": + return resolveFunctionReference(scope, expr); + case "select": + return resolveSelectExpression(scope, expr); + default: + return new FluentNone(); + } +} +function resolveVariableReference(scope, { + name +}) { + let arg; + if (scope.params) { + if (Object.prototype.hasOwnProperty.call(scope.params, name)) { + arg = scope.params[name]; + } else { + return new FluentNone(`$${name}`); + } + } else if (scope.args && Object.prototype.hasOwnProperty.call(scope.args, name)) { + arg = scope.args[name]; + } else { + scope.reportError(new ReferenceError(`Unknown variable: $${name}`)); + return new FluentNone(`$${name}`); + } + if (arg instanceof FluentType) { + return arg; + } + switch (typeof arg) { + case "string": + return arg; + case "number": + return new FluentNumber(arg); + case "object": + if (arg instanceof Date) { + return new FluentDateTime(arg.getTime()); + } + default: + scope.reportError(new TypeError(`Variable type not supported: $${name}, ${typeof arg}`)); + return new FluentNone(`$${name}`); + } +} +function resolveMessageReference(scope, { + name, + attr +}) { + const message = scope.bundle._messages.get(name); + if (!message) { + scope.reportError(new ReferenceError(`Unknown message: ${name}`)); + return new FluentNone(name); + } + if (attr) { + const attribute = message.attributes[attr]; + if (attribute) { + return resolvePattern(scope, attribute); + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${name}.${attr}`); + } + if (message.value) { + return resolvePattern(scope, message.value); + } + scope.reportError(new ReferenceError(`No value: ${name}`)); + return new FluentNone(name); +} +function resolveTermReference(scope, { + name, + attr, + args +}) { + const id = `-${name}`; + const term = scope.bundle._terms.get(id); + if (!term) { + scope.reportError(new ReferenceError(`Unknown term: ${id}`)); + return new FluentNone(id); + } + if (attr) { + const attribute = term.attributes[attr]; + if (attribute) { + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, attribute); + scope.params = null; + return resolved; + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${id}.${attr}`); + } + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, term.value); + scope.params = null; + return resolved; +} +function resolveFunctionReference(scope, { + name, + args +}) { + let func = scope.bundle._functions[name]; + if (!func) { + scope.reportError(new ReferenceError(`Unknown function: ${name}()`)); + return new FluentNone(`${name}()`); + } + if (typeof func !== "function") { + scope.reportError(new TypeError(`Function ${name}() is not callable`)); + return new FluentNone(`${name}()`); + } + try { + let resolved = getArguments(scope, args); + return func(resolved.positional, resolved.named); + } catch (err) { + scope.reportError(err); + return new FluentNone(`${name}()`); + } +} +function resolveSelectExpression(scope, { + selector, + variants, + star +}) { + let sel = resolveExpression(scope, selector); + if (sel instanceof FluentNone) { + return getDefault(scope, variants, star); + } + for (const variant of variants) { + const key = resolveExpression(scope, variant.key); + if (match(scope, sel, key)) { + return resolvePattern(scope, variant.value); + } + } + return getDefault(scope, variants, star); +} +function resolveComplexPattern(scope, ptn) { + if (scope.dirty.has(ptn)) { + scope.reportError(new RangeError("Cyclic reference")); + return new FluentNone(); + } + scope.dirty.add(ptn); + const result = []; + const useIsolating = scope.bundle._useIsolating && ptn.length > 1; + for (const elem of ptn) { + if (typeof elem === "string") { + result.push(scope.bundle._transform(elem)); + continue; + } + scope.placeables++; + if (scope.placeables > MAX_PLACEABLES) { + scope.dirty.delete(ptn); + throw new RangeError(`Too many placeables expanded: ${scope.placeables}, ` + `max allowed is ${MAX_PLACEABLES}`); + } + if (useIsolating) { + result.push(FSI); + } + result.push(resolveExpression(scope, elem).toString(scope)); + if (useIsolating) { + result.push(PDI); + } + } + scope.dirty.delete(ptn); + return result.join(""); +} +function resolvePattern(scope, value) { + if (typeof value === "string") { + return scope.bundle._transform(value); + } + return resolveComplexPattern(scope, value); +} +;// ./node_modules/@fluent/bundle/esm/scope.js +class Scope { + constructor(bundle, errors, args) { + this.dirty = new WeakSet(); + this.params = null; + this.placeables = 0; + this.bundle = bundle; + this.errors = errors; + this.args = args; + } + reportError(error) { + if (!this.errors || !(error instanceof Error)) { + throw error; + } + this.errors.push(error); + } + memoizeIntlObject(ctor, opts) { + let cache = this.bundle._intls.get(ctor); + if (!cache) { + cache = {}; + this.bundle._intls.set(ctor, cache); + } + let id = JSON.stringify(opts); + if (!cache[id]) { + cache[id] = new ctor(this.bundle.locales, opts); + } + return cache[id]; + } +} +;// ./node_modules/@fluent/bundle/esm/builtins.js + +function values(opts, allowed) { + const unwrapped = Object.create(null); + for (const [name, opt] of Object.entries(opts)) { + if (allowed.includes(name)) { + unwrapped[name] = opt.valueOf(); + } + } + return unwrapped; +} +const NUMBER_ALLOWED = ["unitDisplay", "currencyDisplay", "useGrouping", "minimumIntegerDigits", "minimumFractionDigits", "maximumFractionDigits", "minimumSignificantDigits", "maximumSignificantDigits"]; +function NUMBER(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`NUMBER(${arg.valueOf()})`); + } + if (arg instanceof FluentNumber) { + return new FluentNumber(arg.valueOf(), { + ...arg.opts, + ...values(opts, NUMBER_ALLOWED) + }); + } + if (arg instanceof FluentDateTime) { + return new FluentNumber(arg.valueOf(), { + ...values(opts, NUMBER_ALLOWED) + }); + } + throw new TypeError("Invalid argument to NUMBER"); +} +const DATETIME_ALLOWED = ["dateStyle", "timeStyle", "fractionalSecondDigits", "dayPeriod", "hour12", "weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName"]; +function DATETIME(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`DATETIME(${arg.valueOf()})`); + } + if (arg instanceof FluentDateTime) { + return new FluentDateTime(arg.valueOf(), { + ...arg.opts, + ...values(opts, DATETIME_ALLOWED) + }); + } + if (arg instanceof FluentNumber) { + return new FluentDateTime(arg.valueOf(), { + ...values(opts, DATETIME_ALLOWED) + }); + } + throw new TypeError("Invalid argument to DATETIME"); +} +;// ./node_modules/@fluent/bundle/esm/memoizer.js +const cache = new Map(); +function getMemoizerForLocale(locales) { + const stringLocale = Array.isArray(locales) ? locales.join(" ") : locales; + let memoizer = cache.get(stringLocale); + if (memoizer === undefined) { + memoizer = new Map(); + cache.set(stringLocale, memoizer); + } + return memoizer; +} +;// ./node_modules/@fluent/bundle/esm/bundle.js + + + + + +class FluentBundle { + constructor(locales, { + functions, + useIsolating = true, + transform = v => v + } = {}) { + this._terms = new Map(); + this._messages = new Map(); + this.locales = Array.isArray(locales) ? locales : [locales]; + this._functions = { + NUMBER: NUMBER, + DATETIME: DATETIME, + ...functions + }; + this._useIsolating = useIsolating; + this._transform = transform; + this._intls = getMemoizerForLocale(locales); + } + hasMessage(id) { + return this._messages.has(id); + } + getMessage(id) { + return this._messages.get(id); + } + addResource(res, { + allowOverrides = false + } = {}) { + const errors = []; + for (let i = 0; i < res.body.length; i++) { + let entry = res.body[i]; + if (entry.id.startsWith("-")) { + if (allowOverrides === false && this._terms.has(entry.id)) { + errors.push(new Error(`Attempt to override an existing term: "${entry.id}"`)); + continue; + } + this._terms.set(entry.id, entry); + } else { + if (allowOverrides === false && this._messages.has(entry.id)) { + errors.push(new Error(`Attempt to override an existing message: "${entry.id}"`)); + continue; + } + this._messages.set(entry.id, entry); + } + } + return errors; + } + formatPattern(pattern, args = null, errors = null) { + if (typeof pattern === "string") { + return this._transform(pattern); + } + let scope = new Scope(this, errors, args); + try { + let value = resolveComplexPattern(scope, pattern); + return value.toString(scope); + } catch (err) { + if (scope.errors && err instanceof Error) { + scope.errors.push(err); + return new FluentNone().toString(scope); + } + throw err; + } + } +} +;// ./node_modules/@fluent/bundle/esm/resource.js +const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm; +const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y; +const RE_VARIANT_START = /\*?\[/y; +const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y; +const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y; +const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y; +const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/; +const RE_TEXT_RUN = /([^{}\n\r]+)/y; +const RE_STRING_RUN = /([^\\"\n\r]*)/y; +const RE_STRING_ESCAPE = /\\([\\"])/y; +const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y; +const RE_LEADING_NEWLINES = /^\n+/; +const RE_TRAILING_SPACES = / +$/; +const RE_BLANK_LINES = / *\r?\n/g; +const RE_INDENT = /( *)$/; +const TOKEN_BRACE_OPEN = /{\s*/y; +const TOKEN_BRACE_CLOSE = /\s*}/y; +const TOKEN_BRACKET_OPEN = /\[\s*/y; +const TOKEN_BRACKET_CLOSE = /\s*] */y; +const TOKEN_PAREN_OPEN = /\s*\(\s*/y; +const TOKEN_ARROW = /\s*->\s*/y; +const TOKEN_COLON = /\s*:\s*/y; +const TOKEN_COMMA = /\s*,?\s*/y; +const TOKEN_BLANK = /\s+/y; +class FluentResource { + constructor(source) { + this.body = []; + RE_MESSAGE_START.lastIndex = 0; + let cursor = 0; + while (true) { + let next = RE_MESSAGE_START.exec(source); + if (next === null) { + break; + } + cursor = RE_MESSAGE_START.lastIndex; + try { + this.body.push(parseMessage(next[1])); + } catch (err) { + if (err instanceof SyntaxError) { + continue; + } + throw err; + } + } + function test(re) { + re.lastIndex = cursor; + return re.test(source); + } + function consumeChar(char, errorClass) { + if (source[cursor] === char) { + cursor++; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${char}`); + } + return false; + } + function consumeToken(re, errorClass) { + if (test(re)) { + cursor = re.lastIndex; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${re.toString()}`); + } + return false; + } + function match(re) { + re.lastIndex = cursor; + let result = re.exec(source); + if (result === null) { + throw new SyntaxError(`Expected ${re.toString()}`); + } + cursor = re.lastIndex; + return result; + } + function match1(re) { + return match(re)[1]; + } + function parseMessage(id) { + let value = parsePattern(); + let attributes = parseAttributes(); + if (value === null && Object.keys(attributes).length === 0) { + throw new SyntaxError("Expected message value or attributes"); + } + return { + id, + value, + attributes + }; + } + function parseAttributes() { + let attrs = Object.create(null); + while (test(RE_ATTRIBUTE_START)) { + let name = match1(RE_ATTRIBUTE_START); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected attribute value"); + } + attrs[name] = value; + } + return attrs; + } + function parsePattern() { + let first; + if (test(RE_TEXT_RUN)) { + first = match1(RE_TEXT_RUN); + } + if (source[cursor] === "{" || source[cursor] === "}") { + return parsePatternElements(first ? [first] : [], Infinity); + } + let indent = parseIndent(); + if (indent) { + if (first) { + return parsePatternElements([first, indent], indent.length); + } + indent.value = trim(indent.value, RE_LEADING_NEWLINES); + return parsePatternElements([indent], indent.length); + } + if (first) { + return trim(first, RE_TRAILING_SPACES); + } + return null; + } + function parsePatternElements(elements = [], commonIndent) { + while (true) { + if (test(RE_TEXT_RUN)) { + elements.push(match1(RE_TEXT_RUN)); + continue; + } + if (source[cursor] === "{") { + elements.push(parsePlaceable()); + continue; + } + if (source[cursor] === "}") { + throw new SyntaxError("Unbalanced closing brace"); + } + let indent = parseIndent(); + if (indent) { + elements.push(indent); + commonIndent = Math.min(commonIndent, indent.length); + continue; + } + break; + } + let lastIndex = elements.length - 1; + let lastElement = elements[lastIndex]; + if (typeof lastElement === "string") { + elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES); + } + let baked = []; + for (let element of elements) { + if (element instanceof Indent) { + element = element.value.slice(0, element.value.length - commonIndent); + } + if (element) { + baked.push(element); + } + } + return baked; + } + function parsePlaceable() { + consumeToken(TOKEN_BRACE_OPEN, SyntaxError); + let selector = parseInlineExpression(); + if (consumeToken(TOKEN_BRACE_CLOSE)) { + return selector; + } + if (consumeToken(TOKEN_ARROW)) { + let variants = parseVariants(); + consumeToken(TOKEN_BRACE_CLOSE, SyntaxError); + return { + type: "select", + selector, + ...variants + }; + } + throw new SyntaxError("Unclosed placeable"); + } + function parseInlineExpression() { + if (source[cursor] === "{") { + return parsePlaceable(); + } + if (test(RE_REFERENCE)) { + let [, sigil, name, attr = null] = match(RE_REFERENCE); + if (sigil === "$") { + return { + type: "var", + name + }; + } + if (consumeToken(TOKEN_PAREN_OPEN)) { + let args = parseArguments(); + if (sigil === "-") { + return { + type: "term", + name, + attr, + args + }; + } + if (RE_FUNCTION_NAME.test(name)) { + return { + type: "func", + name, + args + }; + } + throw new SyntaxError("Function names must be all upper-case"); + } + if (sigil === "-") { + return { + type: "term", + name, + attr, + args: [] + }; + } + return { + type: "mesg", + name, + attr + }; + } + return parseLiteral(); + } + function parseArguments() { + let args = []; + while (true) { + switch (source[cursor]) { + case ")": + cursor++; + return args; + case undefined: + throw new SyntaxError("Unclosed argument list"); + } + args.push(parseArgument()); + consumeToken(TOKEN_COMMA); + } + } + function parseArgument() { + let expr = parseInlineExpression(); + if (expr.type !== "mesg") { + return expr; + } + if (consumeToken(TOKEN_COLON)) { + return { + type: "narg", + name: expr.name, + value: parseLiteral() + }; + } + return expr; + } + function parseVariants() { + let variants = []; + let count = 0; + let star; + while (test(RE_VARIANT_START)) { + if (consumeChar("*")) { + star = count; + } + let key = parseVariantKey(); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected variant value"); + } + variants[count++] = { + key, + value + }; + } + if (count === 0) { + return null; + } + if (star === undefined) { + throw new SyntaxError("Expected default variant"); + } + return { + variants, + star + }; + } + function parseVariantKey() { + consumeToken(TOKEN_BRACKET_OPEN, SyntaxError); + let key; + if (test(RE_NUMBER_LITERAL)) { + key = parseNumberLiteral(); + } else { + key = { + type: "str", + value: match1(RE_IDENTIFIER) + }; + } + consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError); + return key; + } + function parseLiteral() { + if (test(RE_NUMBER_LITERAL)) { + return parseNumberLiteral(); + } + if (source[cursor] === '"') { + return parseStringLiteral(); + } + throw new SyntaxError("Invalid expression"); + } + function parseNumberLiteral() { + let [, value, fraction = ""] = match(RE_NUMBER_LITERAL); + let precision = fraction.length; + return { + type: "num", + value: parseFloat(value), + precision + }; + } + function parseStringLiteral() { + consumeChar('"', SyntaxError); + let value = ""; + while (true) { + value += match1(RE_STRING_RUN); + if (source[cursor] === "\\") { + value += parseEscapeSequence(); + continue; + } + if (consumeChar('"')) { + return { + type: "str", + value + }; + } + throw new SyntaxError("Unclosed string literal"); + } + } + function parseEscapeSequence() { + if (test(RE_STRING_ESCAPE)) { + return match1(RE_STRING_ESCAPE); + } + if (test(RE_UNICODE_ESCAPE)) { + let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE); + let codepoint = parseInt(codepoint4 || codepoint6, 16); + return codepoint <= 0xd7ff || 0xe000 <= codepoint ? String.fromCodePoint(codepoint) : "�"; + } + throw new SyntaxError("Unknown escape sequence"); + } + function parseIndent() { + let start = cursor; + consumeToken(TOKEN_BLANK); + switch (source[cursor]) { + case ".": + case "[": + case "*": + case "}": + case undefined: + return false; + case "{": + return makeIndent(source.slice(start, cursor)); + } + if (source[cursor - 1] === " ") { + return makeIndent(source.slice(start, cursor)); + } + return false; + } + function trim(text, re) { + return text.replace(re, ""); + } + function makeIndent(blank) { + let value = blank.replace(RE_BLANK_LINES, "\n"); + let length = RE_INDENT.exec(blank)[1].length; + return new Indent(value, length); + } + } +} +class Indent { + constructor(value, length) { + this.value = value; + this.length = length; + } +} +;// ./node_modules/@fluent/bundle/esm/index.js + + + +;// ./node_modules/@fluent/dom/esm/overlay.js +const reOverlay = /<|&#?\w+;/; +const TEXT_LEVEL_ELEMENTS = { + "http://www.w3.org/1999/xhtml": ["em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data", "time", "code", "var", "samp", "kbd", "sub", "sup", "i", "b", "u", "mark", "bdi", "bdo", "span", "br", "wbr"] +}; +const LOCALIZABLE_ATTRIBUTES = { + "http://www.w3.org/1999/xhtml": { + global: ["title", "aria-label", "aria-valuetext"], + a: ["download"], + area: ["download", "alt"], + input: ["alt", "placeholder"], + menuitem: ["label"], + menu: ["label"], + optgroup: ["label"], + option: ["label"], + track: ["label"], + img: ["alt"], + textarea: ["placeholder"], + th: ["abbr"] + }, + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": { + global: ["accesskey", "aria-label", "aria-valuetext", "label", "title", "tooltiptext"], + description: ["value"], + key: ["key", "keycode"], + label: ["value"], + textbox: ["placeholder", "value"] + } +}; +function translateElement(element, translation) { + const { + value + } = translation; + if (typeof value === "string") { + if (element.localName === "title" && element.namespaceURI === "http://www.w3.org/1999/xhtml") { + element.textContent = value; + } else if (!reOverlay.test(value)) { + element.textContent = value; + } else { + const templateElement = element.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "template"); + templateElement.innerHTML = value; + overlayChildNodes(templateElement.content, element); + } + } + overlayAttributes(translation, element); +} +function overlayChildNodes(fromFragment, toElement) { + for (const childNode of fromFragment.childNodes) { + if (childNode.nodeType === childNode.TEXT_NODE) { + continue; + } + if (childNode.hasAttribute("data-l10n-name")) { + const sanitized = getNodeForNamedElement(toElement, childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + if (isElementAllowed(childNode)) { + const sanitized = createSanitizedElement(childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + console.warn(`An element of forbidden type "${childNode.localName}" was found in ` + "the translation. Only safe text-level elements and elements with " + "data-l10n-name are allowed."); + fromFragment.replaceChild(createTextNodeFromTextContent(childNode), childNode); + } + toElement.textContent = ""; + toElement.appendChild(fromFragment); +} +function hasAttribute(attributes, name) { + if (!attributes) { + return false; + } + for (let attr of attributes) { + if (attr.name === name) { + return true; + } + } + return false; +} +function overlayAttributes(fromElement, toElement) { + const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs") ? toElement.getAttribute("data-l10n-attrs").split(",").map(i => i.trim()) : null; + for (const attr of Array.from(toElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && !hasAttribute(fromElement.attributes, attr.name)) { + toElement.removeAttribute(attr.name); + } + } + if (!fromElement.attributes) { + return; + } + for (const attr of Array.from(fromElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && toElement.getAttribute(attr.name) !== attr.value) { + toElement.setAttribute(attr.name, attr.value); + } + } +} +function getNodeForNamedElement(sourceElement, translatedChild) { + const childName = translatedChild.getAttribute("data-l10n-name"); + const sourceChild = sourceElement.querySelector(`[data-l10n-name="${childName}"]`); + if (!sourceChild) { + console.warn(`An element named "${childName}" wasn't found in the source.`); + return createTextNodeFromTextContent(translatedChild); + } + if (sourceChild.localName !== translatedChild.localName) { + console.warn(`An element named "${childName}" was found in the translation ` + `but its type ${translatedChild.localName} didn't match the ` + `element found in the source (${sourceChild.localName}).`); + return createTextNodeFromTextContent(translatedChild); + } + sourceElement.removeChild(sourceChild); + const clone = sourceChild.cloneNode(false); + return shallowPopulateUsing(translatedChild, clone); +} +function createSanitizedElement(element) { + const clone = element.ownerDocument.createElement(element.localName); + return shallowPopulateUsing(element, clone); +} +function createTextNodeFromTextContent(element) { + return element.ownerDocument.createTextNode(element.textContent); +} +function isElementAllowed(element) { + const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI]; + return allowed && allowed.includes(element.localName); +} +function isAttrNameLocalizable(name, element, explicitlyAllowed = null) { + if (explicitlyAllowed && explicitlyAllowed.includes(name)) { + return true; + } + const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI]; + if (!allowed) { + return false; + } + const attrName = name.toLowerCase(); + const elemName = element.localName; + if (allowed.global.includes(attrName)) { + return true; + } + if (!allowed[elemName]) { + return false; + } + if (allowed[elemName].includes(attrName)) { + return true; + } + if (element.namespaceURI === "http://www.w3.org/1999/xhtml" && elemName === "input" && attrName === "value") { + const type = element.type.toLowerCase(); + if (type === "submit" || type === "button" || type === "reset") { + return true; + } + } + return false; +} +function shallowPopulateUsing(fromElement, toElement) { + toElement.textContent = fromElement.textContent; + overlayAttributes(fromElement, toElement); + return toElement; +} +;// ./node_modules/cached-iterable/src/cached_iterable.mjs +class CachedIterable extends Array { + static from(iterable) { + if (iterable instanceof this) { + return iterable; + } + return new this(iterable); + } +} +;// ./node_modules/cached-iterable/src/cached_sync_iterable.mjs + +class CachedSyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.iterator]() { + const cached = this; + let cur = 0; + return { + next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + } + }; + } + touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && last.done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} +;// ./node_modules/cached-iterable/src/cached_async_iterable.mjs + +class CachedAsyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.asyncIterator in Object(iterable)) { + this.iterator = iterable[Symbol.asyncIterator](); + } else if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.asyncIterator]() { + const cached = this; + let cur = 0; + return { + async next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + } + }; + } + async touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && (await last).done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} +;// ./node_modules/cached-iterable/src/index.mjs + + +;// ./node_modules/@fluent/dom/esm/localization.js + +class Localization { + constructor(resourceIds = [], generateBundles) { + this.resourceIds = resourceIds; + this.generateBundles = generateBundles; + this.onChange(true); + } + addResourceIds(resourceIds, eager = false) { + this.resourceIds.push(...resourceIds); + this.onChange(eager); + return this.resourceIds.length; + } + removeResourceIds(resourceIds) { + this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r)); + this.onChange(); + return this.resourceIds.length; + } + async formatWithFallback(keys, method) { + const translations = []; + let hasAtLeastOneBundle = false; + for await (const bundle of this.bundles) { + hasAtLeastOneBundle = true; + const missingIds = keysFromBundle(method, bundle, keys, translations); + if (missingIds.size === 0) { + break; + } + if (typeof console !== "undefined") { + const locale = bundle.locales[0]; + const ids = Array.from(missingIds).join(", "); + console.warn(`[fluent] Missing translations in ${locale}: ${ids}`); + } + } + if (!hasAtLeastOneBundle && typeof console !== "undefined") { + console.warn(`[fluent] Request for keys failed because no resource bundles got generated. + keys: ${JSON.stringify(keys)}. + resourceIds: ${JSON.stringify(this.resourceIds)}.`); + } + return translations; + } + formatMessages(keys) { + return this.formatWithFallback(keys, messageFromBundle); + } + formatValues(keys) { + return this.formatWithFallback(keys, valueFromBundle); + } + async formatValue(id, args) { + const [val] = await this.formatValues([{ + id, + args + }]); + return val; + } + handleEvent() { + this.onChange(); + } + onChange(eager = false) { + this.bundles = CachedAsyncIterable.from(this.generateBundles(this.resourceIds)); + if (eager) { + this.bundles.touchNext(2); + } + } +} +function valueFromBundle(bundle, errors, message, args) { + if (message.value) { + return bundle.formatPattern(message.value, args, errors); + } + return null; +} +function messageFromBundle(bundle, errors, message, args) { + const formatted = { + value: null, + attributes: null + }; + if (message.value) { + formatted.value = bundle.formatPattern(message.value, args, errors); + } + let attrNames = Object.keys(message.attributes); + if (attrNames.length > 0) { + formatted.attributes = new Array(attrNames.length); + for (let [i, name] of attrNames.entries()) { + let value = bundle.formatPattern(message.attributes[name], args, errors); + formatted.attributes[i] = { + name, + value + }; + } + } + return formatted; +} +function keysFromBundle(method, bundle, keys, translations) { + const messageErrors = []; + const missingIds = new Set(); + keys.forEach(({ + id, + args + }, i) => { + if (translations[i] !== undefined) { + return; + } + let message = bundle.getMessage(id); + if (message) { + messageErrors.length = 0; + translations[i] = method(bundle, messageErrors, message, args); + if (messageErrors.length > 0 && typeof console !== "undefined") { + const locale = bundle.locales[0]; + const errors = messageErrors.join(", "); + console.warn(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`); + } + } else { + missingIds.add(id); + } + }); + return missingIds; +} +;// ./node_modules/@fluent/dom/esm/dom_localization.js + + +const L10NID_ATTR_NAME = "data-l10n-id"; +const L10NARGS_ATTR_NAME = "data-l10n-args"; +const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`; +class DOMLocalization extends Localization { + constructor(resourceIds, generateBundles) { + super(resourceIds, generateBundles); + this.roots = new Set(); + this.pendingrAF = null; + this.pendingElements = new Set(); + this.windowElement = null; + this.mutationObserver = null; + this.observerConfig = { + attributes: true, + characterData: false, + childList: true, + subtree: true, + attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME] + }; + } + onChange(eager = false) { + super.onChange(eager); + if (this.roots) { + this.translateRoots(); + } + } + setAttributes(element, id, args) { + element.setAttribute(L10NID_ATTR_NAME, id); + if (args) { + element.setAttribute(L10NARGS_ATTR_NAME, JSON.stringify(args)); + } else { + element.removeAttribute(L10NARGS_ATTR_NAME); + } + return element; + } + getAttributes(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) + }; + } + connectRoot(newRoot) { + for (const root of this.roots) { + if (root === newRoot || root.contains(newRoot) || newRoot.contains(root)) { + throw new Error("Cannot add a root that overlaps with existing root."); + } + } + if (this.windowElement) { + if (this.windowElement !== newRoot.ownerDocument.defaultView) { + throw new Error(`Cannot connect a root: + DOMLocalization already has a root from a different window.`); + } + } else { + this.windowElement = newRoot.ownerDocument.defaultView; + this.mutationObserver = new this.windowElement.MutationObserver(mutations => this.translateMutations(mutations)); + } + this.roots.add(newRoot); + this.mutationObserver.observe(newRoot, this.observerConfig); + } + disconnectRoot(root) { + this.roots.delete(root); + this.pauseObserving(); + if (this.roots.size === 0) { + this.mutationObserver = null; + if (this.windowElement && this.pendingrAF) { + this.windowElement.cancelAnimationFrame(this.pendingrAF); + } + this.windowElement = null; + this.pendingrAF = null; + this.pendingElements.clear(); + return true; + } + this.resumeObserving(); + return false; + } + translateRoots() { + const roots = Array.from(this.roots); + return Promise.all(roots.map(root => this.translateFragment(root))); + } + pauseObserving() { + if (!this.mutationObserver) { + return; + } + this.translateMutations(this.mutationObserver.takeRecords()); + this.mutationObserver.disconnect(); + } + resumeObserving() { + if (!this.mutationObserver) { + return; + } + for (const root of this.roots) { + this.mutationObserver.observe(root, this.observerConfig); + } + } + translateMutations(mutations) { + for (const mutation of mutations) { + switch (mutation.type) { + case "attributes": + if (mutation.target.hasAttribute("data-l10n-id")) { + this.pendingElements.add(mutation.target); + } + break; + case "childList": + for (const addedNode of mutation.addedNodes) { + if (addedNode.nodeType === addedNode.ELEMENT_NODE) { + if (addedNode.childElementCount) { + for (const element of this.getTranslatables(addedNode)) { + this.pendingElements.add(element); + } + } else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) { + this.pendingElements.add(addedNode); + } + } + } + break; + } + } + if (this.pendingElements.size > 0) { + if (this.pendingrAF === null) { + this.pendingrAF = this.windowElement.requestAnimationFrame(() => { + this.translateElements(Array.from(this.pendingElements)); + this.pendingElements.clear(); + this.pendingrAF = null; + }); + } + } + } + translateFragment(frag) { + return this.translateElements(this.getTranslatables(frag)); + } + async translateElements(elements) { + if (!elements.length) { + return undefined; + } + const keys = elements.map(this.getKeysForElement); + const translations = await this.formatMessages(keys); + return this.applyTranslations(elements, translations); + } + applyTranslations(elements, translations) { + this.pauseObserving(); + for (let i = 0; i < elements.length; i++) { + if (translations[i] !== undefined) { + translateElement(elements[i], translations[i]); + } + } + this.resumeObserving(); + } + getTranslatables(element) { + const nodes = Array.from(element.querySelectorAll(L10N_ELEMENT_QUERY)); + if (typeof element.hasAttribute === "function" && element.hasAttribute(L10NID_ATTR_NAME)) { + nodes.push(element); + } + return nodes; + } + getKeysForElement(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) + }; + } +} +;// ./node_modules/@fluent/dom/esm/index.js + + +;// ./web/l10n.js +class L10n { + #dir; + #elements; + #lang; + #l10n; + constructor({ + lang, + isRTL + }, l10n = null) { + this.#lang = L10n.#fixupLangCode(lang); + this.#l10n = l10n; + this.#dir = isRTL ?? L10n.#isRTL(this.#lang) ? "rtl" : "ltr"; + } + _setL10n(l10n) { + this.#l10n = l10n; + } + getLanguage() { + return this.#lang; + } + getDirection() { + return this.#dir; + } + async get(ids, args = null, fallback) { + if (Array.isArray(ids)) { + ids = ids.map(id => ({ + id + })); + const messages = await this.#l10n.formatMessages(ids); + return messages.map(message => message.value); + } + const messages = await this.#l10n.formatMessages([{ + id: ids, + args + }]); + return messages[0]?.value || fallback; + } + async translate(element) { + (this.#elements ||= new Set()).add(element); + try { + this.#l10n.connectRoot(element); + await this.#l10n.translateRoots(); + } catch {} + } + async translateOnce(element) { + try { + await this.#l10n.translateElements([element]); + } catch (ex) { + console.error("translateOnce:", ex); + } + } + async destroy() { + if (this.#elements) { + for (const element of this.#elements) { + this.#l10n.disconnectRoot(element); + } + this.#elements.clear(); + this.#elements = null; + } + this.#l10n.pauseObserving(); + } + pause() { + this.#l10n.pauseObserving(); + } + resume() { + this.#l10n.resumeObserving(); + } + static #fixupLangCode(langCode) { + langCode = langCode?.toLowerCase() || "en-us"; + const PARTIAL_LANG_CODES = { + en: "en-us", + es: "es-es", + fy: "fy-nl", + ga: "ga-ie", + gu: "gu-in", + hi: "hi-in", + hy: "hy-am", + nb: "nb-no", + ne: "ne-np", + nn: "nn-no", + pa: "pa-in", + pt: "pt-pt", + sv: "sv-se", + zh: "zh-cn" + }; + return PARTIAL_LANG_CODES[langCode] || langCode; + } + static #isRTL(lang) { + const shortCode = lang.split("-", 1)[0]; + return ["ar", "he", "fa", "ps", "ur"].includes(shortCode); + } +} +const GenericL10n = null; + +;// ./web/genericl10n.js + + + + +function createBundle(lang, text) { + const resource = new FluentResource(text); + const bundle = new FluentBundle(lang); + const errors = bundle.addResource(resource); + if (errors.length) { + console.error("L10n errors", errors); + } + return bundle; +} +class genericl10n_GenericL10n extends L10n { + constructor(lang) { + super({ + lang + }); + const generateBundles = !lang ? genericl10n_GenericL10n.#generateBundlesFallback.bind(genericl10n_GenericL10n, this.getLanguage()) : genericl10n_GenericL10n.#generateBundles.bind(genericl10n_GenericL10n, "en-us", this.getLanguage()); + this._setL10n(new DOMLocalization([], generateBundles)); + } + static async *#generateBundles(defaultLang, baseLang) { + const { + baseURL, + paths + } = await this.#getPaths(); + const langs = [baseLang]; + if (defaultLang !== baseLang) { + const shortLang = baseLang.split("-", 1)[0]; + if (shortLang !== baseLang) { + langs.push(shortLang); + } + langs.push(defaultLang); + } + for (const lang of langs) { + const bundle = await this.#createBundle(lang, baseURL, paths); + if (bundle) { + yield bundle; + } else if (lang === "en-us") { + yield this.#createBundleFallback(lang); + } + } + } + static async #createBundle(lang, baseURL, paths) { + const path = paths[lang]; + if (!path) { + return null; + } + const url = new URL(path, baseURL); + const text = await fetchData(url, "text"); + return createBundle(lang, text); + } + static async #getPaths() { + try { + const { + href + } = document.querySelector(`link[type="application/l10n"]`); + const paths = await fetchData(href, "json"); + return { + baseURL: href.replace(/[^/]*$/, "") || "./", + paths + }; + } catch {} + return { + baseURL: "./", + paths: Object.create(null) + }; + } + static async *#generateBundlesFallback(lang) { + yield this.#createBundleFallback(lang); + } + static async #createBundleFallback(lang) { + const text = "pdfjs-previous-button =\n .title = Previous Page\npdfjs-previous-button-label = Previous\npdfjs-next-button =\n .title = Next Page\npdfjs-next-button-label = Next\npdfjs-page-input =\n .title = Page\npdfjs-of-pages = of { $pagesCount }\npdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount })\npdfjs-zoom-out-button =\n .title = Zoom Out\npdfjs-zoom-out-button-label = Zoom Out\npdfjs-zoom-in-button =\n .title = Zoom In\npdfjs-zoom-in-button-label = Zoom In\npdfjs-zoom-select =\n .title = Zoom\npdfjs-presentation-mode-button =\n .title = Switch to Presentation Mode\npdfjs-presentation-mode-button-label = Presentation Mode\npdfjs-open-file-button =\n .title = Open File\npdfjs-open-file-button-label = Open\npdfjs-print-button =\n .title = Print\npdfjs-print-button-label = Print\npdfjs-save-button =\n .title = Save\npdfjs-save-button-label = Save\npdfjs-download-button =\n .title = Download\npdfjs-download-button-label = Download\npdfjs-bookmark-button =\n .title = Current Page (View URL from Current Page)\npdfjs-bookmark-button-label = Current Page\npdfjs-tools-button =\n .title = Tools\npdfjs-tools-button-label = Tools\npdfjs-first-page-button =\n .title = Go to First Page\npdfjs-first-page-button-label = Go to First Page\npdfjs-last-page-button =\n .title = Go to Last Page\npdfjs-last-page-button-label = Go to Last Page\npdfjs-page-rotate-cw-button =\n .title = Rotate Clockwise\npdfjs-page-rotate-cw-button-label = Rotate Clockwise\npdfjs-page-rotate-ccw-button =\n .title = Rotate Counterclockwise\npdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise\npdfjs-cursor-text-select-tool-button =\n .title = Enable Text Selection Tool\npdfjs-cursor-text-select-tool-button-label = Text Selection Tool\npdfjs-cursor-hand-tool-button =\n .title = Enable Hand Tool\npdfjs-cursor-hand-tool-button-label = Hand Tool\npdfjs-scroll-page-button =\n .title = Use Page Scrolling\npdfjs-scroll-page-button-label = Page Scrolling\npdfjs-scroll-vertical-button =\n .title = Use Vertical Scrolling\npdfjs-scroll-vertical-button-label = Vertical Scrolling\npdfjs-scroll-horizontal-button =\n .title = Use Horizontal Scrolling\npdfjs-scroll-horizontal-button-label = Horizontal Scrolling\npdfjs-scroll-wrapped-button =\n .title = Use Wrapped Scrolling\npdfjs-scroll-wrapped-button-label = Wrapped Scrolling\npdfjs-spread-none-button =\n .title = Do not join page spreads\npdfjs-spread-none-button-label = No Spreads\npdfjs-spread-odd-button =\n .title = Join page spreads starting with odd-numbered pages\npdfjs-spread-odd-button-label = Odd Spreads\npdfjs-spread-even-button =\n .title = Join page spreads starting with even-numbered pages\npdfjs-spread-even-button-label = Even Spreads\npdfjs-document-properties-button =\n .title = Document Properties\u2026\npdfjs-document-properties-button-label = Document Properties\u2026\npdfjs-document-properties-file-name = File name:\npdfjs-document-properties-file-size = File size:\npdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes)\npdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes)\npdfjs-document-properties-title = Title:\npdfjs-document-properties-author = Author:\npdfjs-document-properties-subject = Subject:\npdfjs-document-properties-keywords = Keywords:\npdfjs-document-properties-creation-date = Creation Date:\npdfjs-document-properties-modification-date = Modification Date:\npdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: \"short\", timeStyle: \"medium\") }\npdfjs-document-properties-creator = Creator:\npdfjs-document-properties-producer = PDF Producer:\npdfjs-document-properties-version = PDF Version:\npdfjs-document-properties-page-count = Page Count:\npdfjs-document-properties-page-size = Page Size:\npdfjs-document-properties-page-size-unit-inches = in\npdfjs-document-properties-page-size-unit-millimeters = mm\npdfjs-document-properties-page-size-orientation-portrait = portrait\npdfjs-document-properties-page-size-orientation-landscape = landscape\npdfjs-document-properties-page-size-name-a-three = A3\npdfjs-document-properties-page-size-name-a-four = A4\npdfjs-document-properties-page-size-name-letter = Letter\npdfjs-document-properties-page-size-name-legal = Legal\npdfjs-document-properties-page-size-dimension-string = { $width } \xD7 { $height } { $unit } ({ $orientation })\npdfjs-document-properties-page-size-dimension-name-string = { $width } \xD7 { $height } { $unit } ({ $name }, { $orientation })\npdfjs-document-properties-linearized = Fast Web View:\npdfjs-document-properties-linearized-yes = Yes\npdfjs-document-properties-linearized-no = No\npdfjs-document-properties-close-button = Close\npdfjs-print-progress-message = Preparing document for printing\u2026\npdfjs-print-progress-percent = { $progress }%\npdfjs-print-progress-close-button = Cancel\npdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser.\npdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing.\npdfjs-toggle-sidebar-button =\n .title = Toggle Sidebar\npdfjs-toggle-sidebar-notification-button =\n .title = Toggle Sidebar (document contains outline/attachments/layers)\npdfjs-toggle-sidebar-button-label = Toggle Sidebar\npdfjs-document-outline-button =\n .title = Show Document Outline (double-click to expand/collapse all items)\npdfjs-document-outline-button-label = Document Outline\npdfjs-attachments-button =\n .title = Show Attachments\npdfjs-attachments-button-label = Attachments\npdfjs-layers-button =\n .title = Show Layers (double-click to reset all layers to the default state)\npdfjs-layers-button-label = Layers\npdfjs-thumbs-button =\n .title = Show Thumbnails\npdfjs-thumbs-button-label = Thumbnails\npdfjs-current-outline-item-button =\n .title = Find Current Outline Item\npdfjs-current-outline-item-button-label = Current Outline Item\npdfjs-findbar-button =\n .title = Find in Document\npdfjs-findbar-button-label = Find\npdfjs-additional-layers = Additional Layers\npdfjs-thumb-page-title =\n .title = Page { $page }\npdfjs-thumb-page-canvas =\n .aria-label = Thumbnail of Page { $page }\npdfjs-find-input =\n .title = Find\n .placeholder = Find in document\u2026\npdfjs-find-previous-button =\n .title = Find the previous occurrence of the phrase\npdfjs-find-previous-button-label = Previous\npdfjs-find-next-button =\n .title = Find the next occurrence of the phrase\npdfjs-find-next-button-label = Next\npdfjs-find-highlight-checkbox = Highlight All\npdfjs-find-match-case-checkbox-label = Match Case\npdfjs-find-match-diacritics-checkbox-label = Match Diacritics\npdfjs-find-entire-word-checkbox-label = Whole Words\npdfjs-find-reached-top = Reached top of document, continued from bottom\npdfjs-find-reached-bottom = Reached end of document, continued from top\npdfjs-find-match-count =\n { $total ->\n [one] { $current } of { $total } match\n *[other] { $current } of { $total } matches\n }\npdfjs-find-match-count-limit =\n { $limit ->\n [one] More than { $limit } match\n *[other] More than { $limit } matches\n }\npdfjs-find-not-found = Phrase not found\npdfjs-page-scale-width = Page Width\npdfjs-page-scale-fit = Page Fit\npdfjs-page-scale-auto = Automatic Zoom\npdfjs-page-scale-actual = Actual Size\npdfjs-page-scale-percent = { $scale }%\npdfjs-page-landmark =\n .aria-label = Page { $page }\npdfjs-loading-error = An error occurred while loading the PDF.\npdfjs-invalid-file-error = Invalid or corrupted PDF file.\npdfjs-missing-file-error = Missing PDF file.\npdfjs-unexpected-response-error = Unexpected server response.\npdfjs-rendering-error = An error occurred while rendering the page.\npdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: \"short\", timeStyle: \"medium\") }\npdfjs-text-annotation-type =\n .alt = [{ $type } Annotation]\npdfjs-password-label = Enter the password to open this PDF file.\npdfjs-password-invalid = Invalid password. Please try again.\npdfjs-password-ok-button = OK\npdfjs-password-cancel-button = Cancel\npdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts.\npdfjs-editor-free-text-button =\n .title = Text\npdfjs-editor-free-text-button-label = Text\npdfjs-editor-ink-button =\n .title = Draw\npdfjs-editor-ink-button-label = Draw\npdfjs-editor-stamp-button =\n .title = Add or edit images\npdfjs-editor-stamp-button-label = Add or edit images\npdfjs-editor-highlight-button =\n .title = Highlight\npdfjs-editor-highlight-button-label = Highlight\npdfjs-highlight-floating-button1 =\n .title = Highlight\n .aria-label = Highlight\npdfjs-highlight-floating-button-label = Highlight\npdfjs-editor-remove-ink-button =\n .title = Remove drawing\npdfjs-editor-remove-freetext-button =\n .title = Remove text\npdfjs-editor-remove-stamp-button =\n .title = Remove image\npdfjs-editor-remove-highlight-button =\n .title = Remove highlight\npdfjs-editor-free-text-color-input = Color\npdfjs-editor-free-text-size-input = Size\npdfjs-editor-ink-color-input = Color\npdfjs-editor-ink-thickness-input = Thickness\npdfjs-editor-ink-opacity-input = Opacity\npdfjs-editor-stamp-add-image-button =\n .title = Add image\npdfjs-editor-stamp-add-image-button-label = Add image\npdfjs-editor-free-highlight-thickness-input = Thickness\npdfjs-editor-free-highlight-thickness-title =\n .title = Change thickness when highlighting items other than text\npdfjs-free-text2 =\n .aria-label = Text Editor\n .default-content = Start typing\u2026\npdfjs-ink =\n .aria-label = Draw Editor\npdfjs-ink-canvas =\n .aria-label = User-created image\npdfjs-editor-alt-text-button =\n .aria-label = Alt text\npdfjs-editor-alt-text-button-label = Alt text\npdfjs-editor-alt-text-edit-button =\n .aria-label = Edit alt text\npdfjs-editor-alt-text-dialog-label = Choose an option\npdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can\u2019t see the image or when it doesn\u2019t load.\npdfjs-editor-alt-text-add-description-label = Add a description\npdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions.\npdfjs-editor-alt-text-mark-decorative-label = Mark as decorative\npdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks.\npdfjs-editor-alt-text-cancel-button = Cancel\npdfjs-editor-alt-text-save-button = Save\npdfjs-editor-alt-text-decorative-tooltip = Marked as decorative\npdfjs-editor-alt-text-textarea =\n .placeholder = For example, \u201CA young man sits down at a table to eat a meal\u201D\npdfjs-editor-resizer-top-left =\n .aria-label = Top left corner \u2014 resize\npdfjs-editor-resizer-top-middle =\n .aria-label = Top middle \u2014 resize\npdfjs-editor-resizer-top-right =\n .aria-label = Top right corner \u2014 resize\npdfjs-editor-resizer-middle-right =\n .aria-label = Middle right \u2014 resize\npdfjs-editor-resizer-bottom-right =\n .aria-label = Bottom right corner \u2014 resize\npdfjs-editor-resizer-bottom-middle =\n .aria-label = Bottom middle \u2014 resize\npdfjs-editor-resizer-bottom-left =\n .aria-label = Bottom left corner \u2014 resize\npdfjs-editor-resizer-middle-left =\n .aria-label = Middle left \u2014 resize\npdfjs-editor-highlight-colorpicker-label = Highlight color\npdfjs-editor-colorpicker-button =\n .title = Change color\npdfjs-editor-colorpicker-dropdown =\n .aria-label = Color choices\npdfjs-editor-colorpicker-yellow =\n .title = Yellow\npdfjs-editor-colorpicker-green =\n .title = Green\npdfjs-editor-colorpicker-blue =\n .title = Blue\npdfjs-editor-colorpicker-pink =\n .title = Pink\npdfjs-editor-colorpicker-red =\n .title = Red\npdfjs-editor-highlight-show-all-button-label = Show all\npdfjs-editor-highlight-show-all-button =\n .title = Show all\npdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description)\npdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description)\npdfjs-editor-new-alt-text-textarea =\n .placeholder = Write your description here\u2026\npdfjs-editor-new-alt-text-description = Short description for people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate.\npdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more\npdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically\npdfjs-editor-new-alt-text-not-now-button = Not now\npdfjs-editor-new-alt-text-error-title = Couldn\u2019t create alt text automatically\npdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later.\npdfjs-editor-new-alt-text-error-close-button = Close\npdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\n .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\npdfjs-editor-new-alt-text-added-button =\n .aria-label = Alt text added\npdfjs-editor-new-alt-text-added-button-label = Alt text added\npdfjs-editor-new-alt-text-missing-button =\n .aria-label = Missing alt text\npdfjs-editor-new-alt-text-missing-button-label = Missing alt text\npdfjs-editor-new-alt-text-to-review-button =\n .aria-label = Review alt text\npdfjs-editor-new-alt-text-to-review-button-label = Review alt text\npdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText }\npdfjs-image-alt-text-settings-button =\n .title = Image alt text settings\npdfjs-image-alt-text-settings-button-label = Image alt text settings\npdfjs-editor-alt-text-settings-dialog-label = Image alt text settings\npdfjs-editor-alt-text-settings-automatic-title = Automatic alt text\npdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically\npdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB)\npdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text.\npdfjs-editor-alt-text-settings-delete-model-button = Delete\npdfjs-editor-alt-text-settings-download-model-button = Download\npdfjs-editor-alt-text-settings-downloading-model-button = Downloading\u2026\npdfjs-editor-alt-text-settings-editor-title = Alt text editor\npdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image\npdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text.\npdfjs-editor-alt-text-settings-close-button = Close\npdfjs-editor-undo-bar-message-highlight = Highlight removed\npdfjs-editor-undo-bar-message-freetext = Text removed\npdfjs-editor-undo-bar-message-ink = Drawing removed\npdfjs-editor-undo-bar-message-stamp = Image removed\npdfjs-editor-undo-bar-message-multiple =\n { $count ->\n [one] { $count } annotation removed\n *[other] { $count } annotations removed\n }\npdfjs-editor-undo-bar-undo-button =\n .title = Undo\npdfjs-editor-undo-bar-undo-button-label = Undo\npdfjs-editor-undo-bar-close-button =\n .title = Close\npdfjs-editor-undo-bar-close-button-label = Close"; + return createBundle(lang, text); + } +} + +;// ./web/pdf_history.js + + +const HASH_CHANGE_TIMEOUT = 1000; +const POSITION_UPDATED_THRESHOLD = 50; +const UPDATE_VIEWAREA_TIMEOUT = 1000; +function getCurrentHash() { + return document.location.hash; +} +class PDFHistory { + #eventAbortController = null; + constructor({ + linkService, + eventBus + }) { + this.linkService = linkService; + this.eventBus = eventBus; + this._initialized = false; + this._fingerprint = ""; + this.reset(); + this.eventBus._on("pagesinit", () => { + this._isPagesLoaded = false; + this.eventBus._on("pagesloaded", evt => { + this._isPagesLoaded = !!evt.pagesCount; + }, { + once: true + }); + }); + } + initialize({ + fingerprint, + resetHistory = false, + updateUrl = false + }) { + if (!fingerprint || typeof fingerprint !== "string") { + console.error('PDFHistory.initialize: The "fingerprint" must be a non-empty string.'); + return; + } + if (this._initialized) { + this.reset(); + } + const reInitialized = this._fingerprint !== "" && this._fingerprint !== fingerprint; + this._fingerprint = fingerprint; + this._updateUrl = updateUrl === true; + this._initialized = true; + this.#bindEvents(); + const state = window.history.state; + this._popStateInProgress = false; + this._blockHashChange = 0; + this._currentHash = getCurrentHash(); + this._numPositionUpdates = 0; + this._uid = this._maxUid = 0; + this._destination = null; + this._position = null; + if (!this.#isValidState(state, true) || resetHistory) { + const { + hash, + page, + rotation + } = this.#parseCurrentHash(true); + if (!hash || reInitialized || resetHistory) { + this.#pushOrReplaceState(null, true); + return; + } + this.#pushOrReplaceState({ + hash, + page, + rotation + }, true); + return; + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (destination.rotation !== undefined) { + this._initialRotation = destination.rotation; + } + if (destination.dest) { + this._initialBookmark = JSON.stringify(destination.dest); + this._destination.page = null; + } else if (destination.hash) { + this._initialBookmark = destination.hash; + } else if (destination.page) { + this._initialBookmark = `page=${destination.page}`; + } + } + reset() { + if (this._initialized) { + this.#pageHide(); + this._initialized = false; + this.#unbindEvents(); + } + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._initialBookmark = null; + this._initialRotation = null; + } + push({ + namedDest = null, + explicitDest, + pageNumber + }) { + if (!this._initialized) { + return; + } + if (namedDest && typeof namedDest !== "string") { + console.error("PDFHistory.push: " + `"${namedDest}" is not a valid namedDest parameter.`); + return; + } else if (!Array.isArray(explicitDest)) { + console.error("PDFHistory.push: " + `"${explicitDest}" is not a valid explicitDest parameter.`); + return; + } else if (!this.#isValidPage(pageNumber)) { + if (pageNumber !== null || this._destination) { + console.error("PDFHistory.push: " + `"${pageNumber}" is not a valid pageNumber parameter.`); + return; + } + } + const hash = namedDest || JSON.stringify(explicitDest); + if (!hash) { + return; + } + let forceReplace = false; + if (this._destination && (isDestHashesEqual(this._destination.hash, hash) || isDestArraysEqual(this._destination.dest, explicitDest))) { + if (this._destination.page) { + return; + } + forceReplace = true; + } + if (this._popStateInProgress && !forceReplace) { + return; + } + this.#pushOrReplaceState({ + dest: explicitDest, + hash, + page: pageNumber, + rotation: this.linkService.rotation + }, forceReplace); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushPage(pageNumber) { + if (!this._initialized) { + return; + } + if (!this.#isValidPage(pageNumber)) { + console.error(`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`); + return; + } + if (this._destination?.page === pageNumber) { + return; + } + if (this._popStateInProgress) { + return; + } + this.#pushOrReplaceState({ + dest: null, + hash: `page=${pageNumber}`, + page: pageNumber, + rotation: this.linkService.rotation + }); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushCurrentPosition() { + if (!this._initialized || this._popStateInProgress) { + return; + } + this.#tryPushCurrentPosition(); + } + back() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid > 0) { + window.history.back(); + } + } + forward() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid < this._maxUid) { + window.history.forward(); + } + } + get popStateInProgress() { + return this._initialized && (this._popStateInProgress || this._blockHashChange > 0); + } + get initialBookmark() { + return this._initialized ? this._initialBookmark : null; + } + get initialRotation() { + return this._initialized ? this._initialRotation : null; + } + #pushOrReplaceState(destination, forceReplace = false) { + const shouldReplace = forceReplace || !this._destination; + const newState = { + fingerprint: this._fingerprint, + uid: shouldReplace ? this._uid : this._uid + 1, + destination + }; + this.#updateInternalState(destination, newState.uid); + let newUrl; + if (this._updateUrl && destination?.hash) { + const baseUrl = document.location.href.split("#", 1)[0]; + if (!baseUrl.startsWith("file://")) { + newUrl = `${baseUrl}#${destination.hash}`; + } + } + if (shouldReplace) { + window.history.replaceState(newState, "", newUrl); + } else { + window.history.pushState(newState, "", newUrl); + } + } + #tryPushCurrentPosition(temporary = false) { + if (!this._position) { + return; + } + let position = this._position; + if (temporary) { + position = Object.assign(Object.create(null), this._position); + position.temporary = true; + } + if (!this._destination) { + this.#pushOrReplaceState(position); + return; + } + if (this._destination.temporary) { + this.#pushOrReplaceState(position, true); + return; + } + if (this._destination.hash === position.hash) { + return; + } + if (!this._destination.page && (POSITION_UPDATED_THRESHOLD <= 0 || this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) { + return; + } + let forceReplace = false; + if (this._destination.page >= position.first && this._destination.page <= position.page) { + if (this._destination.dest !== undefined || !this._destination.first) { + return; + } + forceReplace = true; + } + this.#pushOrReplaceState(position, forceReplace); + } + #isValidPage(val) { + return Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount; + } + #isValidState(state, checkReload = false) { + if (!state) { + return false; + } + if (state.fingerprint !== this._fingerprint) { + if (checkReload) { + if (typeof state.fingerprint !== "string" || state.fingerprint.length !== this._fingerprint.length) { + return false; + } + const [perfEntry] = performance.getEntriesByType("navigation"); + if (perfEntry?.type !== "reload") { + return false; + } + } else { + return false; + } + } + if (!Number.isInteger(state.uid) || state.uid < 0) { + return false; + } + if (state.destination === null || typeof state.destination !== "object") { + return false; + } + return true; + } + #updateInternalState(destination, uid, removeTemporary = false) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + if (removeTemporary && destination?.temporary) { + delete destination.temporary; + } + this._destination = destination; + this._uid = uid; + this._maxUid = Math.max(this._maxUid, uid); + this._numPositionUpdates = 0; + } + #parseCurrentHash(checkNameddest = false) { + const hash = unescape(getCurrentHash()).substring(1); + const params = parseQueryString(hash); + const nameddest = params.get("nameddest") || ""; + let page = params.get("page") | 0; + if (!this.#isValidPage(page) || checkNameddest && nameddest.length > 0) { + page = null; + } + return { + hash, + page, + rotation: this.linkService.rotation + }; + } + #updateViewarea({ + location + }) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._position = { + hash: location.pdfOpenParams.substring(1), + page: this.linkService.page, + first: location.pageNumber, + rotation: location.rotation + }; + if (this._popStateInProgress) { + return; + } + if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && this._destination && !this._destination.page) { + this._numPositionUpdates++; + } + if (UPDATE_VIEWAREA_TIMEOUT > 0) { + this._updateViewareaTimeout = setTimeout(() => { + if (!this._popStateInProgress) { + this.#tryPushCurrentPosition(true); + } + this._updateViewareaTimeout = null; + }, UPDATE_VIEWAREA_TIMEOUT); + } + } + #popState({ + state + }) { + const newHash = getCurrentHash(), + hashChanged = this._currentHash !== newHash; + this._currentHash = newHash; + if (!state) { + this._uid++; + const { + hash, + page, + rotation + } = this.#parseCurrentHash(); + this.#pushOrReplaceState({ + hash, + page, + rotation + }, true); + return; + } + if (!this.#isValidState(state)) { + return; + } + this._popStateInProgress = true; + if (hashChanged) { + this._blockHashChange++; + waitOnEventOrTimeout({ + target: window, + name: "hashchange", + delay: HASH_CHANGE_TIMEOUT + }).then(() => { + this._blockHashChange--; + }); + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (isValidRotation(destination.rotation)) { + this.linkService.rotation = destination.rotation; + } + if (destination.dest) { + this.linkService.goToDestination(destination.dest); + } else if (destination.hash) { + this.linkService.setHash(destination.hash); + } else if (destination.page) { + this.linkService.page = destination.page; + } + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + #pageHide() { + if (!this._destination || this._destination.temporary) { + this.#tryPushCurrentPosition(); + } + } + #bindEvents() { + if (this.#eventAbortController) { + return; + } + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), { + signal + }); + window.addEventListener("popstate", this.#popState.bind(this), { + signal + }); + window.addEventListener("pagehide", this.#pageHide.bind(this), { + signal + }); + } + #unbindEvents() { + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } +} +function isDestHashesEqual(destHash, pushHash) { + if (typeof destHash !== "string" || typeof pushHash !== "string") { + return false; + } + if (destHash === pushHash) { + return true; + } + const nameddest = parseQueryString(destHash).get("nameddest"); + if (nameddest === pushHash) { + return true; + } + return false; +} +function isDestArraysEqual(firstDest, secondDest) { + function isEntryEqual(first, second) { + if (typeof first !== typeof second) { + return false; + } + if (Array.isArray(first) || Array.isArray(second)) { + return false; + } + if (first !== null && typeof first === "object" && second !== null) { + if (Object.keys(first).length !== Object.keys(second).length) { + return false; + } + for (const key in first) { + if (!isEntryEqual(first[key], second[key])) { + return false; + } + } + return true; + } + return first === second || Number.isNaN(first) && Number.isNaN(second); + } + if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) { + return false; + } + if (firstDest.length !== secondDest.length) { + return false; + } + for (let i = 0, ii = firstDest.length; i < ii; i++) { + if (!isEntryEqual(firstDest[i], secondDest[i])) { + return false; + } + } + return true; +} + +;// ./web/annotation_editor_layer_builder.js + + +class AnnotationEditorLayerBuilder { + #annotationLayer = null; + #drawLayer = null; + #onAppend = null; + #structTreeLayer = null; + #textLayer = null; + #uiManager; + constructor(options) { + this.pdfPage = options.pdfPage; + this.accessibilityManager = options.accessibilityManager; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.annotationEditorLayer = null; + this.div = null; + this._cancelled = false; + this.#uiManager = options.uiManager; + this.#annotationLayer = options.annotationLayer || null; + this.#textLayer = options.textLayer || null; + this.#drawLayer = options.drawLayer || null; + this.#onAppend = options.onAppend || null; + this.#structTreeLayer = options.structTreeLayer || null; + } + async render(viewport, intent = "display") { + if (intent !== "display") { + return; + } + if (this._cancelled) { + return; + } + const clonedViewport = viewport.clone({ + dontFlip: true + }); + if (this.div) { + this.annotationEditorLayer.update({ + viewport: clonedViewport + }); + this.show(); + return; + } + const div = this.div = document.createElement("div"); + div.className = "annotationEditorLayer"; + div.hidden = true; + div.dir = this.#uiManager.direction; + this.#onAppend?.(div); + this.annotationEditorLayer = new AnnotationEditorLayer({ + uiManager: this.#uiManager, + div, + structTreeLayer: this.#structTreeLayer, + accessibilityManager: this.accessibilityManager, + pageIndex: this.pdfPage.pageNumber - 1, + l10n: this.l10n, + viewport: clonedViewport, + annotationLayer: this.#annotationLayer, + textLayer: this.#textLayer, + drawLayer: this.#drawLayer + }); + const parameters = { + viewport: clonedViewport, + div, + annotations: null, + intent + }; + this.annotationEditorLayer.render(parameters); + this.show(); + } + cancel() { + this._cancelled = true; + if (!this.div) { + return; + } + this.annotationEditorLayer.destroy(); + } + hide() { + if (!this.div) { + return; + } + this.annotationEditorLayer.pause(true); + this.div.hidden = true; + } + show() { + if (!this.div || this.annotationEditorLayer.isInvisible) { + return; + } + this.div.hidden = false; + this.annotationEditorLayer.pause(false); + } +} + +;// ./web/app_options.js +{ + var compatParams = new Map(); + const userAgent = navigator.userAgent || ""; + const platform = navigator.platform || ""; + const maxTouchPoints = navigator.maxTouchPoints || 1; + const isAndroid = /Android/.test(userAgent); + const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1; + (function () { + if (isIOS || isAndroid) { + compatParams.set("maxCanvasPixels", 5242880); + } + })(); + (function () { + if (isAndroid) { + compatParams.set("useSystemFonts", false); + } + })(); +} +const OptionKind = { + BROWSER: 0x01, + VIEWER: 0x02, + API: 0x04, + WORKER: 0x08, + EVENT_DISPATCH: 0x10, + PREFERENCE: 0x80 +}; +const Type = { + BOOLEAN: 0x01, + NUMBER: 0x02, + OBJECT: 0x04, + STRING: 0x08, + UNDEFINED: 0x10 +}; +const defaultOptions = { + allowedGlobalEvents: { + value: null, + kind: OptionKind.BROWSER + }, + canvasMaxAreaInBytes: { + value: -1, + kind: OptionKind.BROWSER + OptionKind.API + }, + isInAutomation: { + value: false, + kind: OptionKind.BROWSER + }, + localeProperties: { + value: { + lang: navigator.language || "en-US" + }, + kind: OptionKind.BROWSER + }, + nimbusDataStr: { + value: "", + kind: OptionKind.BROWSER + }, + supportsCaretBrowsingMode: { + value: false, + kind: OptionKind.BROWSER + }, + supportsDocumentFonts: { + value: true, + kind: OptionKind.BROWSER + }, + supportsIntegratedFind: { + value: false, + kind: OptionKind.BROWSER + }, + supportsMouseWheelZoomCtrlKey: { + value: true, + kind: OptionKind.BROWSER + }, + supportsMouseWheelZoomMetaKey: { + value: true, + kind: OptionKind.BROWSER + }, + supportsPinchToZoom: { + value: true, + kind: OptionKind.BROWSER + }, + toolbarDensity: { + value: 0, + kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH + }, + altTextLearnMoreUrl: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + annotationEditorMode: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + annotationMode: { + value: 2, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + cursorToolOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + debuggerSrc: { + value: "./debugger.mjs", + kind: OptionKind.VIEWER + }, + defaultZoomDelay: { + value: 400, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + defaultZoomValue: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + disableHistory: { + value: false, + kind: OptionKind.VIEWER + }, + disablePageLabels: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableAltText: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableAltTextModelDownload: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH + }, + enableGuessAltText: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH + }, + enableHighlightFloatingButton: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableNewAltTextWhenAddingImage: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enablePermissions: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enablePrintAutoRotate: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableScripting: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableUpdatedAddImage: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + externalLinkRel: { + value: "noopener noreferrer nofollow", + kind: OptionKind.VIEWER + }, + externalLinkTarget: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + highlightEditorColors: { + value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + historyUpdateUrl: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + ignoreDestinationZoom: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + imageResourcesPath: { + value: "./images/", + kind: OptionKind.VIEWER + }, + maxCanvasPixels: { + value: 2 ** 25, + kind: OptionKind.VIEWER + }, + forcePageColors: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pageColorsBackground: { + value: "Canvas", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pageColorsForeground: { + value: "CanvasText", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pdfBugEnabled: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + printResolution: { + value: 150, + kind: OptionKind.VIEWER + }, + sidebarViewOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + scrollModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + spreadModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + textLayerMode: { + value: 1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + viewOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + cMapPacked: { + value: true, + kind: OptionKind.API + }, + cMapUrl: { + value: "../web/cmaps/", + kind: OptionKind.API + }, + disableAutoFetch: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableFontFace: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableRange: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableStream: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + docBaseUrl: { + value: "", + kind: OptionKind.API + }, + enableHWA: { + value: true, + kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableXfa: { + value: true, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + fontExtraProperties: { + value: false, + kind: OptionKind.API + }, + isEvalSupported: { + value: true, + kind: OptionKind.API + }, + isOffscreenCanvasSupported: { + value: true, + kind: OptionKind.API + }, + maxImageSize: { + value: -1, + kind: OptionKind.API + }, + pdfBug: { + value: false, + kind: OptionKind.API + }, + standardFontDataUrl: { + value: "../web/standard_fonts/", + kind: OptionKind.API + }, + useSystemFonts: { + value: undefined, + kind: OptionKind.API, + type: Type.BOOLEAN + Type.UNDEFINED + }, + verbosity: { + value: 1, + kind: OptionKind.API + }, + workerPort: { + value: null, + kind: OptionKind.WORKER + }, + workerSrc: { + value: "../build/pdf.worker.mjs", + kind: OptionKind.WORKER + } +}; +{ + defaultOptions.defaultUrl = { + value: "compressed.tracemonkey-pldi-09.pdf", + kind: OptionKind.VIEWER + }; + defaultOptions.sandboxBundleSrc = { + value: "../build/pdf.sandbox.mjs", + kind: OptionKind.VIEWER + }; + defaultOptions.viewerCssTheme = { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }; + defaultOptions.enableFakeMLManager = { + value: true, + kind: OptionKind.VIEWER + }; +} +{ + defaultOptions.disablePreferences = { + value: false, + kind: OptionKind.VIEWER + }; +} +class AppOptions { + static eventBus; + static #opts = new Map(); + static { + for (const name in defaultOptions) { + this.#opts.set(name, defaultOptions[name].value); + } + for (const [name, value] of compatParams) { + this.#opts.set(name, value); + } + this._hasInvokedSet = false; + this._checkDisablePreferences = () => { + if (this.get("disablePreferences")) { + return true; + } + if (this._hasInvokedSet) { + console.warn("The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option to prevent that.'); + } + return false; + }; + } + static get(name) { + return this.#opts.get(name); + } + static getAll(kind = null, defaultOnly = false) { + const options = Object.create(null); + for (const name in defaultOptions) { + const defaultOpt = defaultOptions[name]; + if (kind && !(kind & defaultOpt.kind)) { + continue; + } + options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value; + } + return options; + } + static set(name, value) { + this.setAll({ + [name]: value + }); + } + static setAll(options, prefs = false) { + this._hasInvokedSet ||= true; + let events; + for (const name in options) { + const defaultOpt = defaultOptions[name], + userOpt = options[name]; + if (!defaultOpt || !(typeof userOpt === typeof defaultOpt.value || Type[(typeof userOpt).toUpperCase()] & defaultOpt.type)) { + continue; + } + const { + kind + } = defaultOpt; + if (prefs && !(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { + continue; + } + if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { + (events ||= new Map()).set(name, userOpt); + } + this.#opts.set(name, userOpt); + } + if (events) { + for (const [name, value] of events) { + this.eventBus.dispatch(name.toLowerCase(), { + source: this, + value + }); + } + } + } +} + +;// ./web/draw_layer_builder.js + +class DrawLayerBuilder { + #drawLayer = null; + constructor(options) { + this.pageIndex = options.pageIndex; + } + async render(intent = "display") { + if (intent !== "display" || this.#drawLayer || this._cancelled) { + return; + } + this.#drawLayer = new DrawLayer({ + pageIndex: this.pageIndex + }); + } + cancel() { + this._cancelled = true; + if (!this.#drawLayer) { + return; + } + this.#drawLayer.destroy(); + this.#drawLayer = null; + } + setParent(parent) { + this.#drawLayer?.setParent(parent); + } + getDrawLayer() { + return this.#drawLayer; + } +} + +;// ./web/struct_tree_layer_builder.js + +const PDF_ROLE_TO_HTML_ROLE = { + Document: null, + DocumentFragment: null, + Part: "group", + Sect: "group", + Div: "group", + Aside: "note", + NonStruct: "none", + P: null, + H: "heading", + Title: null, + FENote: "note", + Sub: "group", + Lbl: null, + Span: null, + Em: null, + Strong: null, + Link: "link", + Annot: "note", + Form: "form", + Ruby: null, + RB: null, + RT: null, + RP: null, + Warichu: null, + WT: null, + WP: null, + L: "list", + LI: "listitem", + LBody: null, + Table: "table", + TR: "row", + TH: "columnheader", + TD: "cell", + THead: "columnheader", + TBody: null, + TFoot: null, + Caption: null, + Figure: "figure", + Formula: null, + Artifact: null +}; +const HEADING_PATTERN = /^H(\d+)$/; +class StructTreeLayerBuilder { + #promise; + #treeDom = null; + #treePromise; + #elementAttributes = new Map(); + #rawDims; + #elementsToAddToTextLayer = null; + constructor(pdfPage, rawDims) { + this.#promise = pdfPage.getStructTree(); + this.#rawDims = rawDims; + } + async render() { + if (this.#treePromise) { + return this.#treePromise; + } + const { + promise, + resolve, + reject + } = Promise.withResolvers(); + this.#treePromise = promise; + try { + this.#treeDom = this.#walk(await this.#promise); + } catch (ex) { + reject(ex); + } + this.#promise = null; + this.#treeDom?.classList.add("structTree"); + resolve(this.#treeDom); + return promise; + } + async getAriaAttributes(annotationId) { + try { + await this.render(); + return this.#elementAttributes.get(annotationId); + } catch {} + return null; + } + hide() { + if (this.#treeDom && !this.#treeDom.hidden) { + this.#treeDom.hidden = true; + } + } + show() { + if (this.#treeDom?.hidden) { + this.#treeDom.hidden = false; + } + } + #setAttributes(structElement, htmlElement) { + const { + alt, + id, + lang + } = structElement; + if (alt !== undefined) { + let added = false; + const label = removeNullCharacters(alt); + for (const child of structElement.children) { + if (child.type === "annotation") { + let attrs = this.#elementAttributes.get(child.id); + if (!attrs) { + attrs = new Map(); + this.#elementAttributes.set(child.id, attrs); + } + attrs.set("aria-label", label); + added = true; + } + } + if (!added) { + htmlElement.setAttribute("aria-label", label); + } + } + if (id !== undefined) { + htmlElement.setAttribute("aria-owns", id); + } + if (lang !== undefined) { + htmlElement.setAttribute("lang", removeNullCharacters(lang, true)); + } + } + #addImageInTextLayer(node, element) { + const { + alt, + bbox, + children + } = node; + const child = children?.[0]; + if (!this.#rawDims || !alt || !bbox || child?.type !== "content") { + return false; + } + const { + id + } = child; + if (!id) { + return false; + } + element.setAttribute("aria-owns", id); + const img = document.createElement("span"); + (this.#elementsToAddToTextLayer ||= new Map()).set(id, img); + img.setAttribute("role", "img"); + img.setAttribute("aria-label", removeNullCharacters(alt)); + const { + pageHeight, + pageX, + pageY + } = this.#rawDims; + const calc = "calc(var(--scale-factor)*"; + const { + style + } = img; + style.width = `${calc}${bbox[2] - bbox[0]}px)`; + style.height = `${calc}${bbox[3] - bbox[1]}px)`; + style.left = `${calc}${bbox[0] - pageX}px)`; + style.top = `${calc}${pageHeight - bbox[3] + pageY}px)`; + return true; + } + addElementsToTextLayer() { + if (!this.#elementsToAddToTextLayer) { + return; + } + for (const [id, img] of this.#elementsToAddToTextLayer) { + document.getElementById(id)?.append(img); + } + this.#elementsToAddToTextLayer.clear(); + this.#elementsToAddToTextLayer = null; + } + #walk(node) { + if (!node) { + return null; + } + const element = document.createElement("span"); + if ("role" in node) { + const { + role + } = node; + const match = role.match(HEADING_PATTERN); + if (match) { + element.setAttribute("role", "heading"); + element.setAttribute("aria-level", match[1]); + } else if (PDF_ROLE_TO_HTML_ROLE[role]) { + element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]); + } + if (role === "Figure" && this.#addImageInTextLayer(node, element)) { + return element; + } + } + this.#setAttributes(node, element); + if (node.children) { + if (node.children.length === 1 && "id" in node.children[0]) { + this.#setAttributes(node.children[0], element); + } else { + for (const kid of node.children) { + element.append(this.#walk(kid)); + } + } + } + return element; + } +} + +;// ./web/text_accessibility.js + +class TextAccessibilityManager { + #enabled = false; + #textChildren = null; + #textNodes = new Map(); + #waitingElements = new Map(); + setTextMapping(textDivs) { + this.#textChildren = textDivs; + } + static #compareElementPositions(e1, e2) { + const rect1 = e1.getBoundingClientRect(); + const rect2 = e2.getBoundingClientRect(); + if (rect1.width === 0 && rect1.height === 0) { + return +1; + } + if (rect2.width === 0 && rect2.height === 0) { + return -1; + } + const top1 = rect1.y; + const bot1 = rect1.y + rect1.height; + const mid1 = rect1.y + rect1.height / 2; + const top2 = rect2.y; + const bot2 = rect2.y + rect2.height; + const mid2 = rect2.y + rect2.height / 2; + if (mid1 <= top2 && mid2 >= bot1) { + return -1; + } + if (mid2 <= top1 && mid1 >= bot2) { + return +1; + } + const centerX1 = rect1.x + rect1.width / 2; + const centerX2 = rect2.x + rect2.width / 2; + return centerX1 - centerX2; + } + enable() { + if (this.#enabled) { + throw new Error("TextAccessibilityManager is already enabled."); + } + if (!this.#textChildren) { + throw new Error("Text divs and strings have not been set."); + } + this.#enabled = true; + this.#textChildren = this.#textChildren.slice(); + this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions); + if (this.#textNodes.size > 0) { + const textChildren = this.#textChildren; + for (const [id, nodeIndex] of this.#textNodes) { + const element = document.getElementById(id); + if (!element) { + this.#textNodes.delete(id); + continue; + } + this.#addIdToAriaOwns(id, textChildren[nodeIndex]); + } + } + for (const [element, isRemovable] of this.#waitingElements) { + this.addPointerInTextLayer(element, isRemovable); + } + this.#waitingElements.clear(); + } + disable() { + if (!this.#enabled) { + return; + } + this.#waitingElements.clear(); + this.#textChildren = null; + this.#enabled = false; + } + removePointerInTextLayer(element) { + if (!this.#enabled) { + this.#waitingElements.delete(element); + return; + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return; + } + const { + id + } = element; + const nodeIndex = this.#textNodes.get(id); + if (nodeIndex === undefined) { + return; + } + const node = children[nodeIndex]; + this.#textNodes.delete(id); + let owns = node.getAttribute("aria-owns"); + if (owns?.includes(id)) { + owns = owns.split(" ").filter(x => x !== id).join(" "); + if (owns) { + node.setAttribute("aria-owns", owns); + } else { + node.removeAttribute("aria-owns"); + node.setAttribute("role", "presentation"); + } + } + } + #addIdToAriaOwns(id, node) { + const owns = node.getAttribute("aria-owns"); + if (!owns?.includes(id)) { + node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id); + } + node.removeAttribute("role"); + } + addPointerInTextLayer(element, isRemovable) { + const { + id + } = element; + if (!id) { + return null; + } + if (!this.#enabled) { + this.#waitingElements.set(element, isRemovable); + return null; + } + if (isRemovable) { + this.removePointerInTextLayer(element); + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return null; + } + const index = binarySearchFirstItem(children, node => TextAccessibilityManager.#compareElementPositions(element, node) < 0); + const nodeIndex = Math.max(0, index - 1); + const child = children[nodeIndex]; + this.#addIdToAriaOwns(id, child); + this.#textNodes.set(id, nodeIndex); + const parent = child.parentNode; + return parent?.classList.contains("markedContent") ? parent.id : null; + } + moveElementInDOM(container, element, contentElement, isRemovable) { + const id = this.addPointerInTextLayer(contentElement, isRemovable); + if (!container.hasChildNodes()) { + container.append(element); + return id; + } + const children = Array.from(container.childNodes).filter(node => node !== element); + if (children.length === 0) { + return id; + } + const elementToCompare = contentElement || element; + const index = binarySearchFirstItem(children, node => TextAccessibilityManager.#compareElementPositions(elementToCompare, node) < 0); + if (index === 0) { + children[0].before(element); + } else { + children[index - 1].after(element); + } + return id; + } +} + +;// ./web/text_highlighter.js +class TextHighlighter { + #eventAbortController = null; + constructor({ + findController, + eventBus, + pageIndex + }) { + this.findController = findController; + this.matches = []; + this.eventBus = eventBus; + this.pageIdx = pageIndex; + this.textDivs = null; + this.textContentItemsStr = null; + this.enabled = false; + } + setTextMapping(divs, texts) { + this.textDivs = divs; + this.textContentItemsStr = texts; + } + enable() { + if (!this.textDivs || !this.textContentItemsStr) { + throw new Error("Text divs and strings have not been set."); + } + if (this.enabled) { + throw new Error("TextHighlighter is already enabled."); + } + this.enabled = true; + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this.eventBus._on("updatetextlayermatches", evt => { + if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) { + this._updateMatches(); + } + }, { + signal: this.#eventAbortController.signal + }); + } + this._updateMatches(); + } + disable() { + if (!this.enabled) { + return; + } + this.enabled = false; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._updateMatches(true); + } + _convertMatches(matches, matchesLength) { + if (!matches) { + return []; + } + const { + textContentItemsStr + } = this; + let i = 0, + iIndex = 0; + const end = textContentItemsStr.length - 1; + const result = []; + for (let m = 0, mm = matches.length; m < mm; m++) { + let matchIdx = matches[m]; + while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + if (i === textContentItemsStr.length) { + console.error("Could not find a matching mapping"); + } + const match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex + } + }; + matchIdx += matchesLength[m]; + while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + match.end = { + divIdx: i, + offset: matchIdx - iIndex + }; + result.push(match); + } + return result; + } + _renderMatches(matches) { + if (matches.length === 0) { + return; + } + const { + findController, + pageIdx + } = this; + const { + textContentItemsStr, + textDivs + } = this; + const isSelectedPage = pageIdx === findController.selected.pageIdx; + const selectedMatchIdx = findController.selected.matchIdx; + const highlightAll = findController.state.highlightAll; + let prevEnd = null; + const infinity = { + divIdx: -1, + offset: undefined + }; + function beginText(begin, className) { + const divIdx = begin.divIdx; + textDivs[divIdx].textContent = ""; + return appendTextToDiv(divIdx, 0, begin.offset, className); + } + function appendTextToDiv(divIdx, fromOffset, toOffset, className) { + let div = textDivs[divIdx]; + if (div.nodeType === Node.TEXT_NODE) { + const span = document.createElement("span"); + div.before(span); + span.append(div); + textDivs[divIdx] = span; + div = span; + } + const content = textContentItemsStr[divIdx].substring(fromOffset, toOffset); + const node = document.createTextNode(content); + if (className) { + const span = document.createElement("span"); + span.className = `${className} appended`; + span.append(node); + div.append(span); + if (className.includes("selected")) { + const { + left + } = span.getClientRects()[0]; + const parentLeft = div.getBoundingClientRect().left; + return left - parentLeft; + } + return 0; + } + div.append(node); + return 0; + } + let i0 = selectedMatchIdx, + i1 = i0 + 1; + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + return; + } + let lastDivIdx = -1; + let lastOffset = -1; + for (let i = i0; i < i1; i++) { + const match = matches[i]; + const begin = match.begin; + if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) { + continue; + } + lastDivIdx = begin.divIdx; + lastOffset = begin.offset; + const end = match.end; + const isSelected = isSelectedPage && i === selectedMatchIdx; + const highlightSuffix = isSelected ? " selected" : ""; + let selectedLeft = 0; + if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { + if (prevEnd !== null) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + beginText(begin); + } else { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); + } + if (begin.divIdx === end.divIdx) { + selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, end.offset, "highlight" + highlightSuffix); + } else { + selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, "highlight begin" + highlightSuffix); + for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { + textDivs[n0].className = "highlight middle" + highlightSuffix; + } + beginText(end, "highlight end" + highlightSuffix); + } + prevEnd = end; + if (isSelected) { + findController.scrollMatchIntoView({ + element: textDivs[begin.divIdx], + selectedLeft, + pageIndex: pageIdx, + matchIndex: selectedMatchIdx + }); + } + } + if (prevEnd) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + } + _updateMatches(reset = false) { + if (!this.enabled && !reset) { + return; + } + const { + findController, + matches, + pageIdx + } = this; + const { + textContentItemsStr, + textDivs + } = this; + let clearedUntilDivIdx = -1; + for (const match of matches) { + const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (let n = begin, end = match.end.divIdx; n <= end; n++) { + const div = textDivs[n]; + div.textContent = textContentItemsStr[n]; + div.className = ""; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } + if (!findController?.highlightMatches || reset) { + return; + } + const pageMatches = findController.pageMatches[pageIdx] || null; + const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null; + this.matches = this._convertMatches(pageMatches, pageMatchesLength); + this._renderMatches(this.matches); + } +} + +;// ./web/text_layer_builder.js + + +class TextLayerBuilder { + #enablePermissions = false; + #onAppend = null; + #renderingDone = false; + #textLayer = null; + static #textLayers = new Map(); + static #selectionChangeAbortController = null; + constructor({ + pdfPage, + highlighter = null, + accessibilityManager = null, + enablePermissions = false, + onAppend = null + }) { + this.pdfPage = pdfPage; + this.highlighter = highlighter; + this.accessibilityManager = accessibilityManager; + this.#enablePermissions = enablePermissions === true; + this.#onAppend = onAppend; + this.div = document.createElement("div"); + this.div.tabIndex = 0; + this.div.className = "textLayer"; + } + async render(viewport, textContentParams = null) { + if (this.#renderingDone && this.#textLayer) { + this.#textLayer.update({ + viewport, + onBefore: this.hide.bind(this) + }); + this.show(); + return; + } + this.cancel(); + this.#textLayer = new TextLayer({ + textContentSource: this.pdfPage.streamTextContent(textContentParams || { + includeMarkedContent: true, + disableNormalization: true + }), + container: this.div, + viewport + }); + const { + textDivs, + textContentItemsStr + } = this.#textLayer; + this.highlighter?.setTextMapping(textDivs, textContentItemsStr); + this.accessibilityManager?.setTextMapping(textDivs); + await this.#textLayer.render(); + this.#renderingDone = true; + const endOfContent = document.createElement("div"); + endOfContent.className = "endOfContent"; + this.div.append(endOfContent); + this.#bindMouse(endOfContent); + this.#onAppend?.(this.div); + this.highlighter?.enable(); + this.accessibilityManager?.enable(); + } + hide() { + if (!this.div.hidden && this.#renderingDone) { + this.highlighter?.disable(); + this.div.hidden = true; + } + } + show() { + if (this.div.hidden && this.#renderingDone) { + this.div.hidden = false; + this.highlighter?.enable(); + } + } + cancel() { + this.#textLayer?.cancel(); + this.#textLayer = null; + this.highlighter?.disable(); + this.accessibilityManager?.disable(); + TextLayerBuilder.#removeGlobalSelectionListener(this.div); + } + #bindMouse(end) { + const { + div + } = this; + div.addEventListener("mousedown", () => { + div.classList.add("selecting"); + }); + div.addEventListener("copy", event => { + if (!this.#enablePermissions) { + const selection = document.getSelection(); + event.clipboardData.setData("text/plain", removeNullCharacters(normalizeUnicode(selection.toString()))); + } + stopEvent(event); + }); + TextLayerBuilder.#textLayers.set(div, end); + TextLayerBuilder.#enableGlobalSelectionListener(); + } + static #removeGlobalSelectionListener(textLayerDiv) { + this.#textLayers.delete(textLayerDiv); + if (this.#textLayers.size === 0) { + this.#selectionChangeAbortController?.abort(); + this.#selectionChangeAbortController = null; + } + } + static #enableGlobalSelectionListener() { + if (this.#selectionChangeAbortController) { + return; + } + this.#selectionChangeAbortController = new AbortController(); + const { + signal + } = this.#selectionChangeAbortController; + const reset = (end, textLayer) => { + textLayer.append(end); + end.style.width = ""; + end.style.height = ""; + textLayer.classList.remove("selecting"); + }; + let isPointerDown = false; + document.addEventListener("pointerdown", () => { + isPointerDown = true; + }, { + signal + }); + document.addEventListener("pointerup", () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, { + signal + }); + window.addEventListener("blur", () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, { + signal + }); + document.addEventListener("keyup", () => { + if (!isPointerDown) { + this.#textLayers.forEach(reset); + } + }, { + signal + }); + var isFirefox, prevRange; + document.addEventListener("selectionchange", () => { + const selection = document.getSelection(); + if (selection.rangeCount === 0) { + this.#textLayers.forEach(reset); + return; + } + const activeTextLayers = new Set(); + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + for (const textLayerDiv of this.#textLayers.keys()) { + if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) { + activeTextLayers.add(textLayerDiv); + } + } + } + for (const [textLayerDiv, endDiv] of this.#textLayers) { + if (activeTextLayers.has(textLayerDiv)) { + textLayerDiv.classList.add("selecting"); + } else { + reset(endDiv, textLayerDiv); + } + } + isFirefox ??= getComputedStyle(this.#textLayers.values().next().value).getPropertyValue("-moz-user-select") === "none"; + if (isFirefox) { + return; + } + const range = selection.getRangeAt(0); + const modifyStart = prevRange && (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0); + let anchor = modifyStart ? range.startContainer : range.endContainer; + if (anchor.nodeType === Node.TEXT_NODE) { + anchor = anchor.parentNode; + } + const parentTextLayer = anchor.parentElement?.closest(".textLayer"); + const endDiv = this.#textLayers.get(parentTextLayer); + if (endDiv) { + endDiv.style.width = parentTextLayer.style.width; + endDiv.style.height = parentTextLayer.style.height; + anchor.parentElement.insertBefore(endDiv, modifyStart ? anchor : anchor.nextSibling); + } + prevRange = range.cloneRange(); + }, { + signal + }); + } +} + +;// ./web/xfa_layer_builder.js + +class XfaLayerBuilder { + constructor({ + pdfPage, + annotationStorage = null, + linkService, + xfaHtml = null + }) { + this.pdfPage = pdfPage; + this.annotationStorage = annotationStorage; + this.linkService = linkService; + this.xfaHtml = xfaHtml; + this.div = null; + this._cancelled = false; + } + async render(viewport, intent = "display") { + if (intent === "print") { + const parameters = { + viewport: viewport.clone({ + dontFlip: true + }), + div: this.div, + xfaHtml: this.xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent + }; + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + const xfaHtml = await this.pdfPage.getXfa(); + if (this._cancelled || !xfaHtml) { + return { + textDivs: [] + }; + } + const parameters = { + viewport: viewport.clone({ + dontFlip: true + }), + div: this.div, + xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent + }; + if (this.div) { + return XfaLayer.update(parameters); + } + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + cancel() { + this._cancelled = true; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } +} + +;// ./web/pdf_page_view.js + + + + + + + + + + + + + +const DEFAULT_LAYER_PROPERTIES = { + annotationEditorUIManager: null, + annotationStorage: null, + downloadManager: null, + enableScripting: false, + fieldObjectsPromise: null, + findController: null, + hasJSActionsPromise: null, + get linkService() { + return new SimpleLinkService(); + } +}; +const LAYERS_ORDER = new Map([["canvasWrapper", 0], ["textLayer", 1], ["annotationLayer", 2], ["annotationEditorLayer", 3], ["xfaLayer", 3]]); +class PDFPageView { + #annotationMode = AnnotationMode.ENABLE_FORMS; + #canvasWrapper = null; + #enableHWA = false; + #hasRestrictedScaling = false; + #isEditing = false; + #layerProperties = null; + #loadingId = null; + #originalViewport = null; + #previousRotation = null; + #scaleRoundX = 1; + #scaleRoundY = 1; + #renderError = null; + #renderingState = RenderingStates.INITIAL; + #textLayerMode = TextLayerMode.ENABLE; + #useThumbnailCanvas = { + directDrawing: true, + initialOptionalContent: true, + regularAnnotations: true + }; + #layers = [null, null, null, null]; + constructor(options) { + const container = options.container; + const defaultViewport = options.defaultViewport; + this.id = options.id; + this.renderingId = "page" + this.id; + this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES; + this.pdfPage = null; + this.pageLabel = null; + this.rotation = 0; + this.scale = options.scale || DEFAULT_SCALE; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this._optionalContentConfigPromise = options.optionalContentConfigPromise || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); + this.pageColors = options.pageColors || null; + this.#enableHWA = options.enableHWA || false; + this.eventBus = options.eventBus; + this.renderingQueue = options.renderingQueue; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.renderTask = null; + this.resume = null; + this._isStandalone = !this.renderingQueue?.hasViewer(); + this._container = container; + this._annotationCanvasMap = null; + this.annotationLayer = null; + this.annotationEditorLayer = null; + this.textLayer = null; + this.xfaLayer = null; + this.structTreeLayer = null; + this.drawLayer = null; + const div = document.createElement("div"); + div.className = "page"; + div.setAttribute("data-page-number", this.id); + div.setAttribute("role", "region"); + div.setAttribute("data-l10n-id", "pdfjs-page-landmark"); + div.setAttribute("data-l10n-args", JSON.stringify({ + page: this.id + })); + this.div = div; + this.#setDimensions(); + container?.append(div); + if (this._isStandalone) { + container?.style.setProperty("--scale-factor", this.scale * PixelsPerInch.PDF_TO_CSS_UNITS); + if (this.pageColors?.background) { + container?.style.setProperty("--page-bg-color", this.pageColors.background); + } + const { + optionalContentConfigPromise + } = options; + if (optionalContentConfigPromise) { + optionalContentConfigPromise.then(optionalContentConfig => { + if (optionalContentConfigPromise !== this._optionalContentConfigPromise) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility; + }); + } + if (!options.l10n) { + this.l10n.translate(this.div); + } + } + } + #addLayer(div, name) { + const pos = LAYERS_ORDER.get(name); + const oldDiv = this.#layers[pos]; + this.#layers[pos] = div; + if (oldDiv) { + oldDiv.replaceWith(div); + return; + } + for (let i = pos - 1; i >= 0; i--) { + const layer = this.#layers[i]; + if (layer) { + layer.after(div); + return; + } + } + this.div.prepend(div); + } + get renderingState() { + return this.#renderingState; + } + set renderingState(state) { + if (state === this.#renderingState) { + return; + } + this.#renderingState = state; + if (this.#loadingId) { + clearTimeout(this.#loadingId); + this.#loadingId = null; + } + switch (state) { + case RenderingStates.PAUSED: + this.div.classList.remove("loading"); + break; + case RenderingStates.RUNNING: + this.div.classList.add("loadingIcon"); + this.#loadingId = setTimeout(() => { + this.div.classList.add("loading"); + this.#loadingId = null; + }, 0); + break; + case RenderingStates.INITIAL: + case RenderingStates.FINISHED: + this.div.classList.remove("loadingIcon", "loading"); + break; + } + } + #setDimensions() { + const { + viewport + } = this; + if (this.pdfPage) { + if (this.#previousRotation === viewport.rotation) { + return; + } + this.#previousRotation = viewport.rotation; + } + setLayerDimensions(this.div, viewport, true, false); + } + setPdfPage(pdfPage) { + if (this._isStandalone && (this.pageColors?.foreground === "CanvasText" || this.pageColors?.background === "Canvas")) { + this._container?.style.setProperty("--hcm-highlight-filter", pdfPage.filterFactory.addHighlightHCMFilter("highlight", "CanvasText", "Canvas", "HighlightText", "Highlight")); + this._container?.style.setProperty("--hcm-highlight-selected-filter", pdfPage.filterFactory.addHighlightHCMFilter("highlight_selected", "CanvasText", "Canvas", "HighlightText", "Highlight")); + } + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation + }); + this.#setDimensions(); + this.reset(); + } + destroy() { + this.reset(); + this.pdfPage?.cleanup(); + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + get _textHighlighter() { + return shadow(this, "_textHighlighter", new TextHighlighter({ + pageIndex: this.id - 1, + eventBus: this.eventBus, + findController: this.#layerProperties.findController + })); + } + #dispatchLayerRendered(name, error) { + this.eventBus.dispatch(name, { + source: this, + pageNumber: this.id, + error + }); + } + async #renderAnnotationLayer() { + let error = null; + try { + await this.annotationLayer.render(this.viewport, { + structTreeLayer: this.structTreeLayer + }, "display"); + } catch (ex) { + console.error("#renderAnnotationLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationlayerrendered", error); + } + } + async #renderAnnotationEditorLayer() { + let error = null; + try { + await this.annotationEditorLayer.render(this.viewport, "display"); + } catch (ex) { + console.error("#renderAnnotationEditorLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationeditorlayerrendered", error); + } + } + async #renderDrawLayer() { + try { + await this.drawLayer.render("display"); + } catch (ex) { + console.error("#renderDrawLayer:", ex); + } + } + async #renderXfaLayer() { + let error = null; + try { + const result = await this.xfaLayer.render(this.viewport, "display"); + if (result?.textDivs && this._textHighlighter) { + this.#buildXfaTextContentItems(result.textDivs); + } + } catch (ex) { + console.error("#renderXfaLayer:", ex); + error = ex; + } finally { + if (this.xfaLayer?.div) { + this.l10n.pause(); + this.#addLayer(this.xfaLayer.div, "xfaLayer"); + this.l10n.resume(); + } + this.#dispatchLayerRendered("xfalayerrendered", error); + } + } + async #renderTextLayer() { + if (!this.textLayer) { + return; + } + let error = null; + try { + await this.textLayer.render(this.viewport); + } catch (ex) { + if (ex instanceof AbortException) { + return; + } + console.error("#renderTextLayer:", ex); + error = ex; + } + this.#dispatchLayerRendered("textlayerrendered", error); + this.#renderStructTreeLayer(); + } + async #renderStructTreeLayer() { + if (!this.textLayer) { + return; + } + const treeDom = await this.structTreeLayer?.render(); + if (treeDom) { + this.l10n.pause(); + this.structTreeLayer?.addElementsToTextLayer(); + if (this.canvas && treeDom.parentNode !== this.canvas) { + this.canvas.append(treeDom); + } + this.l10n.resume(); + } + this.structTreeLayer?.show(); + } + async #buildXfaTextContentItems(textDivs) { + const text = await this.pdfPage.getTextContent(); + const items = []; + for (const item of text.items) { + items.push(item.str); + } + this._textHighlighter.setTextMapping(textDivs, items); + this._textHighlighter.enable(); + } + #resetCanvas() { + const { + canvas + } = this; + if (!canvas) { + return; + } + canvas.remove(); + canvas.width = canvas.height = 0; + this.canvas = null; + this.#originalViewport = null; + } + reset({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + keepCanvasWrapper = false + } = {}) { + this.cancelRendering({ + keepAnnotationLayer, + keepAnnotationEditorLayer, + keepXfaLayer, + keepTextLayer + }); + this.renderingState = RenderingStates.INITIAL; + const div = this.div; + const childNodes = div.childNodes, + annotationLayerNode = keepAnnotationLayer && this.annotationLayer?.div || null, + annotationEditorLayerNode = keepAnnotationEditorLayer && this.annotationEditorLayer?.div || null, + xfaLayerNode = keepXfaLayer && this.xfaLayer?.div || null, + textLayerNode = keepTextLayer && this.textLayer?.div || null, + canvasWrapperNode = keepCanvasWrapper && this.#canvasWrapper || null; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + switch (node) { + case annotationLayerNode: + case annotationEditorLayerNode: + case xfaLayerNode: + case textLayerNode: + case canvasWrapperNode: + continue; + } + node.remove(); + const layerIndex = this.#layers.indexOf(node); + if (layerIndex >= 0) { + this.#layers[layerIndex] = null; + } + } + div.removeAttribute("data-loaded"); + if (annotationLayerNode) { + this.annotationLayer.hide(); + } + if (annotationEditorLayerNode) { + this.annotationEditorLayer.hide(); + } + if (xfaLayerNode) { + this.xfaLayer.hide(); + } + if (textLayerNode) { + this.textLayer.hide(); + } + this.structTreeLayer?.hide(); + if (!keepCanvasWrapper && this.#canvasWrapper) { + this.#canvasWrapper = null; + this.#resetCanvas(); + } + } + toggleEditingMode(isEditing) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#isEditing = isEditing; + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true + }); + } + update({ + scale = 0, + rotation = null, + optionalContentConfigPromise = null, + drawingDelay = -1 + }) { + this.scale = scale || this.scale; + if (typeof rotation === "number") { + this.rotation = rotation; + } + if (optionalContentConfigPromise instanceof Promise) { + this._optionalContentConfigPromise = optionalContentConfigPromise; + optionalContentConfigPromise.then(optionalContentConfig => { + if (optionalContentConfigPromise !== this._optionalContentConfigPromise) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility; + }); + } + this.#useThumbnailCanvas.directDrawing = true; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation + }); + this.#setDimensions(); + if (this._isStandalone) { + this._container?.style.setProperty("--scale-factor", this.viewport.scale); + } + if (this.canvas) { + let onlyCssZoom = false; + if (this.#hasRestrictedScaling) { + if (this.maxCanvasPixels === 0) { + onlyCssZoom = true; + } else if (this.maxCanvasPixels > 0) { + const { + width, + height + } = this.viewport; + const { + sx, + sy + } = this.outputScale; + onlyCssZoom = (Math.floor(width) * sx | 0) * (Math.floor(height) * sy | 0) > this.maxCanvasPixels; + } + } + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + if (postponeDrawing || onlyCssZoom) { + if (postponeDrawing && !onlyCssZoom && this.renderingState !== RenderingStates.FINISHED) { + this.cancelRendering({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + cancelExtraDelay: drawingDelay + }); + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.directDrawing = false; + } + this.cssTransform({ + redrawAnnotationLayer: true, + redrawAnnotationEditorLayer: true, + redrawXfaLayer: true, + redrawTextLayer: !postponeDrawing, + hideTextLayer: postponeDrawing + }); + if (postponeDrawing) { + return; + } + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: true, + timestamp: performance.now(), + error: this.#renderError + }); + return; + } + } + this.cssTransform({}); + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true + }); + } + cancelRendering({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + cancelExtraDelay = 0 + } = {}) { + if (this.renderTask) { + this.renderTask.cancel(cancelExtraDelay); + this.renderTask = null; + } + this.resume = null; + if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) { + this.textLayer.cancel(); + this.textLayer = null; + } + if (this.annotationLayer && (!keepAnnotationLayer || !this.annotationLayer.div)) { + this.annotationLayer.cancel(); + this.annotationLayer = null; + this._annotationCanvasMap = null; + } + if (this.structTreeLayer && !this.textLayer) { + this.structTreeLayer = null; + } + if (this.annotationEditorLayer && (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)) { + if (this.drawLayer) { + this.drawLayer.cancel(); + this.drawLayer = null; + } + this.annotationEditorLayer.cancel(); + this.annotationEditorLayer = null; + } + if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { + this.xfaLayer.cancel(); + this.xfaLayer = null; + this._textHighlighter?.disable(); + } + } + cssTransform({ + redrawAnnotationLayer = false, + redrawAnnotationEditorLayer = false, + redrawXfaLayer = false, + redrawTextLayer = false, + hideTextLayer = false + }) { + const { + canvas + } = this; + if (!canvas) { + return; + } + const originalViewport = this.#originalViewport; + if (this.viewport !== originalViewport) { + const relativeRotation = (360 + this.viewport.rotation - originalViewport.rotation) % 360; + if (relativeRotation === 90 || relativeRotation === 270) { + const { + width, + height + } = this.viewport; + const scaleX = height / width; + const scaleY = width / height; + canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`; + } else { + canvas.style.transform = relativeRotation === 0 ? "" : `rotate(${relativeRotation}deg)`; + } + } + if (redrawAnnotationLayer && this.annotationLayer) { + this.#renderAnnotationLayer(); + } + if (redrawAnnotationEditorLayer && this.annotationEditorLayer) { + if (this.drawLayer) { + this.#renderDrawLayer(); + } + this.#renderAnnotationEditorLayer(); + } + if (redrawXfaLayer && this.xfaLayer) { + this.#renderXfaLayer(); + } + if (this.textLayer) { + if (hideTextLayer) { + this.textLayer.hide(); + this.structTreeLayer?.hide(); + } else if (redrawTextLayer) { + this.#renderTextLayer(); + } + } + } + get width() { + return this.viewport.width; + } + get height() { + return this.viewport.height; + } + getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + } + async #finishRenderTask(renderTask, error = null) { + if (renderTask === this.renderTask) { + this.renderTask = null; + } + if (error instanceof RenderingCancelledException) { + this.#renderError = null; + return; + } + this.#renderError = error; + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots; + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: false, + timestamp: performance.now(), + error: this.#renderError + }); + if (error) { + throw error; + } + } + async draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error("Must be in new state before drawing"); + this.reset(); + } + const { + div, + l10n, + pageColors, + pdfPage, + viewport + } = this; + if (!pdfPage) { + this.renderingState = RenderingStates.FINISHED; + throw new Error("pdfPage is not loaded"); + } + this.renderingState = RenderingStates.RUNNING; + let canvasWrapper = this.#canvasWrapper; + if (!canvasWrapper) { + canvasWrapper = this.#canvasWrapper = document.createElement("div"); + canvasWrapper.classList.add("canvasWrapper"); + this.#addLayer(canvasWrapper, "canvasWrapper"); + } + if (!this.textLayer && this.#textLayerMode !== TextLayerMode.DISABLE && !pdfPage.isPureXfa) { + this._accessibilityManager ||= new TextAccessibilityManager(); + this.textLayer = new TextLayerBuilder({ + pdfPage, + highlighter: this._textHighlighter, + accessibilityManager: this._accessibilityManager, + enablePermissions: this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS, + onAppend: textLayerDiv => { + this.l10n.pause(); + this.#addLayer(textLayerDiv, "textLayer"); + this.l10n.resume(); + } + }); + } + if (!this.annotationLayer && this.#annotationMode !== AnnotationMode.DISABLE) { + const { + annotationStorage, + annotationEditorUIManager, + downloadManager, + enableScripting, + fieldObjectsPromise, + hasJSActionsPromise, + linkService + } = this.#layerProperties; + this._annotationCanvasMap ||= new Map(); + this.annotationLayer = new AnnotationLayerBuilder({ + pdfPage, + annotationStorage, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS, + linkService, + downloadManager, + enableScripting, + hasJSActionsPromise, + fieldObjectsPromise, + annotationCanvasMap: this._annotationCanvasMap, + accessibilityManager: this._accessibilityManager, + annotationEditorUIManager, + onAppend: annotationLayerDiv => { + this.#addLayer(annotationLayerDiv, "annotationLayer"); + } + }); + } + const renderContinueCallback = cont => { + showCanvas?.(false); + if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) { + this.renderingState = RenderingStates.PAUSED; + this.resume = () => { + this.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + const { + width, + height + } = viewport; + const canvas = document.createElement("canvas"); + canvas.setAttribute("role", "presentation"); + const hasHCM = !!(pageColors?.background && pageColors?.foreground); + const prevCanvas = this.canvas; + const updateOnFirstShow = !prevCanvas && !hasHCM; + this.canvas = canvas; + this.#originalViewport = viewport; + let showCanvas = isLastShow => { + if (updateOnFirstShow) { + canvasWrapper.prepend(canvas); + showCanvas = null; + return; + } + if (!isLastShow) { + return; + } + if (prevCanvas) { + prevCanvas.replaceWith(canvas); + prevCanvas.width = prevCanvas.height = 0; + } else { + canvasWrapper.prepend(canvas); + } + showCanvas = null; + }; + const ctx = canvas.getContext("2d", { + alpha: false, + willReadFrequently: !this.#enableHWA + }); + const outputScale = this.outputScale = new OutputScale(); + if (this.maxCanvasPixels === 0) { + const invScale = 1 / this.scale; + outputScale.sx *= invScale; + outputScale.sy *= invScale; + this.#hasRestrictedScaling = true; + } else if (this.maxCanvasPixels > 0) { + const pixelsInViewport = width * height; + const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + this.#hasRestrictedScaling = true; + } else { + this.#hasRestrictedScaling = false; + } + } + const sfx = approximateFraction(outputScale.sx); + const sfy = approximateFraction(outputScale.sy); + const canvasWidth = canvas.width = floorToDivide(calcRound(width * outputScale.sx), sfx[0]); + const canvasHeight = canvas.height = floorToDivide(calcRound(height * outputScale.sy), sfy[0]); + const pageWidth = floorToDivide(calcRound(width), sfx[1]); + const pageHeight = floorToDivide(calcRound(height), sfy[1]); + outputScale.sx = canvasWidth / pageWidth; + outputScale.sy = canvasHeight / pageHeight; + if (this.#scaleRoundX !== sfx[1]) { + div.style.setProperty("--scale-round-x", `${sfx[1]}px`); + this.#scaleRoundX = sfx[1]; + } + if (this.#scaleRoundY !== sfy[1]) { + div.style.setProperty("--scale-round-y", `${sfy[1]}px`); + this.#scaleRoundY = sfy[1]; + } + const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null; + const renderContext = { + canvasContext: ctx, + transform, + viewport, + annotationMode: this.#annotationMode, + optionalContentConfigPromise: this._optionalContentConfigPromise, + annotationCanvasMap: this._annotationCanvasMap, + pageColors, + isEditing: this.#isEditing + }; + const renderTask = this.renderTask = pdfPage.render(renderContext); + renderTask.onContinue = renderContinueCallback; + const resultPromise = renderTask.promise.then(async () => { + showCanvas?.(true); + await this.#finishRenderTask(renderTask); + this.structTreeLayer ||= new StructTreeLayerBuilder(pdfPage, viewport.rawDims); + this.#renderTextLayer(); + if (this.annotationLayer) { + await this.#renderAnnotationLayer(); + } + const { + annotationEditorUIManager + } = this.#layerProperties; + if (!annotationEditorUIManager) { + return; + } + this.drawLayer ||= new DrawLayerBuilder({ + pageIndex: this.id + }); + await this.#renderDrawLayer(); + this.drawLayer.setParent(canvasWrapper); + this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({ + uiManager: annotationEditorUIManager, + pdfPage, + l10n, + structTreeLayer: this.structTreeLayer, + accessibilityManager: this._accessibilityManager, + annotationLayer: this.annotationLayer?.annotationLayer, + textLayer: this.textLayer, + drawLayer: this.drawLayer.getDrawLayer(), + onAppend: annotationEditorLayerDiv => { + this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer"); + } + }); + this.#renderAnnotationEditorLayer(); + }, error => { + if (!(error instanceof RenderingCancelledException)) { + showCanvas?.(true); + } else { + prevCanvas?.remove(); + this.#resetCanvas(); + } + return this.#finishRenderTask(renderTask, error); + }); + if (pdfPage.isPureXfa) { + if (!this.xfaLayer) { + const { + annotationStorage, + linkService + } = this.#layerProperties; + this.xfaLayer = new XfaLayerBuilder({ + pdfPage, + annotationStorage, + linkService + }); + } + this.#renderXfaLayer(); + } + div.setAttribute("data-loaded", true); + this.eventBus.dispatch("pagerender", { + source: this, + pageNumber: this.id + }); + return resultPromise; + } + setPageLabel(label) { + this.pageLabel = typeof label === "string" ? label : null; + this.div.setAttribute("data-l10n-args", JSON.stringify({ + page: this.pageLabel ?? this.id + })); + if (this.pageLabel !== null) { + this.div.setAttribute("data-page-label", this.pageLabel); + } else { + this.div.removeAttribute("data-page-label"); + } + } + get thumbnailCanvas() { + const { + directDrawing, + initialOptionalContent, + regularAnnotations + } = this.#useThumbnailCanvas; + return directDrawing && initialOptionalContent && regularAnnotations ? this.canvas : null; + } +} + +;// ./web/generic_scripting.js + +async function docProperties(pdfDocument) { + const url = "", + baseUrl = url.split("#", 1)[0]; + let { + info, + metadata, + contentDispositionFilename, + contentLength + } = await pdfDocument.getMetadata(); + if (!contentLength) { + const { + length + } = await pdfDocument.getDownloadInfo(); + contentLength = length; + } + return { + ...info, + baseURL: baseUrl, + filesize: contentLength, + filename: contentDispositionFilename || getPdfFilenameFromUrl(url), + metadata: metadata?.getRaw(), + authors: metadata?.get("dc:creator"), + numPages: pdfDocument.numPages, + URL: url + }; +} +class GenericScripting { + constructor(sandboxBundleSrc) { + this._ready = new Promise((resolve, reject) => { + const sandbox = import(/*webpackIgnore: true*/sandboxBundleSrc); + sandbox.then(pdfjsSandbox => { + resolve(pdfjsSandbox.QuickJSSandbox()); + }).catch(reject); + }); + } + async createSandbox(data) { + const sandbox = await this._ready; + sandbox.create(data); + } + async dispatchEventInSandbox(event) { + const sandbox = await this._ready; + setTimeout(() => sandbox.dispatchEvent(event), 0); + } + async destroySandbox() { + const sandbox = await this._ready; + sandbox.nukeSandbox(); + } +} + +;// ./web/pdf_scripting_manager.js + + +class PDFScriptingManager { + #closeCapability = null; + #destroyCapability = null; + #docProperties = null; + #eventAbortController = null; + #eventBus = null; + #externalServices = null; + #pdfDocument = null; + #pdfViewer = null; + #ready = false; + #scripting = null; + #willPrintCapability = null; + constructor({ + eventBus, + externalServices = null, + docProperties = null + }) { + this.#eventBus = eventBus; + this.#externalServices = externalServices; + this.#docProperties = docProperties; + } + setViewer(pdfViewer) { + this.#pdfViewer = pdfViewer; + } + async setDocument(pdfDocument) { + if (this.#pdfDocument) { + await this.#destroyScripting(); + } + this.#pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const [objects, calculationOrder, docActions] = await Promise.all([pdfDocument.getFieldObjects(), pdfDocument.getCalculationOrderIds(), pdfDocument.getJSActions()]); + if (!objects && !docActions) { + await this.#destroyScripting(); + return; + } + if (pdfDocument !== this.#pdfDocument) { + return; + } + try { + this.#scripting = this.#initScripting(); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + const eventBus = this.#eventBus; + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + eventBus._on("updatefromsandbox", event => { + if (event?.source === window) { + this.#updateFromSandbox(event.detail); + } + }, { + signal + }); + eventBus._on("dispatcheventinsandbox", event => { + this.#scripting?.dispatchEventInSandbox(event.detail); + }, { + signal + }); + eventBus._on("pagechanging", ({ + pageNumber, + previous + }) => { + if (pageNumber === previous) { + return; + } + this.#dispatchPageClose(previous); + this.#dispatchPageOpen(pageNumber); + }, { + signal + }); + eventBus._on("pagerendered", ({ + pageNumber + }) => { + if (!this._pageOpenPending.has(pageNumber)) { + return; + } + if (pageNumber !== this.#pdfViewer.currentPageNumber) { + return; + } + this.#dispatchPageOpen(pageNumber); + }, { + signal + }); + eventBus._on("pagesdestroy", async () => { + await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber); + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillClose" + }); + this.#closeCapability?.resolve(); + }, { + signal + }); + try { + const docProperties = await this.#docProperties(pdfDocument); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting.createSandbox({ + objects, + calculationOrder, + appInfo: { + platform: navigator.platform, + language: navigator.language + }, + docInfo: { + ...docProperties, + actions: docActions + } + }); + eventBus.dispatch("sandboxcreated", { + source: this + }); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "Open" + }); + await this.#dispatchPageOpen(this.#pdfViewer.currentPageNumber, true); + Promise.resolve().then(() => { + if (pdfDocument === this.#pdfDocument) { + this.#ready = true; + } + }); + } + async dispatchWillSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillSave" + }); + } + async dispatchDidSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidSave" + }); + } + async dispatchWillPrint() { + if (!this.#scripting) { + return; + } + await this.#willPrintCapability?.promise; + this.#willPrintCapability = Promise.withResolvers(); + try { + await this.#scripting.dispatchEventInSandbox({ + id: "doc", + name: "WillPrint" + }); + } catch (ex) { + this.#willPrintCapability.resolve(); + this.#willPrintCapability = null; + throw ex; + } + await this.#willPrintCapability.promise; + } + async dispatchDidPrint() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidPrint" + }); + } + get destroyPromise() { + return this.#destroyCapability?.promise || null; + } + get ready() { + return this.#ready; + } + get _pageOpenPending() { + return shadow(this, "_pageOpenPending", new Set()); + } + get _visitedPages() { + return shadow(this, "_visitedPages", new Map()); + } + async #updateFromSandbox(detail) { + const pdfViewer = this.#pdfViewer; + const isInPresentationMode = pdfViewer.isInPresentationMode || pdfViewer.isChangingPresentationMode; + const { + id, + siblings, + command, + value + } = detail; + if (!id) { + switch (command) { + case "clear": + console.clear(); + break; + case "error": + console.error(value); + break; + case "layout": + if (!isInPresentationMode) { + const modes = apiPageLayoutToViewerModes(value); + pdfViewer.spreadMode = modes.spreadMode; + } + break; + case "page-num": + pdfViewer.currentPageNumber = value + 1; + break; + case "print": + await pdfViewer.pagesPromise; + this.#eventBus.dispatch("print", { + source: this + }); + break; + case "println": + console.log(value); + break; + case "zoom": + if (!isInPresentationMode) { + pdfViewer.currentScaleValue = value; + } + break; + case "SaveAs": + this.#eventBus.dispatch("download", { + source: this + }); + break; + case "FirstPage": + pdfViewer.currentPageNumber = 1; + break; + case "LastPage": + pdfViewer.currentPageNumber = pdfViewer.pagesCount; + break; + case "NextPage": + pdfViewer.nextPage(); + break; + case "PrevPage": + pdfViewer.previousPage(); + break; + case "ZoomViewIn": + if (!isInPresentationMode) { + pdfViewer.increaseScale(); + } + break; + case "ZoomViewOut": + if (!isInPresentationMode) { + pdfViewer.decreaseScale(); + } + break; + case "WillPrintFinished": + this.#willPrintCapability?.resolve(); + this.#willPrintCapability = null; + break; + } + return; + } + if (isInPresentationMode && detail.focus) { + return; + } + delete detail.id; + delete detail.siblings; + const ids = siblings ? [id, ...siblings] : [id]; + for (const elementId of ids) { + const element = document.querySelector(`[data-element-id="${elementId}"]`); + if (element) { + element.dispatchEvent(new CustomEvent("updatefromsandbox", { + detail + })); + } else { + this.#pdfDocument?.annotationStorage.setValue(elementId, detail); + } + } + } + async #dispatchPageOpen(pageNumber, initialize = false) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (initialize) { + this.#closeCapability = Promise.withResolvers(); + } + if (!this.#closeCapability) { + return; + } + const pageView = this.#pdfViewer.getPageView(pageNumber - 1); + if (pageView?.renderingState !== RenderingStates.FINISHED) { + this._pageOpenPending.add(pageNumber); + return; + } + this._pageOpenPending.delete(pageNumber); + const actionsPromise = (async () => { + const actions = await (!visitedPages.has(pageNumber) ? pageView.pdfPage?.getJSActions() : null); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageOpen", + pageNumber, + actions + }); + })(); + visitedPages.set(pageNumber, actionsPromise); + } + async #dispatchPageClose(pageNumber) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (!this.#closeCapability) { + return; + } + if (this._pageOpenPending.has(pageNumber)) { + return; + } + const actionsPromise = visitedPages.get(pageNumber); + if (!actionsPromise) { + return; + } + visitedPages.set(pageNumber, null); + await actionsPromise; + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageClose", + pageNumber + }); + } + #initScripting() { + this.#destroyCapability = Promise.withResolvers(); + if (this.#scripting) { + throw new Error("#initScripting: Scripting already exists."); + } + return this.#externalServices.createScripting(); + } + async #destroyScripting() { + if (!this.#scripting) { + this.#pdfDocument = null; + this.#destroyCapability?.resolve(); + return; + } + if (this.#closeCapability) { + await Promise.race([this.#closeCapability.promise, new Promise(resolve => { + setTimeout(resolve, 1000); + })]).catch(() => {}); + this.#closeCapability = null; + } + this.#pdfDocument = null; + try { + await this.#scripting.destroySandbox(); + } catch {} + this.#willPrintCapability?.reject(new Error("Scripting destroyed.")); + this.#willPrintCapability = null; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._pageOpenPending.clear(); + this._visitedPages.clear(); + this.#scripting = null; + this.#ready = false; + this.#destroyCapability?.resolve(); + } +} + +;// ./web/pdf_scripting_manager.component.js + + +class PDFScriptingManagerComponents extends PDFScriptingManager { + constructor(options) { + if (!options.externalServices) { + window.addEventListener("updatefromsandbox", event => { + options.eventBus.dispatch("updatefromsandbox", { + source: window, + detail: event.detail + }); + }); + } + options.externalServices ||= { + createScripting: () => new GenericScripting(options.sandboxBundleSrc) + }; + options.docProperties ||= pdfDocument => docProperties(pdfDocument); + super(options); + } +} + +;// ./web/pdf_rendering_queue.js + + +const CLEANUP_TIMEOUT = 30000; +class PDFRenderingQueue { + constructor() { + this.pdfViewer = null; + this.pdfThumbnailViewer = null; + this.onIdle = null; + this.highestPriorityPage = null; + this.idleTimeout = null; + this.printing = false; + this.isThumbnailViewEnabled = false; + Object.defineProperty(this, "hasViewer", { + value: () => !!this.pdfViewer + }); + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setThumbnailViewer(pdfThumbnailViewer) { + this.pdfThumbnailViewer = pdfThumbnailViewer; + } + isHighestPriority(view) { + return this.highestPriorityPage === view.renderingId; + } + renderHighestPriority(currentlyVisiblePages) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; + } + if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { + return; + } + if (this.isThumbnailViewEnabled && this.pdfThumbnailViewer?.forceRendering()) { + return; + } + if (this.printing) { + return; + } + if (this.onIdle) { + this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); + } + } + getHighestPriority(visible, views, scrolledDown, preRenderExtra = false) { + const visibleViews = visible.views, + numVisible = visibleViews.length; + if (numVisible === 0) { + return null; + } + for (let i = 0; i < numVisible; i++) { + const view = visibleViews[i].view; + if (!this.isViewFinished(view)) { + return view; + } + } + const firstId = visible.first.id, + lastId = visible.last.id; + if (lastId - firstId + 1 > numVisible) { + const visibleIds = visible.ids; + for (let i = 1, ii = lastId - firstId; i < ii; i++) { + const holeId = scrolledDown ? firstId + i : lastId - i; + if (visibleIds.has(holeId)) { + continue; + } + const holeView = views[holeId - 1]; + if (!this.isViewFinished(holeView)) { + return holeView; + } + } + } + let preRenderIndex = scrolledDown ? lastId : firstId - 2; + let preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + if (preRenderExtra) { + preRenderIndex += scrolledDown ? 1 : -1; + preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + } + return null; + } + isViewFinished(view) { + return view.renderingState === RenderingStates.FINISHED; + } + renderView(view) { + switch (view.renderingState) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + this.highestPriorityPage = view.renderingId; + view.resume(); + break; + case RenderingStates.RUNNING: + this.highestPriorityPage = view.renderingId; + break; + case RenderingStates.INITIAL: + this.highestPriorityPage = view.renderingId; + view.draw().finally(() => { + this.renderHighestPriority(); + }).catch(reason => { + if (reason instanceof RenderingCancelledException) { + return; + } + console.error("renderView:", reason); + }); + break; + } + return true; + } +} + +;// ./web/pdf_viewer.js + + + + + + +const DEFAULT_CACHE_SIZE = 10; +const PagesCountLimit = { + FORCE_SCROLL_MODE_PAGE: 10000, + FORCE_LAZY_PAGE_INIT: 5000, + PAUSE_EAGER_PAGE_INIT: 250 +}; +function isValidAnnotationEditorMode(mode) { + return Object.values(AnnotationEditorType).includes(mode) && mode !== AnnotationEditorType.DISABLE; +} +class PDFPageViewBuffer { + #buf = new Set(); + #size = 0; + constructor(size) { + this.#size = size; + } + push(view) { + const buf = this.#buf; + if (buf.has(view)) { + buf.delete(view); + } + buf.add(view); + if (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + resize(newSize, idsToKeep = null) { + this.#size = newSize; + const buf = this.#buf; + if (idsToKeep) { + const ii = buf.size; + let i = 1; + for (const view of buf) { + if (idsToKeep.has(view.id)) { + buf.delete(view); + buf.add(view); + } + if (++i > ii) { + break; + } + } + } + while (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + has(view) { + return this.#buf.has(view); + } + [Symbol.iterator]() { + return this.#buf.keys(); + } + #destroyFirstView() { + const firstView = this.#buf.keys().next().value; + firstView?.destroy(); + this.#buf.delete(firstView); + } +} +class PDFViewer { + #buffer = null; + #altTextManager = null; + #annotationEditorHighlightColors = null; + #annotationEditorMode = AnnotationEditorType.NONE; + #annotationEditorUIManager = null; + #annotationMode = AnnotationMode.ENABLE_FORMS; + #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; + #enableHighlightFloatingButton = false; + #enablePermissions = false; + #enableUpdatedAddImage = false; + #enableNewAltTextWhenAddingImage = false; + #eventAbortController = null; + #mlManager = null; + #switchAnnotationEditorModeAC = null; + #switchAnnotationEditorModeTimeoutId = null; + #getAllTextInProgress = false; + #hiddenCopyElement = null; + #interruptCopyCondition = false; + #previousContainerHeight = 0; + #resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this)); + #scrollModePageState = null; + #scaleTimeoutId = null; + #supportsPinchToZoom = true; + #textLayerMode = TextLayerMode.ENABLE; + constructor(options) { + const viewerVersion = "4.10.38"; + if (version !== viewerVersion) { + throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); + } + this.container = options.container; + this.viewer = options.viewer || options.container.firstElementChild; + if (this.container?.tagName !== "DIV" || this.viewer?.tagName !== "DIV") { + throw new Error("Invalid `container` and/or `viewer` option."); + } + if (this.container.offsetParent && getComputedStyle(this.container).position !== "absolute") { + throw new Error("The `container` must be absolutely positioned."); + } + this.#resizeObserver.observe(this.container); + this.eventBus = options.eventBus; + this.linkService = options.linkService || new SimpleLinkService(); + this.downloadManager = options.downloadManager || null; + this.findController = options.findController || null; + this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; + if (this.findController) { + this.findController.onIsPageVisible = pageNumber => this._getVisiblePages().ids.has(pageNumber); + } + this._scriptingManager = options.scriptingManager || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE; + this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null; + this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true; + this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true; + this.#enableNewAltTextWhenAddingImage = options.enableNewAltTextWhenAddingImage === true; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; + this.removePageBorders = options.removePageBorders || false; + this.maxCanvasPixels = options.maxCanvasPixels; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.#enablePermissions = options.enablePermissions || false; + this.pageColors = options.pageColors || null; + this.#mlManager = options.mlManager || null; + this.#enableHWA = options.enableHWA || false; + this.#supportsPinchToZoom = options.supportsPinchToZoom !== false; + this.defaultRenderingQueue = !options.renderingQueue; + if (this.defaultRenderingQueue) { + this.renderingQueue = new PDFRenderingQueue(); + this.renderingQueue.setViewer(this); + } else { + this.renderingQueue = options.renderingQueue; + } + const { + abortSignal + } = options; + abortSignal?.addEventListener("abort", () => { + this.#resizeObserver.disconnect(); + this.#resizeObserver = null; + }, { + once: true + }); + this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this), abortSignal); + this.presentationModeState = PresentationModeState.UNKNOWN; + this._resetView(); + if (this.removePageBorders) { + this.viewer.classList.add("removePageBorders"); + } + this.#updateContainerHeightCss(); + this.eventBus._on("thumbnailrendered", ({ + pageNumber, + pdfPage + }) => { + const pageView = this._pages[pageNumber - 1]; + if (!this.#buffer.has(pageView)) { + pdfPage?.cleanup(); + } + }); + if (!options.l10n) { + this.l10n.translate(this.container); + } + } + get pagesCount() { + return this._pages.length; + } + getPageView(index) { + return this._pages[index]; + } + getCachedPageViews() { + return new Set(this.#buffer); + } + get pageViewsReady() { + return this._pages.every(pageView => pageView?.pdfPage); + } + get renderForms() { + return this.#annotationMode === AnnotationMode.ENABLE_FORMS; + } + get enableScripting() { + return !!this._scriptingManager; + } + get currentPageNumber() { + return this._currentPageNumber; + } + set currentPageNumber(val) { + if (!Number.isInteger(val)) { + throw new Error("Invalid page number."); + } + if (!this.pdfDocument) { + return; + } + if (!this._setCurrentPageNumber(val, true)) { + console.error(`currentPageNumber: "${val}" is not a valid page.`); + } + } + _setCurrentPageNumber(val, resetCurrentPageView = false) { + if (this._currentPageNumber === val) { + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + if (!(0 < val && val <= this.pagesCount)) { + return false; + } + const previous = this._currentPageNumber; + this._currentPageNumber = val; + this.eventBus.dispatch("pagechanging", { + source: this, + pageNumber: val, + pageLabel: this._pageLabels?.[val - 1] ?? null, + previous + }); + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + get currentPageLabel() { + return this._pageLabels?.[this._currentPageNumber - 1] ?? null; + } + set currentPageLabel(val) { + if (!this.pdfDocument) { + return; + } + let page = val | 0; + if (this._pageLabels) { + const i = this._pageLabels.indexOf(val); + if (i >= 0) { + page = i + 1; + } + } + if (!this._setCurrentPageNumber(page, true)) { + console.error(`currentPageLabel: "${val}" is not a valid page.`); + } + } + get currentScale() { + return this._currentScale !== UNKNOWN_SCALE ? this._currentScale : DEFAULT_SCALE; + } + set currentScale(val) { + if (isNaN(val)) { + throw new Error("Invalid numeric scale."); + } + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false + }); + } + get currentScaleValue() { + return this._currentScaleValue; + } + set currentScaleValue(val) { + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false + }); + } + get pagesRotation() { + return this._pagesRotation; + } + set pagesRotation(rotation) { + if (!isValidRotation(rotation)) { + throw new Error("Invalid pages rotation angle."); + } + if (!this.pdfDocument) { + return; + } + rotation %= 360; + if (rotation < 0) { + rotation += 360; + } + if (this._pagesRotation === rotation) { + return; + } + this._pagesRotation = rotation; + const pageNumber = this._currentPageNumber; + this.refresh(true, { + rotation + }); + if (this._currentScaleValue) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this.eventBus.dispatch("rotationchanging", { + source: this, + pagesRotation: rotation, + pageNumber + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get firstPagePromise() { + return this.pdfDocument ? this._firstPageCapability.promise : null; + } + get onePageRendered() { + return this.pdfDocument ? this._onePageRenderedCapability.promise : null; + } + get pagesPromise() { + return this.pdfDocument ? this._pagesCapability.promise : null; + } + get _layerProperties() { + const self = this; + return shadow(this, "_layerProperties", { + get annotationEditorUIManager() { + return self.#annotationEditorUIManager; + }, + get annotationStorage() { + return self.pdfDocument?.annotationStorage; + }, + get downloadManager() { + return self.downloadManager; + }, + get enableScripting() { + return !!self._scriptingManager; + }, + get fieldObjectsPromise() { + return self.pdfDocument?.getFieldObjects(); + }, + get findController() { + return self.findController; + }, + get hasJSActionsPromise() { + return self.pdfDocument?.hasJSActions(); + }, + get linkService() { + return self.linkService; + } + }); + } + #initializePermissions(permissions) { + const params = { + annotationEditorMode: this.#annotationEditorMode, + annotationMode: this.#annotationMode, + textLayerMode: this.#textLayerMode + }; + if (!permissions) { + return params; + } + if (!permissions.includes(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { + params.textLayerMode = TextLayerMode.ENABLE_PERMISSIONS; + } + if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { + params.annotationEditorMode = AnnotationEditorType.DISABLE; + } + if (!permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { + params.annotationMode = AnnotationMode.ENABLE; + } + return params; + } + async #onePageRenderedOrForceFetch(signal) { + if (document.visibilityState === "hidden" || !this.container.offsetParent || this._getVisiblePages().views.length === 0) { + return; + } + const hiddenCapability = Promise.withResolvers(), + ac = new AbortController(); + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + hiddenCapability.resolve(); + } + }, { + signal: typeof AbortSignal.any === "function" ? AbortSignal.any([signal, ac.signal]) : signal + }); + await Promise.race([this._onePageRenderedCapability.promise, hiddenCapability.promise]); + ac.abort(); + } + async getAllText() { + const texts = []; + const buffer = []; + for (let pageNum = 1, pagesCount = this.pdfDocument.numPages; pageNum <= pagesCount; ++pageNum) { + if (this.#interruptCopyCondition) { + return null; + } + buffer.length = 0; + const page = await this.pdfDocument.getPage(pageNum); + const { + items + } = await page.getTextContent(); + for (const item of items) { + if (item.str) { + buffer.push(item.str); + } + if (item.hasEOL) { + buffer.push("\n"); + } + } + texts.push(removeNullCharacters(buffer.join(""))); + } + return texts.join("\n"); + } + #copyCallback(textLayerMode, event) { + const selection = document.getSelection(); + const { + focusNode, + anchorNode + } = selection; + if (anchorNode && focusNode && selection.containsNode(this.#hiddenCopyElement)) { + if (this.#getAllTextInProgress || textLayerMode === TextLayerMode.ENABLE_PERMISSIONS) { + stopEvent(event); + return; + } + this.#getAllTextInProgress = true; + const { + classList + } = this.viewer; + classList.add("copyAll"); + const ac = new AbortController(); + window.addEventListener("keydown", ev => this.#interruptCopyCondition = ev.key === "Escape", { + signal: ac.signal + }); + this.getAllText().then(async text => { + if (text !== null) { + await navigator.clipboard.writeText(text); + } + }).catch(reason => { + console.warn(`Something goes wrong when extracting the text: ${reason.message}`); + }).finally(() => { + this.#getAllTextInProgress = false; + this.#interruptCopyCondition = false; + ac.abort(); + classList.remove("copyAll"); + }); + stopEvent(event); + } + } + setDocument(pdfDocument) { + if (this.pdfDocument) { + this.eventBus.dispatch("pagesdestroy", { + source: this + }); + this._cancelRendering(); + this._resetView(); + this.findController?.setDocument(null); + this._scriptingManager?.setDocument(null); + this.#annotationEditorUIManager?.destroy(); + this.#annotationEditorUIManager = null; + } + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const pagesCount = pdfDocument.numPages; + const firstPagePromise = pdfDocument.getPage(1); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display" + }); + const permissionsPromise = this.#enablePermissions ? pdfDocument.getPermissions() : Promise.resolve(); + const { + eventBus, + pageColors, + viewer + } = this; + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + if (pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + console.warn("Forcing PAGE-scrolling for performance reasons, given the length of the document."); + const mode = this._scrollMode = ScrollMode.PAGE; + eventBus.dispatch("scrollmodechanged", { + source: this, + mode + }); + } + this._pagesCapability.promise.then(() => { + eventBus.dispatch("pagesloaded", { + source: this, + pagesCount + }); + }, () => {}); + const onBeforeDraw = evt => { + const pageView = this._pages[evt.pageNumber - 1]; + if (!pageView) { + return; + } + this.#buffer.push(pageView); + }; + eventBus._on("pagerender", onBeforeDraw, { + signal + }); + const onAfterDraw = evt => { + if (evt.cssTransform) { + return; + } + this._onePageRenderedCapability.resolve({ + timestamp: evt.timestamp + }); + eventBus._off("pagerendered", onAfterDraw); + }; + eventBus._on("pagerendered", onAfterDraw, { + signal + }); + Promise.all([firstPagePromise, permissionsPromise]).then(([firstPdfPage, permissions]) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this._firstPageCapability.resolve(firstPdfPage); + this._optionalContentConfigPromise = optionalContentConfigPromise; + const { + annotationEditorMode, + annotationMode, + textLayerMode + } = this.#initializePermissions(permissions); + if (textLayerMode !== TextLayerMode.DISABLE) { + const element = this.#hiddenCopyElement = document.createElement("div"); + element.id = "hiddenCopyElement"; + viewer.before(element); + } + if (typeof AbortSignal.any === "function" && annotationEditorMode !== AnnotationEditorType.DISABLE) { + const mode = annotationEditorMode; + if (pdfDocument.isPureXfa) { + console.warn("Warning: XFA-editing is not implemented."); + } else if (isValidAnnotationEditorMode(mode)) { + this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, this.#mlManager, this.#editorUndoBar, this.#supportsPinchToZoom); + eventBus.dispatch("annotationeditoruimanager", { + source: this, + uiManager: this.#annotationEditorUIManager + }); + if (mode !== AnnotationEditorType.NONE) { + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + this.#annotationEditorUIManager.updateMode(mode); + } + } else { + console.error(`Invalid AnnotationEditor mode: ${mode}`); + } + } + const viewerElement = this._scrollMode === ScrollMode.PAGE ? null : viewer; + const scale = this.currentScale; + const viewport = firstPdfPage.getViewport({ + scale: scale * PixelsPerInch.PDF_TO_CSS_UNITS + }); + viewer.style.setProperty("--scale-factor", viewport.scale); + if (pageColors?.background) { + viewer.style.setProperty("--page-bg-color", pageColors.background); + } + if (pageColors?.foreground === "CanvasText" || pageColors?.background === "Canvas") { + viewer.style.setProperty("--hcm-highlight-filter", pdfDocument.filterFactory.addHighlightHCMFilter("highlight", "CanvasText", "Canvas", "HighlightText", "Highlight")); + viewer.style.setProperty("--hcm-highlight-selected-filter", pdfDocument.filterFactory.addHighlightHCMFilter("highlight_selected", "CanvasText", "Canvas", "HighlightText", "ButtonText")); + } + for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { + const pageView = new PDFPageView({ + container: viewerElement, + eventBus, + id: pageNum, + scale, + defaultViewport: viewport.clone(), + optionalContentConfigPromise, + renderingQueue: this.renderingQueue, + textLayerMode, + annotationMode, + imageResourcesPath: this.imageResourcesPath, + maxCanvasPixels: this.maxCanvasPixels, + pageColors, + l10n: this.l10n, + layerProperties: this._layerProperties, + enableHWA: this.#enableHWA + }); + this._pages.push(pageView); + } + this._pages[0]?.setPdfPage(firstPdfPage); + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._spreadMode !== SpreadMode.NONE) { + this._updateSpreadMode(); + } + this.#onePageRenderedOrForceFetch(signal).then(async () => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.findController?.setDocument(pdfDocument); + this._scriptingManager?.setDocument(pdfDocument); + if (this.#hiddenCopyElement) { + document.addEventListener("copy", this.#copyCallback.bind(this, textLayerMode), { + signal + }); + } + if (this.#annotationEditorUIManager) { + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode: this.#annotationEditorMode + }); + } + if (pdfDocument.loadingParams.disableAutoFetch || pagesCount > PagesCountLimit.FORCE_LAZY_PAGE_INIT) { + this._pagesCapability.resolve(); + return; + } + let getPagesLeft = pagesCount - 1; + if (getPagesLeft <= 0) { + this._pagesCapability.resolve(); + return; + } + for (let pageNum = 2; pageNum <= pagesCount; ++pageNum) { + const promise = pdfDocument.getPage(pageNum).then(pdfPage => { + const pageView = this._pages[pageNum - 1]; + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }, reason => { + console.error(`Unable to get page ${pageNum} to initialize viewer`, reason); + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }); + if (pageNum % PagesCountLimit.PAUSE_EAGER_PAGE_INIT === 0) { + await promise; + } + } + }); + eventBus.dispatch("pagesinit", { + source: this + }); + pdfDocument.getMetadata().then(({ + info + }) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + if (info.Language) { + viewer.lang = info.Language; + } + }); + if (this.defaultRenderingQueue) { + this.update(); + } + }).catch(reason => { + console.error("Unable to initialize viewer", reason); + this._pagesCapability.reject(reason); + }); + } + setPageLabels(labels) { + if (!this.pdfDocument) { + return; + } + if (!labels) { + this._pageLabels = null; + } else if (!(Array.isArray(labels) && this.pdfDocument.numPages === labels.length)) { + this._pageLabels = null; + console.error(`setPageLabels: Invalid page labels.`); + } else { + this._pageLabels = labels; + } + for (let i = 0, ii = this._pages.length; i < ii; i++) { + this._pages[i].setPageLabel(this._pageLabels?.[i] ?? null); + } + } + _resetView() { + this._pages = []; + this._currentPageNumber = 1; + this._currentScale = UNKNOWN_SCALE; + this._currentScaleValue = null; + this._pageLabels = null; + this.#buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); + this._location = null; + this._pagesRotation = 0; + this._optionalContentConfigPromise = null; + this._firstPageCapability = Promise.withResolvers(); + this._onePageRenderedCapability = Promise.withResolvers(); + this._pagesCapability = Promise.withResolvers(); + this._scrollMode = ScrollMode.VERTICAL; + this._previousScrollMode = ScrollMode.UNKNOWN; + this._spreadMode = SpreadMode.NONE; + this.#scrollModePageState = { + previousPageNumber: 1, + scrollDown: true, + pages: [] + }; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this.viewer.textContent = ""; + this._updateScrollMode(); + this.viewer.removeAttribute("lang"); + this.#hiddenCopyElement?.remove(); + this.#hiddenCopyElement = null; + this.#cleanupSwitchAnnotationEditorMode(); + } + #ensurePageViewVisible() { + if (this._scrollMode !== ScrollMode.PAGE) { + throw new Error("#ensurePageViewVisible: Invalid scrollMode value."); + } + const pageNumber = this._currentPageNumber, + state = this.#scrollModePageState, + viewer = this.viewer; + viewer.textContent = ""; + state.pages.length = 0; + if (this._spreadMode === SpreadMode.NONE && !this.isInPresentationMode) { + const pageView = this._pages[pageNumber - 1]; + viewer.append(pageView.div); + state.pages.push(pageView); + } else { + const pageIndexSet = new Set(), + parity = this._spreadMode - 1; + if (parity === -1) { + pageIndexSet.add(pageNumber - 1); + } else if (pageNumber % 2 !== parity) { + pageIndexSet.add(pageNumber - 1); + pageIndexSet.add(pageNumber); + } else { + pageIndexSet.add(pageNumber - 2); + pageIndexSet.add(pageNumber - 1); + } + const spread = document.createElement("div"); + spread.className = "spread"; + if (this.isInPresentationMode) { + const dummyPage = document.createElement("div"); + dummyPage.className = "dummyPage"; + spread.append(dummyPage); + } + for (const i of pageIndexSet) { + const pageView = this._pages[i]; + if (!pageView) { + continue; + } + spread.append(pageView.div); + state.pages.push(pageView); + } + viewer.append(spread); + } + state.scrollDown = pageNumber >= state.previousPageNumber; + state.previousPageNumber = pageNumber; + } + _scrollUpdate() { + if (this.pagesCount === 0) { + return; + } + this.update(); + } + #scrollIntoView(pageView, pageSpot = null) { + const { + div, + id + } = pageView; + if (this._currentPageNumber !== id) { + this._setCurrentPageNumber(id); + } + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + this.update(); + } + if (!pageSpot && !this.isInPresentationMode) { + const left = div.offsetLeft + div.clientLeft, + right = left + div.clientWidth; + const { + scrollLeft, + clientWidth + } = this.container; + if (this._scrollMode === ScrollMode.HORIZONTAL || left < scrollLeft || right > scrollLeft + clientWidth) { + pageSpot = { + left: 0, + top: 0 + }; + } + } + scrollIntoView(div, pageSpot); + if (!this._currentScaleValue && this._location) { + this._location = null; + } + } + #isSameScale(newScale) { + return newScale === this._currentScale || Math.abs(newScale - this._currentScale) < 1e-15; + } + #setScaleUpdatePages(newScale, newValue, { + noScroll = false, + preset = false, + drawingDelay = -1, + origin = null + }) { + this._currentScaleValue = newValue.toString(); + if (this.#isSameScale(newScale)) { + if (preset) { + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: newValue + }); + } + return; + } + this.viewer.style.setProperty("--scale-factor", newScale * PixelsPerInch.PDF_TO_CSS_UNITS); + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + this.refresh(true, { + scale: newScale, + drawingDelay: postponeDrawing ? drawingDelay : -1 + }); + if (postponeDrawing) { + this.#scaleTimeoutId = setTimeout(() => { + this.#scaleTimeoutId = null; + this.refresh(); + }, drawingDelay); + } + const previousScale = this._currentScale; + this._currentScale = newScale; + if (!noScroll) { + let page = this._currentPageNumber, + dest; + if (this._location && !(this.isInPresentationMode || this.isChangingPresentationMode)) { + page = this._location.pageNumber; + dest = [null, { + name: "XYZ" + }, this._location.left, this._location.top, null]; + } + this.scrollPageIntoView({ + pageNumber: page, + destArray: dest, + allowNegativeOffset: true + }); + if (Array.isArray(origin)) { + const scaleDiff = newScale / previousScale - 1; + const [top, left] = this.containerTopLeft; + this.container.scrollLeft += (origin[0] - left) * scaleDiff; + this.container.scrollTop += (origin[1] - top) * scaleDiff; + } + } + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: preset ? newValue : undefined + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get #pageWidthScaleFactor() { + if (this._spreadMode !== SpreadMode.NONE && this._scrollMode !== ScrollMode.HORIZONTAL) { + return 2; + } + return 1; + } + #setScale(value, options) { + let scale = parseFloat(value); + if (scale > 0) { + options.preset = false; + this.#setScaleUpdatePages(scale, value, options); + } else { + const currentPage = this._pages[this._currentPageNumber - 1]; + if (!currentPage) { + return; + } + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.isInPresentationMode) { + hPadding = vPadding = 4; + if (this._spreadMode !== SpreadMode.NONE) { + hPadding *= 2; + } + } else if (this.removePageBorders) { + hPadding = vPadding = 0; + } else if (this._scrollMode === ScrollMode.HORIZONTAL) { + [hPadding, vPadding] = [vPadding, hPadding]; + } + const pageWidthScale = (this.container.clientWidth - hPadding) / currentPage.width * currentPage.scale / this.#pageWidthScaleFactor; + const pageHeightScale = (this.container.clientHeight - vPadding) / currentPage.height * currentPage.scale; + switch (value) { + case "page-actual": + scale = 1; + break; + case "page-width": + scale = pageWidthScale; + break; + case "page-height": + scale = pageHeightScale; + break; + case "page-fit": + scale = Math.min(pageWidthScale, pageHeightScale); + break; + case "auto": + const horizontalScale = isPortraitOrientation(currentPage) ? pageWidthScale : Math.min(pageHeightScale, pageWidthScale); + scale = Math.min(MAX_AUTO_SCALE, horizontalScale); + break; + default: + console.error(`#setScale: "${value}" is an unknown zoom value.`); + return; + } + options.preset = true; + this.#setScaleUpdatePages(scale, value, options); + } + } + #resetCurrentPageView() { + const pageView = this._pages[this._currentPageNumber - 1]; + if (this.isInPresentationMode) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this.#scrollIntoView(pageView); + } + pageLabelToPageNumber(label) { + if (!this._pageLabels) { + return null; + } + const i = this._pageLabels.indexOf(label); + if (i < 0) { + return null; + } + return i + 1; + } + scrollPageIntoView({ + pageNumber, + destArray = null, + allowNegativeOffset = false, + ignoreDestinationZoom = false + }) { + if (!this.pdfDocument) { + return; + } + const pageView = Number.isInteger(pageNumber) && this._pages[pageNumber - 1]; + if (!pageView) { + console.error(`scrollPageIntoView: "${pageNumber}" is not a valid pageNumber parameter.`); + return; + } + if (this.isInPresentationMode || !destArray) { + this._setCurrentPageNumber(pageNumber, true); + return; + } + let x = 0, + y = 0; + let width = 0, + height = 0, + widthScale, + heightScale; + const changeOrientation = pageView.rotation % 180 !== 0; + const pageWidth = (changeOrientation ? pageView.height : pageView.width) / pageView.scale / PixelsPerInch.PDF_TO_CSS_UNITS; + const pageHeight = (changeOrientation ? pageView.width : pageView.height) / pageView.scale / PixelsPerInch.PDF_TO_CSS_UNITS; + let scale = 0; + switch (destArray[1].name) { + case "XYZ": + x = destArray[2]; + y = destArray[3]; + scale = destArray[4]; + x = x !== null ? x : 0; + y = y !== null ? y : pageHeight; + break; + case "Fit": + case "FitB": + scale = "page-fit"; + break; + case "FitH": + case "FitBH": + y = destArray[2]; + scale = "page-width"; + if (y === null && this._location) { + x = this._location.left; + y = this._location.top; + } else if (typeof y !== "number" || y < 0) { + y = pageHeight; + } + break; + case "FitV": + case "FitBV": + x = destArray[2]; + width = pageWidth; + height = pageHeight; + scale = "page-height"; + break; + case "FitR": + x = destArray[2]; + y = destArray[3]; + width = destArray[4] - x; + height = destArray[5] - y; + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.removePageBorders) { + hPadding = vPadding = 0; + } + widthScale = (this.container.clientWidth - hPadding) / width / PixelsPerInch.PDF_TO_CSS_UNITS; + heightScale = (this.container.clientHeight - vPadding) / height / PixelsPerInch.PDF_TO_CSS_UNITS; + scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); + break; + default: + console.error(`scrollPageIntoView: "${destArray[1].name}" is not a valid destination type.`); + return; + } + if (!ignoreDestinationZoom) { + if (scale && scale !== this._currentScale) { + this.currentScaleValue = scale; + } else if (this._currentScale === UNKNOWN_SCALE) { + this.currentScaleValue = DEFAULT_SCALE_VALUE; + } + } + if (scale === "page-fit" && !destArray[4]) { + this.#scrollIntoView(pageView); + return; + } + const boundingRect = [pageView.viewport.convertToViewportPoint(x, y), pageView.viewport.convertToViewportPoint(x + width, y + height)]; + let left = Math.min(boundingRect[0][0], boundingRect[1][0]); + let top = Math.min(boundingRect[0][1], boundingRect[1][1]); + if (!allowNegativeOffset) { + left = Math.max(left, 0); + top = Math.max(top, 0); + } + this.#scrollIntoView(pageView, { + left, + top + }); + } + _updateLocation(firstPage) { + const currentScale = this._currentScale; + const currentScaleValue = this._currentScaleValue; + const normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue; + const pageNumber = firstPage.id; + const currentPageView = this._pages[pageNumber - 1]; + const container = this.container; + const topLeft = currentPageView.getPagePoint(container.scrollLeft - firstPage.x, container.scrollTop - firstPage.y); + const intLeft = Math.round(topLeft[0]); + const intTop = Math.round(topLeft[1]); + let pdfOpenParams = `#page=${pageNumber}`; + if (!this.isInPresentationMode) { + pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`; + } + this._location = { + pageNumber, + scale: normalizedScaleValue, + top: intTop, + left: intLeft, + rotation: this._pagesRotation, + pdfOpenParams + }; + } + update() { + const visible = this._getVisiblePages(); + const visiblePages = visible.views, + numVisiblePages = visiblePages.length; + if (numVisiblePages === 0) { + return; + } + const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1); + this.#buffer.resize(newCacheSize, visible.ids); + this.renderingQueue.renderHighestPriority(visible); + const isSimpleLayout = this._spreadMode === SpreadMode.NONE && (this._scrollMode === ScrollMode.PAGE || this._scrollMode === ScrollMode.VERTICAL); + const currentId = this._currentPageNumber; + let stillFullyVisible = false; + for (const page of visiblePages) { + if (page.percent < 100) { + break; + } + if (page.id === currentId && isSimpleLayout) { + stillFullyVisible = true; + break; + } + } + this._setCurrentPageNumber(stillFullyVisible ? currentId : visiblePages[0].id); + this._updateLocation(visible.first); + this.eventBus.dispatch("updateviewarea", { + source: this, + location: this._location + }); + } + #switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { + ids, + views + } = visible; + for (const page of views) { + const { + view + } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + } + if (pagesToRefresh.length === 0) { + return null; + } + this.renderingQueue.renderHighestPriority({ + first: pagesToRefresh[0], + last: pagesToRefresh.at(-1), + views: pagesToRefresh, + ids + }); + return ids; + } + containsElement(element) { + return this.container.contains(element); + } + focus() { + this.container.focus(); + } + get _isContainerRtl() { + return getComputedStyle(this.container).direction === "rtl"; + } + get isInPresentationMode() { + return this.presentationModeState === PresentationModeState.FULLSCREEN; + } + get isChangingPresentationMode() { + return this.presentationModeState === PresentationModeState.CHANGING; + } + get isHorizontalScrollbarEnabled() { + return this.isInPresentationMode ? false : this.container.scrollWidth > this.container.clientWidth; + } + get isVerticalScrollbarEnabled() { + return this.isInPresentationMode ? false : this.container.scrollHeight > this.container.clientHeight; + } + _getVisiblePages() { + const views = this._scrollMode === ScrollMode.PAGE ? this.#scrollModePageState.pages : this._pages, + horizontal = this._scrollMode === ScrollMode.HORIZONTAL, + rtl = horizontal && this._isContainerRtl; + return getVisibleElements({ + scrollEl: this.container, + views, + sortByVisibility: true, + horizontal, + rtl + }); + } + cleanup() { + for (const pageView of this._pages) { + if (pageView.renderingState !== RenderingStates.FINISHED) { + pageView.reset(); + } + } + } + _cancelRendering() { + for (const pageView of this._pages) { + pageView.cancelRendering(); + } + } + async #ensurePdfPageLoaded(pageView) { + if (pageView.pdfPage) { + return pageView.pdfPage; + } + try { + const pdfPage = await this.pdfDocument.getPage(pageView.id); + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + return pdfPage; + } catch (reason) { + console.error("Unable to get page for page view", reason); + return null; + } + } + #getScrollAhead(visible) { + if (visible.first?.id === 1) { + return true; + } else if (visible.last?.id === this.pagesCount) { + return false; + } + switch (this._scrollMode) { + case ScrollMode.PAGE: + return this.#scrollModePageState.scrollDown; + case ScrollMode.HORIZONTAL: + return this.scroll.right; + } + return this.scroll.down; + } + forceRendering(currentlyVisiblePages) { + const visiblePages = currentlyVisiblePages || this._getVisiblePages(); + const scrollAhead = this.#getScrollAhead(visiblePages); + const preRenderExtra = this._spreadMode !== SpreadMode.NONE && this._scrollMode !== ScrollMode.HORIZONTAL; + const pageView = this.renderingQueue.getHighestPriority(visiblePages, this._pages, scrollAhead, preRenderExtra); + if (pageView) { + this.#ensurePdfPageLoaded(pageView).then(() => { + this.renderingQueue.renderView(pageView); + }); + return true; + } + return false; + } + get hasEqualPageSizes() { + const firstPageView = this._pages[0]; + for (let i = 1, ii = this._pages.length; i < ii; ++i) { + const pageView = this._pages[i]; + if (pageView.width !== firstPageView.width || pageView.height !== firstPageView.height) { + return false; + } + } + return true; + } + getPagesOverview() { + let initialOrientation; + return this._pages.map(pageView => { + const viewport = pageView.pdfPage.getViewport({ + scale: 1 + }); + const orientation = isPortraitOrientation(viewport); + if (initialOrientation === undefined) { + initialOrientation = orientation; + } else if (this.enablePrintAutoRotate && orientation !== initialOrientation) { + return { + width: viewport.height, + height: viewport.width, + rotation: (viewport.rotation - 90) % 360 + }; + } + return { + width: viewport.width, + height: viewport.height, + rotation: viewport.rotation + }; + }); + } + get optionalContentConfigPromise() { + if (!this.pdfDocument) { + return Promise.resolve(null); + } + if (!this._optionalContentConfigPromise) { + console.error("optionalContentConfigPromise: Not initialized yet."); + return this.pdfDocument.getOptionalContentConfig({ + intent: "display" + }); + } + return this._optionalContentConfigPromise; + } + set optionalContentConfigPromise(promise) { + if (!(promise instanceof Promise)) { + throw new Error(`Invalid optionalContentConfigPromise: ${promise}`); + } + if (!this.pdfDocument) { + return; + } + if (!this._optionalContentConfigPromise) { + return; + } + this._optionalContentConfigPromise = promise; + this.refresh(false, { + optionalContentConfigPromise: promise + }); + this.eventBus.dispatch("optionalcontentconfigchanged", { + source: this, + promise + }); + } + get scrollMode() { + return this._scrollMode; + } + set scrollMode(mode) { + if (this._scrollMode === mode) { + return; + } + if (!isValidScrollMode(mode)) { + throw new Error(`Invalid scroll mode: ${mode}`); + } + if (this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + return; + } + this._previousScrollMode = this._scrollMode; + this._scrollMode = mode; + this.eventBus.dispatch("scrollmodechanged", { + source: this, + mode + }); + this._updateScrollMode(this._currentPageNumber); + } + _updateScrollMode(pageNumber = null) { + const scrollMode = this._scrollMode, + viewer = this.viewer; + viewer.classList.toggle("scrollHorizontal", scrollMode === ScrollMode.HORIZONTAL); + viewer.classList.toggle("scrollWrapped", scrollMode === ScrollMode.WRAPPED); + if (!this.pdfDocument || !pageNumber) { + return; + } + if (scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._previousScrollMode === ScrollMode.PAGE) { + this._updateSpreadMode(); + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + get spreadMode() { + return this._spreadMode; + } + set spreadMode(mode) { + if (this._spreadMode === mode) { + return; + } + if (!isValidSpreadMode(mode)) { + throw new Error(`Invalid spread mode: ${mode}`); + } + this._spreadMode = mode; + this.eventBus.dispatch("spreadmodechanged", { + source: this, + mode + }); + this._updateSpreadMode(this._currentPageNumber); + } + _updateSpreadMode(pageNumber = null) { + if (!this.pdfDocument) { + return; + } + const viewer = this.viewer, + pages = this._pages; + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else { + viewer.textContent = ""; + if (this._spreadMode === SpreadMode.NONE) { + for (const pageView of this._pages) { + viewer.append(pageView.div); + } + } else { + const parity = this._spreadMode - 1; + let spread = null; + for (let i = 0, ii = pages.length; i < ii; ++i) { + if (spread === null) { + spread = document.createElement("div"); + spread.className = "spread"; + viewer.append(spread); + } else if (i % 2 === parity) { + spread = spread.cloneNode(false); + viewer.append(spread); + } + spread.append(pages[i].div); + } + } + } + if (!pageNumber) { + return; + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + _getPageAdvance(currentPageNumber, previous = false) { + switch (this._scrollMode) { + case ScrollMode.WRAPPED: + { + const { + views + } = this._getVisiblePages(), + pageLayout = new Map(); + for (const { + id, + y, + percent, + widthPercent + } of views) { + if (percent === 0 || widthPercent < 100) { + continue; + } + let yArray = pageLayout.get(y); + if (!yArray) { + pageLayout.set(y, yArray ||= []); + } + yArray.push(id); + } + for (const yArray of pageLayout.values()) { + const currentIndex = yArray.indexOf(currentPageNumber); + if (currentIndex === -1) { + continue; + } + const numPages = yArray.length; + if (numPages === 1) { + break; + } + if (previous) { + for (let i = currentIndex - 1, ii = 0; i >= ii; i--) { + const currentId = yArray[i], + expectedId = yArray[i + 1] - 1; + if (currentId < expectedId) { + return currentPageNumber - expectedId; + } + } + } else { + for (let i = currentIndex + 1, ii = numPages; i < ii; i++) { + const currentId = yArray[i], + expectedId = yArray[i - 1] + 1; + if (currentId > expectedId) { + return expectedId - currentPageNumber; + } + } + } + if (previous) { + const firstId = yArray[0]; + if (firstId < currentPageNumber) { + return currentPageNumber - firstId + 1; + } + } else { + const lastId = yArray[numPages - 1]; + if (lastId > currentPageNumber) { + return lastId - currentPageNumber + 1; + } + } + break; + } + break; + } + case ScrollMode.HORIZONTAL: + { + break; + } + case ScrollMode.PAGE: + case ScrollMode.VERTICAL: + { + if (this._spreadMode === SpreadMode.NONE) { + break; + } + const parity = this._spreadMode - 1; + if (previous && currentPageNumber % 2 !== parity) { + break; + } else if (!previous && currentPageNumber % 2 === parity) { + break; + } + const { + views + } = this._getVisiblePages(), + expectedId = previous ? currentPageNumber - 1 : currentPageNumber + 1; + for (const { + id, + percent, + widthPercent + } of views) { + if (id !== expectedId) { + continue; + } + if (percent > 0 && widthPercent === 100) { + return 2; + } + break; + } + break; + } + } + return 1; + } + nextPage() { + const currentPageNumber = this._currentPageNumber, + pagesCount = this.pagesCount; + if (currentPageNumber >= pagesCount) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, false) || 1; + this.currentPageNumber = Math.min(currentPageNumber + advance, pagesCount); + return true; + } + previousPage() { + const currentPageNumber = this._currentPageNumber; + if (currentPageNumber <= 1) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, true) || 1; + this.currentPageNumber = Math.max(currentPageNumber - advance, 1); + return true; + } + updateScale({ + drawingDelay, + scaleFactor = null, + steps = null, + origin + }) { + if (steps === null && scaleFactor === null) { + throw new Error("Invalid updateScale options: either `steps` or `scaleFactor` must be provided."); + } + if (!this.pdfDocument) { + return; + } + let newScale = this._currentScale; + if (scaleFactor > 0 && scaleFactor !== 1) { + newScale = Math.round(newScale * scaleFactor * 100) / 100; + } else if (steps) { + const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA; + const round = steps > 0 ? Math.ceil : Math.floor; + steps = Math.abs(steps); + do { + newScale = round((newScale * delta).toFixed(2) * 10) / 10; + } while (--steps > 0); + } + newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale)); + this.#setScale(newScale, { + noScroll: false, + drawingDelay, + origin + }); + } + increaseScale(options = {}) { + this.updateScale({ + ...options, + steps: options.steps ?? 1 + }); + } + decreaseScale(options = {}) { + this.updateScale({ + ...options, + steps: -(options.steps ?? 1) + }); + } + #updateContainerHeightCss(height = this.container.clientHeight) { + if (height !== this.#previousContainerHeight) { + this.#previousContainerHeight = height; + docStyle.setProperty("--viewer-container-height", `${height}px`); + } + } + #resizeObserverCallback(entries) { + for (const entry of entries) { + if (entry.target === this.container) { + this.#updateContainerHeightCss(Math.floor(entry.borderBoxSize[0].blockSize)); + this.#containerTopLeft = null; + break; + } + } + } + get containerTopLeft() { + return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft]; + } + #cleanupSwitchAnnotationEditorMode() { + this.#switchAnnotationEditorModeAC?.abort(); + this.#switchAnnotationEditorModeAC = null; + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + } + get annotationEditorMode() { + return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE; + } + set annotationEditorMode({ + mode, + editId = null, + isFromKeyboard = false + }) { + if (!this.#annotationEditorUIManager) { + throw new Error(`The AnnotationEditor is not enabled.`); + } + if (this.#annotationEditorMode === mode) { + return; + } + if (!isValidAnnotationEditorMode(mode)) { + throw new Error(`Invalid AnnotationEditor mode: ${mode}`); + } + if (!this.pdfDocument) { + return; + } + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + const { + eventBus + } = this; + const updater = () => { + this.#cleanupSwitchAnnotationEditorMode(); + this.#annotationEditorMode = mode; + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode + }); + }; + if (mode === AnnotationEditorType.NONE || this.#annotationEditorMode === AnnotationEditorType.NONE) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.annotationStorage.resetModifiedIds(); + } + for (const pageView of this._pages) { + pageView.toggleEditingMode(isEditing); + } + const idsToRefresh = this.#switchToEditAnnotationMode(); + if (isEditing && idsToRefresh) { + this.#cleanupSwitchAnnotationEditorMode(); + this.#switchAnnotationEditorModeAC = new AbortController(); + const signal = AbortSignal.any([this.#eventAbortController.signal, this.#switchAnnotationEditorModeAC.signal]); + eventBus._on("pagerendered", ({ + pageNumber + }) => { + idsToRefresh.delete(pageNumber); + if (idsToRefresh.size === 0) { + this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0); + } + }, { + signal + }); + return; + } + } + updater(); + } + refresh(noUpdate = false, updateArgs = Object.create(null)) { + if (!this.pdfDocument) { + return; + } + for (const pageView of this._pages) { + pageView.update(updateArgs); + } + if (this.#scaleTimeoutId !== null) { + clearTimeout(this.#scaleTimeoutId); + this.#scaleTimeoutId = null; + } + if (!noUpdate) { + this.update(); + } + } +} + +;// ./web/pdf_single_page_viewer.js + + +class PDFSinglePageViewer extends PDFViewer { + _resetView() { + super._resetView(); + this._scrollMode = ScrollMode.PAGE; + this._spreadMode = SpreadMode.NONE; + } + set scrollMode(mode) {} + _updateScrollMode() {} + set spreadMode(mode) {} + _updateSpreadMode() {} +} + +;// ./web/pdf_viewer.component.js + + + + + + + + + + + + + + + +const pdfjsVersion = "4.10.38"; +const pdfjsBuild = "f9bea397f"; + +var __webpack_exports__AnnotationLayerBuilder = __webpack_exports__.AnnotationLayerBuilder; +var __webpack_exports__DownloadManager = __webpack_exports__.DownloadManager; +var __webpack_exports__EventBus = __webpack_exports__.EventBus; +var __webpack_exports__FindState = __webpack_exports__.FindState; +var __webpack_exports__GenericL10n = __webpack_exports__.GenericL10n; +var __webpack_exports__LinkTarget = __webpack_exports__.LinkTarget; +var __webpack_exports__PDFFindController = __webpack_exports__.PDFFindController; +var __webpack_exports__PDFHistory = __webpack_exports__.PDFHistory; +var __webpack_exports__PDFLinkService = __webpack_exports__.PDFLinkService; +var __webpack_exports__PDFPageView = __webpack_exports__.PDFPageView; +var __webpack_exports__PDFScriptingManager = __webpack_exports__.PDFScriptingManager; +var __webpack_exports__PDFSinglePageViewer = __webpack_exports__.PDFSinglePageViewer; +var __webpack_exports__PDFViewer = __webpack_exports__.PDFViewer; +var __webpack_exports__ProgressBar = __webpack_exports__.ProgressBar; +var __webpack_exports__RenderingStates = __webpack_exports__.RenderingStates; +var __webpack_exports__ScrollMode = __webpack_exports__.ScrollMode; +var __webpack_exports__SimpleLinkService = __webpack_exports__.SimpleLinkService; +var __webpack_exports__SpreadMode = __webpack_exports__.SpreadMode; +var __webpack_exports__StructTreeLayerBuilder = __webpack_exports__.StructTreeLayerBuilder; +var __webpack_exports__TextLayerBuilder = __webpack_exports__.TextLayerBuilder; +var __webpack_exports__XfaLayerBuilder = __webpack_exports__.XfaLayerBuilder; +var __webpack_exports__parseQueryString = __webpack_exports__.parseQueryString; +export { __webpack_exports__AnnotationLayerBuilder as AnnotationLayerBuilder, __webpack_exports__DownloadManager as DownloadManager, __webpack_exports__EventBus as EventBus, __webpack_exports__FindState as FindState, __webpack_exports__GenericL10n as GenericL10n, __webpack_exports__LinkTarget as LinkTarget, __webpack_exports__PDFFindController as PDFFindController, __webpack_exports__PDFHistory as PDFHistory, __webpack_exports__PDFLinkService as PDFLinkService, __webpack_exports__PDFPageView as PDFPageView, __webpack_exports__PDFScriptingManager as PDFScriptingManager, __webpack_exports__PDFSinglePageViewer as PDFSinglePageViewer, __webpack_exports__PDFViewer as PDFViewer, __webpack_exports__ProgressBar as ProgressBar, __webpack_exports__RenderingStates as RenderingStates, __webpack_exports__ScrollMode as ScrollMode, __webpack_exports__SimpleLinkService as SimpleLinkService, __webpack_exports__SpreadMode as SpreadMode, __webpack_exports__StructTreeLayerBuilder as StructTreeLayerBuilder, __webpack_exports__TextLayerBuilder as TextLayerBuilder, __webpack_exports__XfaLayerBuilder as XfaLayerBuilder, __webpack_exports__parseQueryString as parseQueryString }; + +//# sourceMappingURL=pdf_viewer.mjs.map \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md new file mode 100644 index 00000000000..69fb53bf322 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md @@ -0,0 +1,7 @@ +tailwindcss version 4.0.3 +https://github.com/tailwindlabs/tailwindcss +License: MIT + +This template uses only `preflight.css`, the CSS reset stylesheet from Tailwind. For simplicity, this template doesn't use a complete deployment of Tailwind. If you want to use the full Tailwind library, remove `preflight.css` and reference the full version of Tailwind. + +To update, replace the `preflight.css` file with the one in an updated build from https://www.npmjs.com/package/tailwindcss diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css new file mode 100644 index 00000000000..178c881dd23 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css @@ -0,0 +1,383 @@ +/* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. +*/ + +*, +::after, +::before, +::backdrop, +::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ +} + +/* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + 5. Use the user's configured `sans` font-feature-settings by default. + 6. Use the user's configured `sans` font-variation-settings by default. + 7. Disable tap highlights on iOS. +*/ + +html, +:host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: var( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ +} + +/* + Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + line-height: inherit; +} + +/* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* + Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* + Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* + Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; +} + +/* + Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* + 1. Use the user's configured `mono` font-family by default. + 2. Use the user's configured `mono` font-feature-settings by default. + 3. Use the user's configured `mono` font-variation-settings by default. + 4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: var( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 4 */ + font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */ + font-size: 1em; /* 4 */ +} + +/* + Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* + Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* + Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* + Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* + Make lists unstyled by default. +*/ + +ol, +ul, +menu { + list-style: none; +} + +/* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. +*/ + +button, +input, +select, +optgroup, +textarea, +::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ +} + +/* + Restore default font weight. +*/ + +:where(select:is([multiple], [size])) optgroup { + font-weight: bolder; +} + +/* + Restore indentation. +*/ + +:where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; +} + +/* + Restore space after button. +*/ + +::file-selector-button { + margin-inline-end: 4px; +} + +/* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to a semi-transparent version of the current text color. +*/ + +::placeholder { + opacity: 1; /* 1 */ + color: color-mix(in oklab, currentColor 50%, transparent); /* 2 */ +} + +/* + Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* + Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. +*/ + +::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ +} + +/* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. +*/ + +::-webkit-datetime-edit { + display: inline-flex; +} + +/* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. +*/ + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-datetime-edit, +::-webkit-datetime-edit-year-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-minute-field, +::-webkit-datetime-edit-second-field, +::-webkit-datetime-edit-millisecond-field, +::-webkit-datetime-edit-meridiem-field { + padding-block: 0; +} + +/* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* + Correct the inability to style the border radius in iOS Safari. +*/ + +button, +input:where([type='button'], [type='reset'], [type='submit']), +::file-selector-button { + appearance: button; +} + +/* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* + Make elements with the HTML hidden attribute stay hidden by default. +*/ + +[hidden]:where(:not([hidden='until-found'])) { + display: none !important; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.sln b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.sln new file mode 100644 index 00000000000..67d2a3cad3c --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.AppHost", "aichatweb.AppHost\aichatweb.AppHost.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.ServiceDefaults", "aichatweb.ServiceDefaults\aichatweb.ServiceDefaults.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.Web", "aichatweb.Web\aichatweb.Web.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/README.md index 035bd4acaa6..cf7deff5878 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/README.md @@ -6,7 +6,7 @@ This project is an AI chat application that demonstrates how to chat with custom > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. # Configure the AI Model Provider -To use models hosted by GitHub Models, you will need to create a GitHub personal access token. The token should not have any scopes or permissions. See [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). +To use models hosted by GitHub Models, you will need to create a GitHub personal access token with `models:read` permissions, but no other scopes or permissions. See [Prototyping with AI models](https://docs.github.com/github-models/prototyping-with-ai-models) and [Managing your personal access tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) in the GitHub Docs for more information. From the command line, configure your token for this project using .NET User Secrets by running the following commands: diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 904c60567a4..448c2ce43b7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -17,8 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb.Web-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb.Web-documents"); + var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); await chunksCollection.CreateCollectionIfNotExistsAsync(); await documentsCollection.CreateCollectionIfNotExistsAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs index 2ab511c104c..47b30dd0646 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -10,7 +10,7 @@ public class SemanticSearch( public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb.Web-chunks"); + var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md new file mode 100644 index 00000000000..70e543ffeae --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md @@ -0,0 +1,60 @@ +# AI Chat with Custom Data + +This project is an AI chat application that demonstrates how to chat with custom data using an AI language model. Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](https://aka.ms/dotnet-chat-templatePreview2-survey). + +>[!NOTE] +> Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. + +### Known Issues + +#### Errors running Ollama or Docker + +A recent incompatibility was found between Ollama and Docker Desktop. This issue results in runtime errors when connecting to Ollama, and the workaround for that can lead to Docker not working for Aspire projects. + +This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See [ollama/ollama#9509](https://github.com/ollama/ollama/issues/9509#issuecomment-2842461831) for more information and a link to install the version of Docker Desktop with the fix. + +# Configure the AI Model Provider + +## Setting up a local environment for Ollama +This project is configured to run Ollama in a Docker container. Docker Desktop must be installed and running for the project to run successfully. An Ollama container will automatically start when running the application. + +Download, install, and run Docker Desktop from the [official website](https://www.docker.com/). Follow the installation instructions specific to your operating system. + +Note: Ollama and Docker are excellent open source products, but are not maintained by Microsoft. + + +## Setting up a local environment for Qdrant +This project is configured to run Qdrant in a Docker container. Docker Desktop must be installed and running for the project to run successfully. A Qdrant container will automatically start when running the application. + +Download, install, and run Docker Desktop from the [official website](https://www.docker.com/). Follow the installation instructions specific to your operating system. + +Note: Qdrant and Docker are excellent open source products, but are not maintained by Microsoft. + +# Running the application + +## Using Visual Studio + +1. Open the `.sln` file in Visual Studio. +2. Press `Ctrl+F5` or click the "Start" button in the toolbar to run the project. + +## Using Visual Studio Code + +1. Open the project folder in Visual Studio Code. +2. Install the [C# Dev Kit extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) for Visual Studio Code. +3. Once installed, Open the `Program.cs` file in the aichatweb.AppHost project. +4. Run the project by clicking the "Run" button in the Debug view. + +## Trust the localhost certificate + +Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. + +See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. + +# Updating JavaScript dependencies + +This template leverages JavaScript libraries to provide essential functionality. These libraries are located in the wwwroot/lib folder of the aichatweb.Web project. For instructions on updating each dependency, please refer to the README.md file in each respective folder. + +# Learn More +To learn more about development with .NET and AI, check out the following links: + +* [AI for .NET Developers](https://learn.microsoft.com/dotnet/ai/) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs new file mode 100644 index 00000000000..9521e1a0297 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs @@ -0,0 +1,22 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var ollama = builder.AddOllama("ollama") + .WithDataVolume(); +var chat = ollama.AddModel("chat", "llama3.2"); +var embeddings = ollama.AddModel("embeddings", "all-minilm"); + +var vectorDB = builder.AddQdrant("vectordb") + .WithDataVolume() + .WithLifetime(ContainerLifetime.Persistent); + +var webApp = builder.AddProject("aichatweb-app"); +webApp + .WithReference(chat) + .WithReference(embeddings) + .WaitFor(chat) + .WaitFor(embeddings); +webApp + .WithReference(vectorDB) + .WaitFor(vectorDB); + +builder.Build().Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..4444e808585 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:9999;http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj new file mode 100644 index 00000000000..a9a9fc626a9 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net9.0 + enable + enable + true + secret + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json new file mode 100644 index 00000000000..b0bacf42851 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.json new file mode 100644 index 00000000000..bfad98588cd --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000000..81cc28b27d2 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { +#pragma warning disable EXTEXP0001 // RemoveAllResilienceHandlers is experimental + http.RemoveAllResilienceHandlers(); +#pragma warning restore EXTEXP0001 + + // Turn on resilience by default + http.AddStandardResilienceHandler(config => + { + // Extend the HTTP Client timeout for Ollama + config.AttemptTimeout.Timeout = TimeSpan.FromMinutes(3); + + // Must be at least double the AttemptTimeout to pass options validation + config.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10); + config.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); + }); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddMeter("Experimental.Microsoft.Extensions.AI"); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddSource("Experimental.Microsoft.Extensions.AI"); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj new file mode 100644 index 00000000000..74656777aaf --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/App.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/App.razor new file mode 100644 index 00000000000..262359d5f5a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/App.razor @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + +@code { + private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor new file mode 100644 index 00000000000..116455ce45b --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor @@ -0,0 +1 @@ +
    diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css new file mode 100644 index 00000000000..d85b851a679 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css @@ -0,0 +1,89 @@ +/* Used under CC0 license */ + +.lds-ellipsis { + color: #666; + animation: fade-in 1s; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + + .lds-ellipsis, + .lds-ellipsis div { + box-sizing: border-box; + } + +.lds-ellipsis { + margin: auto; + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .lds-ellipsis div { + position: absolute; + top: 33.33333px; + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + + .lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; + } + + .lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; + } + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(0); + } +} + +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(24px, 0); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor new file mode 100644 index 00000000000..96fbbe6cc42 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
    diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000000..cda2020dcb0 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor new file mode 100644 index 00000000000..77557f20173 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor @@ -0,0 +1,13 @@ +
    + + +
    + How well is this template working for you? Please take a + brief survey + and tell us what you think. +
    +
    diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css new file mode 100644 index 00000000000..c939b902afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css @@ -0,0 +1,20 @@ +.surveyContainer { + display: flex; + justify-content: center; + gap: 0.5rem; + font-size: 0.9em; + margin: 0.5rem auto -0.7rem auto; + max-width: 1024px; + color: #444; +} + + .surveyContainer a { + text-decoration: underline; + } + + .surveyContainer .tool-icon { + margin-top: 0.15rem; + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor new file mode 100644 index 00000000000..718c2c46785 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -0,0 +1,109 @@ +@page "/" +@using System.ComponentModel +@inject IChatClient ChatClient +@inject NavigationManager Nav +@inject SemanticSearch Search +@implements IDisposable + +Chat + + + + + +
    To get started, try asking about these example documents. You can replace these with your own data and replace this message.
    + + +
    +
    + +
    + + + @* Remove this line to eliminate the template survey message *@ +
    + +@code { + private const string SystemPrompt = @" + You are an assistant who answers questions about information you retrieve. + Do not answer questions about anything else. + Use only simple markdown to format your responses. + + Use the search tool to find relevant information. When you do this, end your + reply with citations in the special XML format: + + exact quote here + + Always include the citation in your response if there are results. + + The quote must be max 5 words, taken word-for-word from the search result, and is the basis for why the citation is relevant. + Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. + "; + + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Display a new response from the IChatClient, streaming responses + // aren't supported because Ollama will not support both streaming and using Tools + currentResponseCancellation = new(); + var response = await ChatClient.GetResponseAsync(messages, chatOptions, currentResponseCancellation.Token); + + // Store responses in the conversation, and begin getting suggestions + messages.AddMessages(response); + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + } + + [Description("Searches for information using a phrase or keyword")] + private async Task> SearchAsync( + [Description("The phrase to search for.")] string searchPhrase, + [Description("If possible, specify the filename to search that file only. If not provided or empty, the search includes all files.")] string? filenameFilter = null) + { + await InvokeAsync(StateHasChanged); + var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); + return results.Select(result => + $"{result.Text}"); + } + + public void Dispose() + => currentResponseCancellation?.Cancel(); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css new file mode 100644 index 00000000000..98ed1ba7d1e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css @@ -0,0 +1,11 @@ +.chat-container { + position: sticky; + bottom: 0; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 1.5rem; + border-top-width: 1px; + background-color: #F3F4F6; + border-color: #E5E7EB; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor new file mode 100644 index 00000000000..ccb5853cec4 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor @@ -0,0 +1,38 @@ +@using System.Web +@if (!string.IsNullOrWhiteSpace(viewerUrl)) +{ + + + + +
    +
    @File
    +
    @Quote
    +
    +
    +} + +@code { + [Parameter] + public required string File { get; set; } + + [Parameter] + public int? PageNumber { get; set; } + + [Parameter] + public required string Quote { get; set; } + + private string? viewerUrl; + + protected override void OnParametersSet() + { + viewerUrl = null; + + // If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here + if (File.EndsWith(".pdf")) + { + var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\''); + viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true"; + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css new file mode 100644 index 00000000000..0ca029b7e64 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css @@ -0,0 +1,37 @@ +.citation { + display: inline-flex; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + margin-top: 1rem; + margin-right: 1rem; + border-bottom: 2px solid #a770de; + gap: 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + background-color: #ffffff; +} + + .citation[href]:hover { + outline: 1px solid #865cb1; + } + + .citation svg { + width: 1.5rem; + height: 1.5rem; + } + + .citation:active { + background-color: rgba(0,0,0,0.05); + } + +.citation-content { + display: flex; + flex-direction: column; +} + +.citation-file { + font-weight: 600; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor new file mode 100644 index 00000000000..12b1d524e23 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor @@ -0,0 +1,17 @@ +
    +
    + +
    + +

    aichatweb.Web

    +
    + +@code { + [Parameter] + public EventCallback OnNewChat { get; set; } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css new file mode 100644 index 00000000000..6adcc414540 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css @@ -0,0 +1,25 @@ +.chat-header-container { + top: 0; + padding: 1.5rem; +} + +.chat-header-controls { + margin-bottom: 1.5rem; +} + +h1 { + overflow: hidden; + text-overflow: ellipsis; +} + +.new-chat-icon { + width: 1.25rem; + height: 1.25rem; + color: rgb(55, 65, 81); +} + +@media (min-width: 768px) { + .chat-header-container { + position: sticky; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor new file mode 100644 index 00000000000..e87ac6ccf47 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor @@ -0,0 +1,51 @@ +@inject IJSRuntime JS + + + + + +@code { + private ElementReference textArea; + private string? messageText; + + [Parameter] + public EventCallback OnSend { get; set; } + + public ValueTask FocusAsync() + => textArea.FocusAsync(); + + private async Task SendMessageAsync() + { + if (messageText is { Length: > 0 } text) + { + messageText = null; + await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text)); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js"); + await module.InvokeVoidAsync("init", textArea); + await module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css new file mode 100644 index 00000000000..3b26c9af316 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css @@ -0,0 +1,57 @@ +.input-box { + display: flex; + flex-direction: column; + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 8px; + padding: 0.5rem 0.75rem; + margin-top: 0.75rem; +} + + .input-box:focus-within { + outline: 2px solid #4152d5; + } + +textarea { + resize: none; + border: none; + outline: none; + flex-grow: 1; +} + + textarea:placeholder-shown + .tools { + --send-button-color: #aaa; + } + +.tools { + display: flex; + margin-top: 1rem; + align-items: center; +} + +.tool-icon { + width: 1.25rem; + height: 1.25rem; +} + +.send-button { + color: var(--send-button-color); + margin-left: auto; +} + + .send-button:hover { + color: black; + } + +.attach { + background-color: white; + border-style: dashed; + color: #888; + border-color: #888; + padding: 3px 8px; +} + + .attach:hover { + background-color: #f0f0f0; + color: black; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js new file mode 100644 index 00000000000..39e18ac7b74 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js @@ -0,0 +1,43 @@ +export function init(elem) { + elem.focus(); + + // Auto-resize whenever the user types or if the value is set programmatically + elem.addEventListener('input', () => resizeToFit(elem)); + afterPropertyWritten(elem, 'value', () => resizeToFit(elem)); + + // Auto-submit the form on 'enter' keypress + elem.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + elem.dispatchEvent(new CustomEvent('change', { bubbles: true })); + elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true })); + } + }); +} + +function resizeToFit(elem) { + const lineHeight = parseFloat(getComputedStyle(elem).lineHeight); + + elem.rows = 1; + const numLines = Math.ceil(elem.scrollHeight / lineHeight); + elem.rows = Math.min(5, Math.max(1, numLines)); +} + +function afterPropertyWritten(target, propName, callback) { + const descriptor = getPropertyDescriptor(target, propName); + Object.defineProperty(target, propName, { + get: function () { + return descriptor.get.apply(this, arguments); + }, + set: function () { + const result = descriptor.set.apply(this, arguments); + callback(); + return result; + } + }); +} + +function getPropertyDescriptor(target, propertyName) { + return Object.getOwnPropertyDescriptor(target, propertyName) + || getPropertyDescriptor(Object.getPrototypeOf(target), propertyName); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor new file mode 100644 index 00000000000..92c20c70667 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor @@ -0,0 +1,94 @@ +@using System.Runtime.CompilerServices +@using System.Text.RegularExpressions +@using System.Linq + +@if (Message.Role == ChatRole.User) +{ +
    + @Message.Text +
    +} +else if (Message.Role == ChatRole.Assistant) +{ + foreach (var content in Message.Contents) + { + if (content is TextContent { Text: { Length: > 0 } text }) + { +
    +
    +
    + + + +
    +
    +
    Assistant
    +
    + + + @foreach (var citation in citations ?? []) + { + + } +
    +
    + } + else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true) + { + + } + } +} + +@code { + private static readonly ConditionalWeakTable SubscribersLookup = new(); + private static readonly Regex CitationRegex = new(@"(?.*?)", RegexOptions.NonBacktracking); + + private List<(string File, int? Page, string Quote)>? citations; + + [Parameter, EditorRequired] + public required ChatMessage Message { get; set; } + + [Parameter] + public bool InProgress { get; set;} + + protected override void OnInitialized() + { + SubscribersLookup.AddOrUpdate(Message, this); + + if (!InProgress && Message.Role == ChatRole.Assistant && Message.Text is { Length: > 0 } text) + { + ParseCitations(text); + } + } + + public static void NotifyChanged(ChatMessage source) + { + if (SubscribersLookup.TryGetValue(source, out var subscriber)) + { + subscriber.StateHasChanged(); + } + } + + private void ParseCitations(string text) + { + var matches = CitationRegex.Matches(text); + citations = matches.Any() + ? matches.Select(m => (m.Groups["file"].Value, int.TryParse(m.Groups["page"].Value, out var page) ? page : (int?)null, m.Groups["quote"].Value)).ToList() + : null; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css new file mode 100644 index 00000000000..10453454be8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css @@ -0,0 +1,120 @@ +.user-message { + background: rgb(182 215 232); + align-self: flex-end; + min-width: 25%; + max-width: calc(100% - 5rem); + padding: 0.5rem 1.25rem; + border-radius: 0.25rem; + color: #1F2937; + white-space: pre-wrap; +} + +.assistant-message, .assistant-search { + display: grid; + grid-template-rows: min-content; + grid-template-columns: 2rem minmax(0, 1fr); + gap: 0.25rem; +} + +.assistant-message-header { + font-weight: 600; +} + +.assistant-message-text { + grid-column-start: 2; +} + +.assistant-message-icon { + display: flex; + justify-content: center; + align-items: center; + border-radius: 9999px; + width: 1.5rem; + height: 1.5rem; + color: #ffffff; + background: #9b72ce; +} + + .assistant-message-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.assistant-search-icon { + display: flex; + justify-content: center; + align-items: center; + width: 1.5rem; + height: 1.5rem; +} + + .assistant-search-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search-content { + align-content: center; +} + +.assistant-search-phrase { + font-weight: 600; +} + +/* Default styling for markdown-formatted assistant messages */ +::deep ul { + list-style-type: disc; + margin-left: 1.5rem; +} + +::deep ol { + list-style-type: decimal; + margin-left: 1.5rem; +} + +::deep li { + margin: 0.5rem 0; +} + +::deep strong { + font-weight: 600; +} + +::deep h3 { + margin: 1rem 0; + font-weight: 600; +} + +::deep p + p { + margin-top: 1rem; +} + +::deep table { + margin: 1rem 0; +} + +::deep th { + text-align: left; + border-bottom: 1px solid silver; +} + +::deep th, ::deep td { + padding: 0.1rem 0.5rem; +} + +::deep th, ::deep tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); +} + +::deep pre > code { + background-color: white; + display: block; + padding: 0.5rem 1rem; + margin: 1rem 0; + overflow-x: auto; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor new file mode 100644 index 00000000000..d245f455f11 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor @@ -0,0 +1,42 @@ +@inject IJSRuntime JS + +
    + + @foreach (var message in Messages) + { + + } + + @if (InProgressMessage is not null) + { + + + } + else if (IsEmpty) + { +
    @NoMessagesContent
    + } +
    +
    + +@code { + [Parameter] + public required IEnumerable Messages { get; set; } + + [Parameter] + public ChatMessage? InProgressMessage { get; set; } + + [Parameter] + public RenderFragment? NoMessagesContent { get; set; } + + private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text)); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Activates the auto-scrolling behavior + await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js"); + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css new file mode 100644 index 00000000000..6fbf083c7fa --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css @@ -0,0 +1,22 @@ +.message-list-container { + margin: 2rem 1.5rem; + flex-grow: 1; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.no-messages { + text-align: center; + font-size: 1.25rem; + color: #999; + margin-top: calc(40vh - 18rem); +} + +chat-messages > ::deep div:last-of-type { + /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */ + margin-bottom: 2rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js new file mode 100644 index 00000000000..3de8de273b8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js @@ -0,0 +1,34 @@ +// The following logic provides auto-scroll behavior for the chat messages list. +// If you don't want that behavior, you can simply not load this module. + +window.customElements.define('chat-messages', class ChatMessages extends HTMLElement { + static _isFirstAutoScroll = true; + + connectedCallback() { + this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations)); + this._observer.observe(this, { childList: true, attributes: true }); + } + + disconnectedCallback() { + this._observer.disconnect(); + } + + _scheduleAutoScroll(mutations) { + // Debounce the calls in case multiple DOM updates occur together + cancelAnimationFrame(this._nextAutoScroll); + this._nextAutoScroll = requestAnimationFrame(() => { + const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message'))); + const elem = this.lastElementChild; + if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) { + elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' }); + ChatMessages._isFirstAutoScroll = false; + } + }); + } + + _elemIsNearScrollBoundary(elem, threshold) { + const maxScrollPos = document.body.scrollHeight - window.innerHeight; + const remainingScrollDistance = maxScrollPos - window.scrollY; + return remainingScrollDistance < elem.offsetHeight + threshold; + } +}); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor new file mode 100644 index 00000000000..69ca922a8ce --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor @@ -0,0 +1,78 @@ +@inject IChatClient ChatClient + +@if (suggestions is not null) +{ +
    + @foreach (var suggestion in suggestions) + { + + } +
    +} + +@code { + private static string Prompt = @" + Suggest up to 3 follow-up questions that I could ask you to help me complete my task. + Each suggestion must be a complete sentence, maximum 6 words. + Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message, + for example 'How do I do that?' or 'Explain ...'. + If there are no suggestions, reply with an empty list. + "; + + private string[]? suggestions; + private CancellationTokenSource? cancellation; + + [Parameter] + public EventCallback OnSelected { get; set; } + + public void Clear() + { + suggestions = null; + cancellation?.Cancel(); + } + + public void Update(IReadOnlyList messages) + { + // Runs in the background and handles its own cancellation/errors + _ = UpdateSuggestionsAsync(messages); + } + + private async Task UpdateSuggestionsAsync(IReadOnlyList messages) + { + cancellation?.Cancel(); + cancellation = new CancellationTokenSource(); + + try + { + var response = await ChatClient.GetResponseAsync( + [.. ReduceMessages(messages), new(ChatRole.User, Prompt)], + cancellationToken: cancellation.Token); + if (!response.TryGetResult(out suggestions)) + { + suggestions = null; + } + + StateHasChanged(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + await DispatchExceptionAsync(ex); + } + } + + private async Task AddSuggestionAsync(string text) + { + await OnSelected.InvokeAsync(new(ChatRole.User, text)); + } + + private IEnumerable ReduceMessages(IReadOnlyList messages) + { + // Get any leading system messages, plus up to 5 user/assistant messages + // This should be enough context to generate suggestions without unnecessarily resending entire conversations when long + var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System); + var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5); + return systemMessages.Concat(otherMessages); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css new file mode 100644 index 00000000000..b291042c6d4 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css @@ -0,0 +1,9 @@ +.suggestions { + text-align: right; + white-space: nowrap; + gap: 0.5rem; + justify-content: flex-end; + flex-wrap: wrap; + display: flex; + margin-bottom: 0.75rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor new file mode 100644 index 00000000000..576cc2d2f4d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

    Error.

    +

    An error occurred while processing your request.

    + +@if (ShowRequestId) +{ +

    + Request ID: @RequestId +

    +} + +

    Development Mode

    +

    + Swapping to Development environment will display more detailed information about the error that occurred. +

    +

    + The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

    + +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Routes.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Routes.razor new file mode 100644 index 00000000000..f756e19dfbc --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/_Imports.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/_Imports.razor new file mode 100644 index 00000000000..fa7cadef6ea --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.Extensions.AI +@using Microsoft.JSInterop +@using aichatweb.Web +@using aichatweb.Web.Components +@using aichatweb.Web.Components.Layout +@using aichatweb.Web.Services diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs new file mode 100644 index 00000000000..a38b248d45e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; +using aichatweb.Web.Components; +using aichatweb.Web.Services; +using aichatweb.Web.Services.Ingestion; +using Microsoft.SemanticKernel.Connectors.Qdrant; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +builder.AddOllamaApiClient("chat") + .AddChatClient() + .UseFunctionInvocation() + .UseOpenTelemetry(configure: c => + c.EnableSensitiveData = builder.Environment.IsDevelopment()); +builder.AddOllamaApiClient("embeddings") + .AddEmbeddingGenerator(); + +builder.AddQdrantClient("vectordb"); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseAntiforgery(); + +app.UseStaticFiles(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// By default, we ingest PDF files from the /wwwroot/Data directory. You can ingest from +// other sources by implementing IIngestionSource. +// Important: ensure that any content you ingest is trusted, as it may be reflected back +// to users or could be a source of prompt injection risk. +await DataIngestor.IngestDataAsync( + app.Services, + new PDFDirectorySource(Path.Combine(builder.Environment.WebRootPath, "Data"))); + +app.Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json new file mode 100644 index 00000000000..e2d900a219d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:9999;http://localhost:9999", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs new file mode 100644 index 00000000000..018a4e5b4e5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class IngestedChunk +{ + [VectorStoreRecordKey] + public required Guid Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public int PageNumber { get; set; } + + [VectorStoreRecordData] + public required string Text { get; set; } + + [VectorStoreRecordVector(384, DistanceFunction = DistanceFunction.CosineSimilarity)] // 384 is the default vector size for the all-minilm embedding model + public ReadOnlyMemory Vector { get; set; } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs new file mode 100644 index 00000000000..1cce8f3566c --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class IngestedDocument +{ + [VectorStoreRecordKey] + public required Guid Key { get; set; } + + [VectorStoreRecordData(IsIndexed = true)] + public required string SourceId { get; set; } + + [VectorStoreRecordData] + public required string DocumentId { get; set; } + + [VectorStoreRecordData] + public required string DocumentVersion { get; set; } + + // The vector is not used but required for some vector databases + [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs new file mode 100644 index 00000000000..0b1aaa24803 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services.Ingestion; + +public class DataIngestor( + ILogger logger, + IEmbeddingGenerator> embeddingGenerator, + IVectorStore vectorStore) +{ + public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) + { + using var scope = services.CreateScope(); + var ingestor = scope.ServiceProvider.GetRequiredService(); + await ingestor.IngestDataAsync(source); + } + + public async Task IngestDataAsync(IIngestionSource source) + { + var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); + await chunksCollection.CreateCollectionIfNotExistsAsync(); + await documentsCollection.CreateCollectionIfNotExistsAsync(); + + var sourceId = source.SourceId; + var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); + + var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); + foreach (var deletedDocument in deletedDocuments) + { + logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + await DeleteChunksForDocumentAsync(deletedDocument); + await documentsCollection.DeleteAsync(deletedDocument.Key); + } + + var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); + foreach (var modifiedDocument in modifiedDocuments) + { + logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + await DeleteChunksForDocumentAsync(modifiedDocument); + + await documentsCollection.UpsertAsync(modifiedDocument); + + var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + await chunksCollection.UpsertAsync(newRecords); + } + + logger.LogInformation("Ingestion is up-to-date"); + + async Task DeleteChunksForDocumentAsync(IngestedDocument document) + { + var documentId = document.DocumentId; + var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); + if (chunksToDelete.Any()) + { + await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs new file mode 100644 index 00000000000..208b32b2fdf --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.AI; + +namespace aichatweb.Web.Services.Ingestion; + +public interface IIngestionSource +{ + string SourceId { get; } + + Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments); + + Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); + + Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs new file mode 100644 index 00000000000..edf5a888880 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -0,0 +1,75 @@ +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.Text; +using UglyToad.PdfPig; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; + +namespace aichatweb.Web.Services.Ingestion; + +public class PDFDirectorySource(string sourceDirectory) : IIngestionSource +{ + public static string SourceFileId(string path) => Path.GetFileName(path); + public static string SourceFileVersion(string path) => File.GetLastWriteTimeUtc(path).ToString("o"); + + public string SourceId => $"{nameof(PDFDirectorySource)}:{sourceDirectory}"; + + public Task> GetNewOrModifiedDocumentsAsync(IReadOnlyList existingDocuments) + { + var results = new List(); + var sourceFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var existingDocumentsById = existingDocuments.ToDictionary(d => d.DocumentId); + + foreach (var sourceFile in sourceFiles) + { + var sourceFileId = SourceFileId(sourceFile); + var sourceFileVersion = SourceFileVersion(sourceFile); + var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; + if (existingDocumentVersion != sourceFileVersion) + { + results.Add(new() { Key = Guid.CreateVersion7(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); + } + } + + return Task.FromResult((IEnumerable)results); + } + + public Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments) + { + var currentFiles = Directory.GetFiles(sourceDirectory, "*.pdf"); + var currentFileIds = currentFiles.ToLookup(SourceFileId); + var deletedDocuments = existingDocuments.Where(d => !currentFileIds.Contains(d.DocumentId)); + return Task.FromResult(deletedDocuments); + } + + public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + { + using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); + var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); + + var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); + + return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + { + Key = Guid.CreateVersion7(), + DocumentId = document.DocumentId, + PageNumber = pair.First.PageNumber, + Text = pair.First.Text, + Vector = pair.Second.Vector, + }); + } + + private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) + { + var letters = pdfPage.Letters; + var words = NearestNeighbourWordExtractor.Instance.GetWords(letters); + var textBlocks = DocstrumBoundingBoxes.Instance.GetBlocks(words); + var pageText = string.Join(Environment.NewLine + Environment.NewLine, + textBlocks.Select(t => t.Text.ReplaceLineEndings(" "))); + +#pragma warning disable SKEXP0050 // Type is for evaluation purposes only + return TextChunker.SplitPlainTextParagraphs([pageText], 200) + .Select((text, index) => (pdfPage.Number, index, text)); +#pragma warning restore SKEXP0050 // Type is for evaluation purposes only + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs new file mode 100644 index 00000000000..b39ae8027a2 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Web.Services; + +public class SemanticSearch( + IEmbeddingGenerator> embeddingGenerator, + IVectorStore vectorStore) +{ + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) + { + var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); + var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); + + var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + { + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, + }); + + return await nearest.Select(result => result.Record).ToListAsync(); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj new file mode 100644 index 00000000000..fcd785dbe01 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + secret + + + + + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.Development.json new file mode 100644 index 00000000000..e22bd83cf3a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.json new file mode 100644 index 00000000000..d286041f99d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/Data/Example_Emergency_Survival_Kit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..94625f0e0e0617c1324cf5243b06d0e8a2da6177 GIT binary patch literal 153539 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

    VE%a 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 W4tn!Pf-WddMQHNc+UE8a^8W?ym9>ok literal 0 HcmV?d00001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.css new file mode 100644 index 00000000000..0dec580e2fd --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.css @@ -0,0 +1,94 @@ +@import url('lib/tailwindcss/dist/preflight.css'); + +html { + min-height: 100vh; +} + +html, .main-background-gradient { + background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem); +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + + html::after { + content: ''; + background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red); + width: 100%; + height: 2px; + position: fixed; + top: 0; + } + +h1 { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: 600; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.btn-default { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #9CA3AF; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + background-color: #D1D5DB; +} + + .btn-default:hover { + background-color: #E5E7EB; + } + +.btn-subtle { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #D1D5DB; + font-size: 0.875rem; + line-height: 1.25rem; +} + + .btn-subtle:hover { + border-color: #93C5FD; + background-color: #DBEAFE; + } + +.page-width { + max-width: 1024px; + margin: auto; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.js new file mode 100644 index 00000000000..8b2cecd007d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/app.js @@ -0,0 +1,24 @@ +import DOMPurify from './lib/dompurify/dist/purify.es.mjs'; +import * as marked from './lib/marked/dist/marked.esm.js'; + +const purify = DOMPurify(window); + +customElements.define('assistant-message', class extends HTMLElement { + static observedAttributes = ['markdown']; + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'markdown') { + newValue = newValue.replace(//gs, ''); + const elements = marked.parse(newValue.replace(/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 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return apply(func, thisArg, args); + }; +} +/** + * Creates a new function that constructs an instance of the given constructor function with the provided arguments. + * + * @param func - The constructor function to be wrapped and called. + * @returns A new function that constructs an instance of the given constructor function with the provided arguments. + */ +function unconstruct(func) { + return function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return construct(func, args); + }; +} +/** + * Add properties to a lookup table + * + * @param set - The set to which elements will be added. + * @param array - The array containing elements to be added to the set. + * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. + * @returns The modified set with added elements. + */ +function addToSet(set, array) { + let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; + if (setPrototypeOf) { + // Make 'in' and truthy checks like Boolean(set.constructor) + // independent of any properties defined on Object.prototype. + // Prevent prototype setters from intercepting set as a this value. + setPrototypeOf(set, null); + } + let l = array.length; + while (l--) { + let element = array[l]; + if (typeof element === 'string') { + const lcElement = transformCaseFunc(element); + if (lcElement !== element) { + // Config presets (e.g. tags.js, attrs.js) are immutable. + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set[element] = true; + } + return set; +} +/** + * Clean up an array to harden against CSPP + * + * @param array - The array to be cleaned. + * @returns The cleaned version of the array + */ +function cleanArray(array) { + for (let index = 0; index < array.length; index++) { + const isPropertyExist = objectHasOwnProperty(array, index); + if (!isPropertyExist) { + array[index] = null; + } + } + return array; +} +/** + * Shallow clone an object + * + * @param object - The object to be cloned. + * @returns A new object that copies the original. + */ +function clone(object) { + const newObject = create(null); + for (const [property, value] of entries(object)) { + const isPropertyExist = objectHasOwnProperty(object, property); + if (isPropertyExist) { + if (Array.isArray(value)) { + newObject[property] = cleanArray(value); + } else if (value && typeof value === 'object' && value.constructor === Object) { + newObject[property] = clone(value); + } else { + newObject[property] = value; + } + } + } + return newObject; +} +/** + * This method automatically checks if the prop is function or getter and behaves accordingly. + * + * @param object - The object to look up the getter function in its prototype chain. + * @param prop - The property name for which to find the getter function. + * @returns The getter function found in the prototype chain or a fallback function. + */ +function lookupGetter(object, prop) { + while (object !== null) { + const desc = getOwnPropertyDescriptor(object, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === 'function') { + return unapply(desc.value); + } + } + object = getPrototypeOf(object); + } + function fallbackValue() { + return null; + } + return fallbackValue; +} + +const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); +const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); +const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); +// List of SVG elements that are disallowed by default. +// We still need to know them so that we can do namespace +// checks properly in case one wants to add them to +// allow-list. +const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); +const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); +// Similarly to SVG, we want to know all MathML elements, +// even those that we disallow by default. +const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); +const text = freeze(['#text']); + +const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']); +const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); +const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); +const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); + +// eslint-disable-next-line unicorn/better-regex +const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode +const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); +const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex +const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape +const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape +const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape +); +const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); +const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex +); +const DOCTYPE_NAME = seal(/^html$/i); +const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); + +var EXPRESSIONS = /*#__PURE__*/Object.freeze({ + __proto__: null, + ARIA_ATTR: ARIA_ATTR, + ATTR_WHITESPACE: ATTR_WHITESPACE, + CUSTOM_ELEMENT: CUSTOM_ELEMENT, + DATA_ATTR: DATA_ATTR, + DOCTYPE_NAME: DOCTYPE_NAME, + ERB_EXPR: ERB_EXPR, + IS_ALLOWED_URI: IS_ALLOWED_URI, + IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, + MUSTACHE_EXPR: MUSTACHE_EXPR, + TMPLIT_EXPR: TMPLIT_EXPR +}); + +/* eslint-disable @typescript-eslint/indent */ +// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType +const NODE_TYPE = { + element: 1, + attribute: 2, + text: 3, + cdataSection: 4, + entityReference: 5, + // Deprecated + entityNode: 6, + // Deprecated + progressingInstruction: 7, + comment: 8, + document: 9, + documentType: 10, + documentFragment: 11, + notation: 12 // Deprecated +}; +const getGlobal = function getGlobal() { + return typeof window === 'undefined' ? null : window; +}; +/** + * Creates a no-op policy for internal use only. + * Don't export this function outside this module! + * @param trustedTypes The policy factory. + * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). + * @return The policy created (or null, if Trusted Types + * are not supported or creating the policy failed). + */ +const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { + if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { + return null; + } + // Allow the callers to control the unique policy name + // by adding a data-tt-policy-suffix to the script element with the DOMPurify. + // Policy creation with duplicate names throws in Trusted Types. + let suffix = null; + const ATTR_NAME = 'data-tt-policy-suffix'; + if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { + suffix = purifyHostElement.getAttribute(ATTR_NAME); + } + const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); + try { + return trustedTypes.createPolicy(policyName, { + createHTML(html) { + return html; + }, + createScriptURL(scriptUrl) { + return scriptUrl; + } + }); + } catch (_) { + // Policy creation failed (most likely another DOMPurify script has + // already run). Skip creating the policy, as this will only cause errors + // if TT are enforced. + console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); + return null; + } +}; +const _createHooksMap = function _createHooksMap() { + return { + afterSanitizeAttributes: [], + afterSanitizeElements: [], + afterSanitizeShadowDOM: [], + beforeSanitizeAttributes: [], + beforeSanitizeElements: [], + beforeSanitizeShadowDOM: [], + uponSanitizeAttribute: [], + uponSanitizeElement: [], + uponSanitizeShadowNode: [] + }; +}; +function createDOMPurify() { + let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); + const DOMPurify = root => createDOMPurify(root); + DOMPurify.version = '3.2.4'; + DOMPurify.removed = []; + if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { + // Not running in a browser, provide a factory function + // so that you can pass your own Window + DOMPurify.isSupported = false; + return DOMPurify; + } + let { + document + } = window; + const originalDocument = document; + const currentScript = originalDocument.currentScript; + const { + DocumentFragment, + HTMLTemplateElement, + Node, + Element, + NodeFilter, + NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, + HTMLFormElement, + DOMParser, + trustedTypes + } = window; + const ElementPrototype = Element.prototype; + const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); + const remove = lookupGetter(ElementPrototype, 'remove'); + const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); + const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); + const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); + // As per issue #47, the web-components registry is inherited by a + // new document created via createHTMLDocument. As per the spec + // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) + // a new empty registry is used when creating a template contents owner + // document, so we use that as our parent document to ensure nothing + // is inherited. + if (typeof HTMLTemplateElement === 'function') { + const template = document.createElement('template'); + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument; + } + } + let trustedTypesPolicy; + let emptyHTML = ''; + const { + implementation, + createNodeIterator, + createDocumentFragment, + getElementsByTagName + } = document; + const { + importNode + } = originalDocument; + let hooks = _createHooksMap(); + /** + * Expose whether this browser supports running the full DOMPurify. + */ + DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; + const { + MUSTACHE_EXPR, + ERB_EXPR, + TMPLIT_EXPR, + DATA_ATTR, + ARIA_ATTR, + IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE, + CUSTOM_ELEMENT + } = EXPRESSIONS; + let { + IS_ALLOWED_URI: IS_ALLOWED_URI$1 + } = EXPRESSIONS; + /** + * We consider the elements and attributes below to be safe. Ideally + * don't add any new ones but feel free to remove unwanted ones. + */ + /* allowed element names */ + let ALLOWED_TAGS = null; + const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); + /* Allowed attribute names */ + let ALLOWED_ATTR = null; + const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); + /* + * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. + * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) + * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) + * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. + */ + let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })); + /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ + let FORBID_TAGS = null; + /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ + let FORBID_ATTR = null; + /* Decide if ARIA attributes are okay */ + let ALLOW_ARIA_ATTR = true; + /* Decide if custom data attributes are okay */ + let ALLOW_DATA_ATTR = true; + /* Decide if unknown protocols are okay */ + let ALLOW_UNKNOWN_PROTOCOLS = false; + /* Decide if self-closing tags in attributes are allowed. + * Usually removed due to a mXSS issue in jQuery 3.0 */ + let ALLOW_SELF_CLOSE_IN_ATTR = true; + /* Output should be safe for common template engines. + * This means, DOMPurify removes data attributes, mustaches and ERB + */ + let SAFE_FOR_TEMPLATES = false; + /* Output should be safe even for XML used within HTML and alike. + * This means, DOMPurify removes comments when containing risky content. + */ + let SAFE_FOR_XML = true; + /* Decide if document with ... should be returned */ + let WHOLE_DOCUMENT = false; + /* Track whether config is already set on this instance of DOMPurify. */ + let SET_CONFIG = false; + /* Decide if all elements (e.g. style, script) must be children of + * document.body. By default, browsers might move them to document.head */ + let FORCE_BODY = false; + /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported). + * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead + */ + let RETURN_DOM = false; + /* Decide if a DOM `DocumentFragment` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported) */ + let RETURN_DOM_FRAGMENT = false; + /* Try to return a Trusted Type object instead of a string, return a string in + * case Trusted Types are not supported */ + let RETURN_TRUSTED_TYPE = false; + /* Output should be free from DOM clobbering attacks? + * This sanitizes markups named with colliding, clobberable built-in DOM APIs. + */ + let SANITIZE_DOM = true; + /* Achieve full DOM Clobbering protection by isolating the namespace of named + * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. + * + * HTML/DOM spec rules that enable DOM Clobbering: + * - Named Access on Window (§7.3.3) + * - DOM Tree Accessors (§3.1.5) + * - Form Element Parent-Child Relations (§4.10.3) + * - Iframe srcdoc / Nested WindowProxies (§4.8.5) + * - HTMLCollection (§4.2.10.2) + * + * Namespace isolation is implemented by prefixing `id` and `name` attributes + * with a constant string, i.e., `user-content-` + */ + let SANITIZE_NAMED_PROPS = false; + const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; + /* Keep element content when removing element? */ + let KEEP_CONTENT = true; + /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead + * of importing it into a new Document and returning a sanitized copy */ + let IN_PLACE = false; + /* Allow usage of profiles like html, svg and mathMl */ + let USE_PROFILES = {}; + /* Tags to ignore content of when KEEP_CONTENT is true */ + let FORBID_CONTENTS = null; + const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); + /* Tags that are safe for data: URIs */ + let DATA_URI_TAGS = null; + const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); + /* Attributes safe for values like "javascript:" */ + let URI_SAFE_ATTRIBUTES = null; + const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); + const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; + const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + /* Document namespace */ + let NAMESPACE = HTML_NAMESPACE; + let IS_EMPTY_INPUT = false; + /* Allowed XHTML+XML namespaces */ + let ALLOWED_NAMESPACES = null; + const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); + let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); + let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']); + // Certain elements are allowed in both SVG and HTML + // namespace. We need to specify them explicitly + // so that they don't get erroneously deleted from + // HTML namespace. + const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); + /* Parsing of strict XHTML documents */ + let PARSER_MEDIA_TYPE = null; + const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; + const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; + let transformCaseFunc = null; + /* Keep a reference to config to pass to hooks */ + let CONFIG = null; + /* Ideally, do not touch anything below this line */ + /* ______________________________________________ */ + const formElement = document.createElement('form'); + const isRegexOrFunction = function isRegexOrFunction(testValue) { + return testValue instanceof RegExp || testValue instanceof Function; + }; + /** + * _parseConfig + * + * @param cfg optional config literal + */ + // eslint-disable-next-line complexity + const _parseConfig = function _parseConfig() { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (CONFIG && CONFIG === cfg) { + return; + } + /* Shield configuration object from tampering */ + if (!cfg || typeof cfg !== 'object') { + cfg = {}; + } + /* Shield configuration object from prototype pollution */ + cfg = clone(cfg); + PARSER_MEDIA_TYPE = + // eslint-disable-next-line unicorn/prefer-includes + SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; + // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. + transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; + /* Set configuration parameters */ + ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; + ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; + URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS; + FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; + FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; + FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; + USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false + ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false + SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false + RETURN_DOM = cfg.RETURN_DOM || false; // Default false + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false + FORCE_BODY = cfg.FORCE_BODY || false; // Default false + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true + SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true + IN_PLACE = cfg.IN_PLACE || false; // Default false + IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; + NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; + MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS; + HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS; + CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { + CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { + CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { + CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; + } + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + /* Parse profile info */ + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, text); + ALLOWED_ATTR = []; + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html$1); + addToSet(ALLOWED_ATTR, html); + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg$1); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl$1); + addToSet(ALLOWED_ATTR, mathMl); + addToSet(ALLOWED_ATTR, xml); + } + } + /* Merge configuration parameters */ + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); + } + if (cfg.FORBID_CONTENTS) { + if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { + FORBID_CONTENTS = clone(FORBID_CONTENTS); + } + addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); + } + /* Add #text in case KEEP_CONTENT is set to true */ + if (KEEP_CONTENT) { + ALLOWED_TAGS['#text'] = true; + } + /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); + } + /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ['tbody']); + delete FORBID_TAGS.tbody; + } + if (cfg.TRUSTED_TYPES_POLICY) { + if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); + } + if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); + } + // Overwrite existing TrustedTypes policy. + trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; + // Sign local variables required by `sanitize`. + emptyHTML = trustedTypesPolicy.createHTML(''); + } else { + // Uninitialized policy, attempt to initialize the internal dompurify policy. + if (trustedTypesPolicy === undefined) { + trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); + } + // If creating the internal policy succeeded sign internal variables. + if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { + emptyHTML = trustedTypesPolicy.createHTML(''); + } + } + // Prevent further manipulation of configuration. + // Not available in IE8, Safari 5, etc. + if (freeze) { + freeze(cfg); + } + CONFIG = cfg; + }; + /* Keep track of all possible SVG and MathML tags + * so that we can perform the namespace checks + * correctly. */ + const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); + const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); + /** + * @param element a DOM element whose namespace is being checked + * @returns Return false if the element has a + * namespace that a spec-compliant parser would never + * return. Return true otherwise. + */ + const _checkValidNamespace = function _checkValidNamespace(element) { + let parent = getParentNode(element); + // In JSDOM, if we're inside shadow DOM, then parentNode + // can be null. We just simulate parent in this case. + if (!parent || !parent.tagName) { + parent = { + namespaceURI: NAMESPACE, + tagName: 'template' + }; + } + const tagName = stringToLowerCase(element.tagName); + const parentTagName = stringToLowerCase(parent.tagName); + if (!ALLOWED_NAMESPACES[element.namespaceURI]) { + return false; + } + if (element.namespaceURI === SVG_NAMESPACE) { + // The only way to switch from HTML namespace to SVG + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'svg'; + } + // The only way to switch from MathML to SVG is via` + // svg if parent is either or MathML + // text integration points. + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } + // We only allow elements that are defined in SVG + // spec. All others are disallowed in SVG namespace. + return Boolean(ALL_SVG_TAGS[tagName]); + } + if (element.namespaceURI === MATHML_NAMESPACE) { + // The only way to switch from HTML namespace to MathML + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'math'; + } + // The only way to switch from SVG to MathML is via + // and HTML integration points + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; + } + // We only allow elements that are defined in MathML + // spec. All others are disallowed in MathML namespace. + return Boolean(ALL_MATHML_TAGS[tagName]); + } + if (element.namespaceURI === HTML_NAMESPACE) { + // The only way to switch from SVG to HTML is via + // HTML integration points, and from MathML to HTML + // is via MathML text integration points + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } + // We disallow tags that are specific for MathML + // or SVG and should never appear in HTML namespace + return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); + } + // For XHTML and XML documents that support custom namespaces + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { + return true; + } + // The code should never reach this place (this means + // that the element somehow got namespace that is not + // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). + // Return false just in case. + return false; + }; + /** + * _forceRemove + * + * @param node a DOM node + */ + const _forceRemove = function _forceRemove(node) { + arrayPush(DOMPurify.removed, { + element: node + }); + try { + // eslint-disable-next-line unicorn/prefer-dom-node-remove + getParentNode(node).removeChild(node); + } catch (_) { + remove(node); + } + }; + /** + * _removeAttribute + * + * @param name an Attribute name + * @param element a DOM node + */ + const _removeAttribute = function _removeAttribute(name, element) { + try { + arrayPush(DOMPurify.removed, { + attribute: element.getAttributeNode(name), + from: element + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: element + }); + } + element.removeAttribute(name); + // We void attribute values for unremovable "is" attributes + if (name === 'is') { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(element); + } catch (_) {} + } else { + try { + element.setAttribute(name, ''); + } catch (_) {} + } + } + }; + /** + * _initDocument + * + * @param dirty - a string of dirty markup + * @return a DOM, filled with the dirty markup + */ + const _initDocument = function _initDocument(dirty) { + /* Create a HTML document */ + let doc = null; + let leadingWhitespace = null; + if (FORCE_BODY) { + dirty = '' + dirty; + } else { + /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ + const matches = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches && matches[0]; + } + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { + // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) + dirty = '' + dirty + ''; + } + const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + /* + * Use the DOMParser API by default, fallback later if needs be + * DOMParser not work for svg when has multiple root element. + */ + if (NAMESPACE === HTML_NAMESPACE) { + try { + doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); + } catch (_) {} + } + /* Use createHTMLDocument in case DOMParser is not available */ + if (!doc || !doc.documentElement) { + doc = implementation.createDocument(NAMESPACE, 'template', null); + try { + doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; + } catch (_) { + // Syntax error if dirtyPayload is invalid xml + } + } + const body = doc.body || doc.documentElement; + if (dirty && leadingWhitespace) { + body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); + } + /* Work on whole document or just its body */ + if (NAMESPACE === HTML_NAMESPACE) { + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; + } + return WHOLE_DOCUMENT ? doc.documentElement : body; + }; + /** + * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. + * + * @param root The root element or node to start traversing on. + * @return The created NodeIterator + */ + const _createNodeIterator = function _createNodeIterator(root) { + return createNodeIterator.call(root.ownerDocument || root, root, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); + }; + /** + * _isClobbered + * + * @param element element to check for clobbering attacks + * @return true if clobbered, false if safe + */ + const _isClobbered = function _isClobbered(element) { + return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function'); + }; + /** + * Checks whether the given object is a DOM node. + * + * @param value object to check whether it's a DOM node + * @return true is object is a DOM node + */ + const _isNode = function _isNode(value) { + return typeof Node === 'function' && value instanceof Node; + }; + function _executeHooks(hooks, currentNode, data) { + arrayForEach(hooks, hook => { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + } + /** + * _sanitizeElements + * + * @protect nodeName + * @protect textContent + * @protect removeChild + * @param currentNode to check for permission to exist + * @return true if node was killed, false if left alive + */ + const _sanitizeElements = function _sanitizeElements(currentNode) { + let content = null; + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeElements, currentNode, null); + /* Check if element is clobbered or can clobber */ + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + return true; + } + /* Now let's check the element's type and name */ + const tagName = transformCaseFunc(currentNode.nodeName); + /* Execute a hook if present */ + _executeHooks(hooks.uponSanitizeElement, currentNode, { + tagName, + allowedTags: ALLOWED_TAGS + }); + /* Detect mXSS attempts abusing namespace confusion */ + if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + return true; + } + /* Remove any occurrence of processing instructions */ + if (currentNode.nodeType === NODE_TYPE.progressingInstruction) { + _forceRemove(currentNode); + return true; + } + /* Remove any kind of possibly harmful comments */ + if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) { + _forceRemove(currentNode); + return true; + } + /* Remove element if anything forbids its presence */ + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + /* Check if we have a custom element to handle */ + if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { + return false; + } + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { + return false; + } + } + /* Keep content except for bad-listed elements */ + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { + const parentNode = getParentNode(currentNode) || currentNode.parentNode; + const childNodes = getChildNodes(currentNode) || currentNode.childNodes; + if (childNodes && parentNode) { + const childCount = childNodes.length; + for (let i = childCount - 1; i >= 0; --i) { + const childClone = cloneNode(childNodes[i], true); + childClone.__removalCount = (currentNode.__removalCount || 0) + 1; + parentNode.insertBefore(childClone, getNextSibling(currentNode)); + } + } + } + _forceRemove(currentNode); + return true; + } + /* Check whether element has a valid namespace */ + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode); + return true; + } + /* Make sure that older browsers don't get fallback-tag mXSS */ + if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { + _forceRemove(currentNode); + return true; + } + /* Sanitize element content to be template-safe */ + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { + /* Get the element's text content */ + content = currentNode.textContent; + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + content = stringReplace(content, expr, ' '); + }); + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { + element: currentNode.cloneNode() + }); + currentNode.textContent = content; + } + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeElements, currentNode, null); + return false; + }; + /** + * _isValidAttribute + * + * @param lcTag Lowercase tag name of containing element. + * @param lcName Lowercase attribute name. + * @param value Attribute value. + * @return Returns true if `value` is valid, otherwise false. + */ + // eslint-disable-next-line complexity + const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { + /* Make sure attribute cannot clobber */ + if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { + return false; + } + /* Allow valid data-* attributes: At least one character after "-" + (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) + XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) + We don't need to check the value; it's always URI safe. */ + if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + if ( + // First condition does a very basic check if a) it's basically a valid custom element tagname AND + // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck + _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || + // Alternative, second condition checks if it's an `is`-attribute, AND + // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { + return false; + } + /* Check value is safe. First, is attr inert? If so, is safe */ + } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { + return false; + } else ; + return true; + }; + /** + * _isBasicCustomElement + * checks if at least one dash is included in tagName, and it's not the first char + * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name + * + * @param tagName name of the tag of the node to sanitize + * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. + */ + const _isBasicCustomElement = function _isBasicCustomElement(tagName) { + return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT); + }; + /** + * _sanitizeAttributes + * + * @protect attributes + * @protect nodeName + * @protect removeAttribute + * @protect setAttribute + * + * @param currentNode to sanitize + */ + const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); + const { + attributes + } = currentNode; + /* Check if we have attributes; if not we might have a text node */ + if (!attributes || _isClobbered(currentNode)) { + return; + } + const hookEvent = { + attrName: '', + attrValue: '', + keepAttr: true, + allowedAttributes: ALLOWED_ATTR, + forceKeepAttr: undefined + }; + let l = attributes.length; + /* Go backwards over all attributes; safely remove bad ones */ + while (l--) { + const attr = attributes[l]; + const { + name, + namespaceURI, + value: attrValue + } = attr; + const lcName = transformCaseFunc(name); + let value = name === 'value' ? attrValue : stringTrim(attrValue); + /* Execute a hook if present */ + hookEvent.attrName = lcName; + hookEvent.attrValue = value; + hookEvent.keepAttr = true; + hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set + _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); + value = hookEvent.attrValue; + /* Full DOM Clobbering protection via namespace isolation, + * Prefix id and name attributes with `user-content-` + */ + if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { + // Remove the attribute with this value + _removeAttribute(name, currentNode); + // Prefix the value and later re-create the attribute with the sanitized value + value = SANITIZE_NAMED_PROPS_PREFIX + value; + } + /* Work around a security issue with comments inside attributes */ + if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + /* Did the hooks approve of the attribute? */ + if (hookEvent.forceKeepAttr) { + continue; + } + /* Remove attribute */ + _removeAttribute(name, currentNode); + /* Did the hooks approve of the attribute? */ + if (!hookEvent.keepAttr) { + continue; + } + /* Work around a security issue in jQuery 3.0 */ + if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + /* Sanitize attribute content to be template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + value = stringReplace(value, expr, ' '); + }); + } + /* Is `value` valid for this attribute? */ + const lcTag = transformCaseFunc(currentNode.nodeName); + if (!_isValidAttribute(lcTag, lcName, value)) { + continue; + } + /* Handle attributes that require Trusted Types */ + if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { + if (namespaceURI) ; else { + switch (trustedTypes.getAttributeType(lcTag, lcName)) { + case 'TrustedHTML': + { + value = trustedTypesPolicy.createHTML(value); + break; + } + case 'TrustedScriptURL': + { + value = trustedTypesPolicy.createScriptURL(value); + break; + } + } + } + } + /* Handle invalid data-* attribute set by try-catching it */ + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value); + } else { + /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ + currentNode.setAttribute(name, value); + } + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + } else { + arrayPop(DOMPurify.removed); + } + } catch (_) {} + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); + }; + /** + * _sanitizeShadowDOM + * + * @param fragment to iterate over recursively + */ + const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { + let shadowNode = null; + const shadowIterator = _createNodeIterator(fragment); + /* Execute a hook if present */ + _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); + while (shadowNode = shadowIterator.nextNode()) { + /* Execute a hook if present */ + _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); + /* Sanitize tags and elements */ + _sanitizeElements(shadowNode); + /* Check attributes next */ + _sanitizeAttributes(shadowNode); + /* Deep shadow DOM detected */ + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(shadowNode.content); + } + } + /* Execute a hook if present */ + _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); + }; + // eslint-disable-next-line complexity + DOMPurify.sanitize = function (dirty) { + let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + let body = null; + let importedNode = null; + let currentNode = null; + let returnNode = null; + /* Make sure we have a string to sanitize. + DO NOT return early, as this will return the wrong type if + the user has requested a DOM object rather than a string */ + IS_EMPTY_INPUT = !dirty; + if (IS_EMPTY_INPUT) { + dirty = ''; + } + /* Stringify, in case dirty is an object */ + if (typeof dirty !== 'string' && !_isNode(dirty)) { + if (typeof dirty.toString === 'function') { + dirty = dirty.toString(); + if (typeof dirty !== 'string') { + throw typeErrorCreate('dirty is not a string, aborting'); + } + } else { + throw typeErrorCreate('toString is not a function'); + } + } + /* Return dirty HTML if DOMPurify cannot run */ + if (!DOMPurify.isSupported) { + return dirty; + } + /* Assign config vars */ + if (!SET_CONFIG) { + _parseConfig(cfg); + } + /* Clean up removed elements */ + DOMPurify.removed = []; + /* Check if dirty is correctly typed for IN_PLACE */ + if (typeof dirty === 'string') { + IN_PLACE = false; + } + if (IN_PLACE) { + /* Do some early pre-sanitization to avoid unsafe root nodes */ + if (dirty.nodeName) { + const tagName = transformCaseFunc(dirty.nodeName); + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); + } + } + } else if (dirty instanceof Node) { + /* If dirty is a DOM element, append to an empty document to avoid + elements being stripped by the parser */ + body = _initDocument(''); + importedNode = body.ownerDocument.importNode(dirty, true); + if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { + /* Node is already a body, use as is */ + body = importedNode; + } else if (importedNode.nodeName === 'HTML') { + body = importedNode; + } else { + // eslint-disable-next-line unicorn/prefer-dom-node-append + body.appendChild(importedNode); + } + } else { + /* Exit directly if we have nothing to do */ + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && + // eslint-disable-next-line unicorn/prefer-includes + dirty.indexOf('<') === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; + } + /* Initialize the document to work on */ + body = _initDocument(dirty); + /* Check we have a DOM node from the data */ + if (!body) { + return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; + } + } + /* Remove first element node (ours) if FORCE_BODY is set */ + if (body && FORCE_BODY) { + _forceRemove(body.firstChild); + } + /* Get node iterator */ + const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); + /* Now start iterating over the created document */ + while (currentNode = nodeIterator.nextNode()) { + /* Sanitize tags and elements */ + _sanitizeElements(currentNode); + /* Check attributes next */ + _sanitizeAttributes(currentNode); + /* Shadow DOM detected, sanitize it */ + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); + } + } + /* If we sanitized `dirty` in-place, return it. */ + if (IN_PLACE) { + return dirty; + } + /* Return sanitized string or DOM */ + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument); + while (body.firstChild) { + // eslint-disable-next-line unicorn/prefer-dom-node-append + returnNode.appendChild(body.firstChild); + } + } else { + returnNode = body; + } + if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { + /* + AdoptNode() is not used because internal state is not reset + (e.g. the past names map of a HTMLFormElement), this is safe + in theory but we would rather not risk another attack vector. + The state that is cloned by importNode() is explicitly defined + by the specs. + */ + returnNode = importNode.call(originalDocument, returnNode, true); + } + return returnNode; + } + let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + /* Serialize doctype if allowed */ + if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { + serializedHTML = '\n' + serializedHTML; + } + /* Sanitize final string template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + serializedHTML = stringReplace(serializedHTML, expr, ' '); + }); + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; + }; + DOMPurify.setConfig = function () { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + _parseConfig(cfg); + SET_CONFIG = true; + }; + DOMPurify.clearConfig = function () { + CONFIG = null; + SET_CONFIG = false; + }; + DOMPurify.isValidAttribute = function (tag, attr, value) { + /* Initialize shared config vars if necessary. */ + if (!CONFIG) { + _parseConfig({}); + } + const lcTag = transformCaseFunc(tag); + const lcName = transformCaseFunc(attr); + return _isValidAttribute(lcTag, lcName, value); + }; + DOMPurify.addHook = function (entryPoint, hookFunction) { + if (typeof hookFunction !== 'function') { + return; + } + arrayPush(hooks[entryPoint], hookFunction); + }; + DOMPurify.removeHook = function (entryPoint, hookFunction) { + if (hookFunction !== undefined) { + const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); + return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; + } + return arrayPop(hooks[entryPoint]); + }; + DOMPurify.removeHooks = function (entryPoint) { + hooks[entryPoint] = []; + }; + DOMPurify.removeAllHooks = function () { + hooks = _createHooksMap(); + }; + return DOMPurify; +} +var purify = createDOMPurify(); + +export { purify as default }; +//# sourceMappingURL=purify.es.mjs.map diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md new file mode 100644 index 00000000000..352b52d5503 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/README.md @@ -0,0 +1,5 @@ +marked version 15.0.6 +https://github.com/markedjs/marked +License: MIT + +To update, replace the `dist/marked.esm.js` file with with an updated version from https://www.npmjs.com/package/marked. diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js new file mode 100644 index 00000000000..a32cd778363 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/marked/dist/marked.esm.js @@ -0,0 +1,2568 @@ +/** + * marked v15.0.6 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +/** + * Gets the original marked default options. + */ +function _getDefaults() { + return { + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }; +} +let _defaults = _getDefaults(); +function changeDefaults(newDefaults) { + _defaults = newDefaults; +} + +const noopTest = { exec: () => null }; +function edit(regex, opt = '') { + let source = typeof regex === 'string' ? regex : regex.source; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(other.caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; +} +const other = { + codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, + outputLinkReplace: /\\([\[\]])/g, + indentCodeCompensation: /^(\s+)(?:```)/, + beginningSpace: /^\s+/, + endingHash: /#$/, + startingSpaceChar: /^ /, + endingSpaceChar: / $/, + nonSpaceChar: /[^ ]/, + newLineCharGlobal: /\n/g, + tabCharGlobal: /\t/g, + multipleSpaceGlobal: /\s+/g, + blankLine: /^[ \t]*$/, + doubleBlankLine: /\n[ \t]*\n[ \t]*$/, + blockquoteStart: /^ {0,3}>/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceTabs: /^\t+/, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] /, + listReplaceTask: /^\[[ xX]\] +/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^/i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`), + hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), + fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`), + headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`), + htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'), +}; +/** + * Block-Level Grammar + */ +const newline = /^(?:[ \t]*(?:\n|$))+/; +const blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; +const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; +const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; +const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; +const bullet = /(?:[*+-]|\d{1,9}[.)])/; +const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); +const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; +const blockText = /^[^\n]+/; +const _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; +const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); +const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); +const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; +const _comment = /|$))/; +const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); +const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); +const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); +/** + * Normal Block Grammar + */ +const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, +}; +/** + * GFM Block Grammar + */ +const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', '(?: {4}| {0,3}\t)[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); +const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), +}; +/** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ +const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), +}; +/** + * Inline-Level Grammar + */ +const escape$1 = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; +const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; +const br = /^( {2,}|\\)\n(?!\s*$)/; +const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ +const blockSkip = /\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g; +const emStrongLDelimCore = /^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/; +const emStrongLDelim = edit(emStrongLDelimCore, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongLDelimGfm = edit(emStrongLDelimCore, 'u') + .replace(/punct/g, _punctuationGfmStrongEm) + .getRegex(); +const emStrongRDelimAstCore = '^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + + '|notPunctSpace(\\*+)(?=notPunctSpace)'; // (6) a***a can be either Left or Right Delimiter +const emStrongRDelimAst = edit(emStrongRDelimAstCore, 'gu') + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongRDelimAstGfm = edit(emStrongRDelimAstCore, 'gu') + .replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmStrongEm) + .replace(/punctSpace/g, _punctuationOrSpaceGfmStrongEm) + .replace(/punct/g, _punctuationGfmStrongEm) + .getRegex(); +// (6) Not allowed for _ +const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + + '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const anyPunctuation = edit(/\\(punct)/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); +const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); +const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); +const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); +const _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); +const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); +const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); +const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); +/** + * Normal Inline Grammar + */ +const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape: escape$1, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, +}; +/** + * Pedantic Inline Grammar + */ +const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), +}; +/** + * GFM Inline Grammar + */ +const inlineGfm = { + ...inlineNormal, + emStrongRDelimAst: emStrongRDelimAstGfm, + emStrongLDelim: emStrongLDelimGfm, + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\': '>', + '"': '"', + "'": ''', +}; +const getEscapeReplacement = (ch) => escapeReplacements[ch]; +function escape(html, encode) { + if (encode) { + if (other.escapeTest.test(html)) { + return html.replace(other.escapeReplace, getEscapeReplacement); + } + } + else { + if (other.escapeTestNoEncode.test(html)) { + return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; +} +function cleanUrl(href) { + try { + href = encodeURI(href).replace(other.percentDecode, '%'); + } + catch { + return null; + } + return href; +} +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(other.findPipe, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(other.splitPipe); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells.at(-1)?.trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(other.slashPipe, '|'); + } + return cells; +} +/** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ +function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && true) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); +} +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +function outputLink(cap, link, raw, lexer, rules) { + const href = link.href; + const title = link.title || null; + const text = cap[1].replace(rules.other.outputLinkReplace, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text, + }; +} +function indentCodeCompensation(raw, text, rules) { + const matchIndentToCode = raw.match(rules.other.indentCodeCompensation); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(rules.other.beginningSpace); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); +} +/** + * Tokenizer + */ +class _Tokenizer { + options; + rules; // set by the lexer + lexer; // set by the lexer + constructor(options) { + this.options = options || _defaults; + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(this.rules.other.codeRemoveIndent, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || '', this.rules); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (this.rules.other.endingHash.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (this.rules.other.blockquoteStart.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(this.rules.other.blockquoteSetextReplace, '\n $1') + .replace(this.rules.other.blockquoteSetextReplace2, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens.at(-1); + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens.at(-1).raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = this.rules.other.listItemRegex(bull); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(this.rules.other.listReplaceTabs, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(this.rules.other.nonSpaceChar); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && this.rules.other.blankLine.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = this.rules.other.nextBulletRegex(indent); + const hrRegex = this.rules.other.hrRegex(indent); + const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent); + const headingBeginRegex = this.rules.other.headingBeginRegex(indent); + const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + let nextLineWithoutTabs; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(this.rules.other.listReplaceNesting, ' '); + nextLineWithoutTabs = nextLine; + } + else { + nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of html block + if (htmlBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(nextLine)) { + break; + } + if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLineWithoutTabs.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.replace(this.rules.other.tabCharGlobal, ' ').search(this.rules.other.nonSpaceChar) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLineWithoutTabs.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (this.rules.other.doubleBlankLine.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = this.rules.other.listIsTask.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(this.rules.other.listReplaceTask, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + const lastItem = list.items.at(-1); + if (lastItem) { + lastItem.raw = lastItem.raw.trimEnd(); + lastItem.text = lastItem.text.trimEnd(); + } + else { + // not a list since there were no items + return; + } + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, ' '); + const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!this.rules.other.tableDelimiter.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(this.rules.other.tableAlignChars, '').split('|'); + const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (this.rules.other.tableAlignRight.test(align)) { + item.align.push('right'); + } + else if (this.rules.other.tableAlignCenter.test(align)) { + item.align.push('center'); + } + else if (this.rules.other.tableAlignLeft.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: cap[1], + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) { + this.lexer.state.inLink = true; + } + else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) { + // commonmark requires matching angle brackets + if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = this.rules.other.pedanticHrefTitle.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (this.rules.other.startAngleBracket.test(href)) { + if (this.options.pedantic && !(this.rules.other.endAngleBracket.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer, this.rules); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer, this.rules); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(this.rules.other.newLineCharGlobal, ' '); + const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text); + const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = cap[1]; + href = 'mailto:' + text; + } + else { + text = cap[1]; + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = cap[0]; + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = cap[0]; + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + const escaped = this.lexer.state.inRawBlock; + return { + type: 'text', + raw: cap[0], + text: cap[0], + escaped, + }; + } + } +} + +/** + * Block Lexer + */ +class _Lexer { + tokens; + options; + state; + tokenizer; + inlineQueue; + constructor(options) { + // TokenList cannot be created in one go + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || _defaults; + this.options.tokenizer = this.options.tokenizer || new _Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true, + }; + const rules = { + other, + block: block.normal, + inline: inline.normal, + }; + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } + else if (this.options.gfm) { + rules.block = block.gfm; + if (this.options.breaks) { + rules.inline = inline.breaks; + } + else { + rules.inline = inline.gfm; + } + } + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + static get rules() { + return { + block, + inline, + }; + } + /** + * Static Lex Method + */ + static lex(src, options) { + const lexer = new _Lexer(options); + return lexer.lex(src); + } + /** + * Static Lex Inline Method + */ + static lexInline(src, options) { + const lexer = new _Lexer(options); + return lexer.inlineTokens(src); + } + /** + * Preprocessing + */ + lex(src) { + src = src.replace(other.carriageReturn, '\n'); + this.blockTokens(src, this.tokens); + for (let i = 0; i < this.inlineQueue.length; i++) { + const next = this.inlineQueue[i]; + this.inlineTokens(next.src, next.tokens); + } + this.inlineQueue = []; + return this.tokens; + } + blockTokens(src, tokens = [], lastParagraphClipped = false) { + if (this.options.pedantic) { + src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''); + } + while (src) { + let token; + if (this.options.extensions?.block?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.raw.length === 1 && lastToken !== undefined) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + lastToken.raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + // An indented code block cannot interrupt a paragraph. + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue.at(-1).src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + const lastToken = tokens.at(-1); + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = cutSrc.length !== src.length; + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match = null; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + + '[' + 'a'.repeat(match[0].length - 2) + ']' + + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + let keepPrevChar = false; + let prevChar = ''; + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + let token; + // extensions + if (this.options.extensions?.inline?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.type === 'text' && lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } +} + +/** + * Renderer + */ +class _Renderer { + options; + parser; // set by the parser + constructor(options) { + this.options = options || _defaults; + } + space(token) { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(other.notSpaceStart)?.[0]; + const code = text.replace(other.endingNewline, '') + '\n'; + if (!langString) { + return '

    '
    +                + (escaped ? code : escape(code, true))
    +                + '
    \n'; + } + return '
    '
    +            + (escaped ? code : escape(code, true))
    +            + '
    \n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
    \n${body}
    \n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr(token) { + return '
    \n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens[0]?.type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + escape(item.tokens[0].tokens[0].text); + item.tokens[0].tokens[0].escaped = true; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + escaped: true, + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${escape(text, true)}`; + } + br(token) { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return escape(text); + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (['options', 'block'].includes(prop)) { + // ignore options and block properties + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(blockType) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + opt.hooks.block = blockType; + } + const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +                    + escape(e.message + '', true)
    +                    + '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } +} + +const markedInstance = new Marked(); +function marked(src, opt) { + return markedInstance.parse(src, opt); +} +/** + * Sets the default options. + * + * @param options Hash of options + */ +marked.options = + marked.setOptions = function (options) { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; +/** + * Gets the original marked default options. + */ +marked.getDefaults = _getDefaults; +marked.defaults = _defaults; +/** + * Use Extension + */ +marked.use = function (...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; +/** + * Run callback for every token + */ +marked.walkTokens = function (tokens, callback) { + return markedInstance.walkTokens(tokens, callback); +}; +/** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ +marked.parseInline = markedInstance.parseInline; +/** + * Expose + */ +marked.Parser = _Parser; +marked.parser = _Parser.parse; +marked.Renderer = _Renderer; +marked.TextRenderer = _TextRenderer; +marked.Lexer = _Lexer; +marked.lexer = _Lexer.lex; +marked.Tokenizer = _Tokenizer; +marked.Hooks = _Hooks; +marked.parse = marked; +const options = marked.options; +const setOptions = marked.setOptions; +const use = marked.use; +const walkTokens = marked.walkTokens; +const parseInline = marked.parseInline; +const parse = marked; +const parser = _Parser.parse; +const lexer = _Lexer.lex; + +export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; +//# sourceMappingURL=marked.esm.js.map diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html new file mode 100644 index 00000000000..32ac36286a7 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.html @@ -0,0 +1,31 @@ + + + + + + PDF viewer + + + + + + +
    +
    +
    + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs new file mode 100644 index 00000000000..8a4a6b76f5e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdf_viewer/viewer.mjs @@ -0,0 +1,62 @@ +import { GlobalWorkerOptions } from '../pdfjs-dist/dist/build/pdf.min.mjs'; +import { EventBus, PDFLinkService, PDFFindController, PDFViewer } from '../pdfjs-dist/dist/web/pdf_viewer.mjs'; + +GlobalWorkerOptions.workerSrc = '../pdfjs-dist/dist/build/pdf.worker.min.mjs'; + +// Extract the file path from the URL query string. +const url = new URL(window.location); +const fileUrl = url.searchParams.get('file'); +if (!fileUrl) { + throw new Error('File not specified in the URL query string'); +} + +const container = document.getElementById('viewerContainer'); +const eventBus = new EventBus(); + +// Enable hyperlinks within PDF files. +const pdfLinkService = new PDFLinkService({ + eventBus, +}); + +// Enable the find controller. +const pdfFindController = new PDFFindController({ + eventBus, + linkService: pdfLinkService, +}); + +// Create the PDF viewer. +const pdfViewer = new PDFViewer({ + container, + eventBus, + linkService: pdfLinkService, + findController: pdfFindController, +}); +pdfLinkService.setViewer(pdfViewer); + +// Allow navigation to a citation from the URL hash. +eventBus.on('pagesinit', function () { + pdfLinkService.setHash(window.location.hash.substring(1)); +}); + +// Define how the "search" query parameter is handled. +eventBus.on('findfromurlhash', function(evt) { + eventBus.dispatch('find', { + source: evt.source, + type: '', + query: evt.query, + caseSensitive: false, + entireWord: false, + highlightAll: false, + findPrevious: false, + matchDiacritics: true, + }); +}); + +// Load and initialize the document. +const pdfDocument = await pdfjsLib.getDocument({ + url: fileUrl, + enableXfa: true, +}).promise; + +pdfViewer.setDocument(pdfDocument); +pdfLinkService.setDocument(pdfDocument, null); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md new file mode 100644 index 00000000000..8e77fba7d43 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/README.md @@ -0,0 +1,10 @@ +pdfjs-dist version 4.10.38 +https://github.com/mozilla/pdf.js +License: Apache-2.0 + +To update, replace the following files with updated versions from https://www.npmjs.com/package/pdfjs-dist: +* `build/pdf.min.mjs` +* `build/pdf.worker.min.mjs` +* `web/pdf_viewer.css` +* `web/pdf_viewer.mjs` +* `web/images/loading-icon.gif` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs new file mode 100644 index 00000000000..d7cfa914562 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.min.mjs @@ -0,0 +1,21 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},__webpack_exports__ = globalThis.pdfjsLib = {};t.d(__webpack_exports__,{AbortException:()=>AbortException,AnnotationEditorLayer:()=>AnnotationEditorLayer,AnnotationEditorParamsType:()=>m,AnnotationEditorType:()=>g,AnnotationEditorUIManager:()=>AnnotationEditorUIManager,AnnotationLayer:()=>AnnotationLayer,AnnotationMode:()=>p,ColorPicker:()=>ColorPicker,DOMSVGFactory:()=>DOMSVGFactory,DrawLayer:()=>DrawLayer,FeatureTest:()=>util_FeatureTest,GlobalWorkerOptions:()=>GlobalWorkerOptions,ImageKind:()=>_,InvalidPDFException:()=>InvalidPDFException,MissingPDFException:()=>MissingPDFException,OPS:()=>X,OutputScale:()=>OutputScale,PDFDataRangeTransport:()=>PDFDataRangeTransport,PDFDateString:()=>PDFDateString,PDFWorker:()=>PDFWorker,PasswordResponses:()=>K,PermissionFlag:()=>f,PixelsPerInch:()=>PixelsPerInch,RenderingCancelledException:()=>RenderingCancelledException,TextLayer:()=>TextLayer,TouchManager:()=>TouchManager,UnexpectedResponseException:()=>UnexpectedResponseException,Util:()=>Util,VerbosityLevel:()=>q,XfaLayer:()=>XfaLayer,build:()=>Nt,createValidAbsoluteUrl:()=>createValidAbsoluteUrl,fetchData:()=>fetchData,getDocument:()=>getDocument,getFilenameFromUrl:()=>getFilenameFromUrl,getPdfFilenameFromUrl:()=>getPdfFilenameFromUrl,getXfaPageViewport:()=>getXfaPageViewport,isDataScheme:()=>isDataScheme,isPdfFile:()=>isPdfFile,noContextMenu:()=>noContextMenu,normalizeUnicode:()=>normalizeUnicode,setLayerDimensions:()=>setLayerDimensions,shadow:()=>shadow,stopEvent:()=>stopEvent,version:()=>Ot});const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),i=[1,0,0,1,0,0],s=[.001,0,0,.001,0,0],n=1.35,a=1,r=2,o=4,l=16,h=32,d=64,c=128,u=256,p={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},g={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15},m={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,HIGHLIGHT_COLOR:31,HIGHLIGHT_DEFAULT_COLOR:32,HIGHLIGHT_THICKNESS:33,HIGHLIGHT_FREE:34,HIGHLIGHT_SHOW_ALL:35,DRAW_STEP:41},f={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},b=0,A=1,w=2,v=3,y=3,x=4,_={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},E=1,S=2,C=3,T=4,M=5,P=6,D=7,k=8,R=9,I=10,F=11,L=12,O=13,N=14,B=15,H=16,z=17,U=20,G=1,$=2,V=3,j=4,W=5,q={ERRORS:0,WARNINGS:1,INFOS:5},X={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93},K={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let Y=q.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(Y=t)}function getVerbosityLevel(){return Y}function info(t){Y>=q.INFOS&&console.log(`Info: ${t}`)}function warn(t){Y>=q.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;try{if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s))return s}catch{}return null}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const Q=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends Q{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends Q{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends Q{constructor(t){super(t,"InvalidPDFException")}}class MissingPDFException extends Q{constructor(t){super(t,"MissingPDFException")}}class UnexpectedResponseException extends Q{constructor(t,e){super(t,"UnexpectedResponseException");this.status=e}}class FormatError extends Q{constructor(t){super(t,"FormatError")}}class AbortException extends Q{constructor(t){super(t,"AbortException")}}function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,i){return`#${J[t]}${J[e]}${J[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e){return[t[0]*e[0]+t[1]*e[2]+e[4],t[0]*e[1]+t[1]*e[3]+e[5]]}static applyInverseTransform(t,e){const i=e[0]*e[3]-e[1]*e[2];return[(t[0]*e[3]-t[1]*e[2]+e[2]*e[5]-e[4]*e[3])/i,(-t[0]*e[1]+t[1]*e[0]+e[4]*e[1]-e[5]*e[0])/i]}static getAxialAlignedBoundingBox(t,e){const i=this.applyTransform(t,e),s=this.applyTransform(t.slice(2,4),e),n=this.applyTransform([t[0],t[3]],e),a=this.applyTransform([t[2],t[1]],e);return[Math.min(i[0],s[0],n[0],a[0]),Math.min(i[1],s[1],n[1],a[1]),Math.max(i[0],s[0],n[0],a[0]),Math.max(i[1],s[1],n[1],a[1])]}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t){const e=[t[0],t[2],t[1],t[3]],i=t[0]*e[0]+t[1]*e[2],s=t[0]*e[1]+t[1]*e[3],n=t[2]*e[0]+t[3]*e[2],a=t[2]*e[1]+t[3]*e[3],r=(i+a)/2,o=Math.sqrt((i+a)**2-4*(i*a-n*s))/2,l=r+o||1,h=r-o||1;return[Math.sqrt(l),Math.sqrt(h)]}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static#t(t,e,i,s,n,a,r,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*a)+3*c*r)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,a,r,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,a,r,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,a,r,o,(-h+p)/g,c);this.#t(t,e,i,s,n,a,r,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,a,r,o,l){if(l){l[0]=Math.min(l[0],t,r);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,r);l[3]=Math.max(l[3],e,o)}else l=[Math.min(t,r),Math.min(e,o),Math.max(t,r),Math.max(e,o)];this.#e(t,i,n,r,e,s,a,o,3*(3*(i-n)-t+r),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,r,e,s,a,o,3*(3*(s-a)-e+o),6*(e-2*s+a),3*(s-e),l);return l}}let Z=null,tt=null;function normalizeUnicode(t){if(!Z){Z=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;tt=new Map([["ſt","ſt"]])}return t.replaceAll(Z,((t,e,i)=>e?e.normalize("NFKC"):tt.get(i)))}const et="pdfjs_internal_id_";"function"!=typeof Promise.try&&(Promise.try=function(t,...e){return new Promise((i=>{i(t(...e))}))});const it="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"arraybuffer":return i.arrayBuffer();case"blob":return i.blob();case"json":return i.json()}return i.text()}return new Promise(((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType=e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"arraybuffer":case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)}))}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:a=0,dontFlip:r=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=a;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,m,f;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(r){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+a;m=(t[3]-t[1])*i;f=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+a;m=(t[2]-t[0])*i;f=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=m;this.height=f}get rawDims(){const{userUnit:t,viewBox:e}=this,i=e.map((e=>e*t));return shadow(this,"rawDims",{pageWidth:i[2]-i[0],pageHeight:i[3]-i[1],pageX:i[0],pageY:i[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){return Util.applyTransform([t,e],this.transform)}convertToViewportRectangle(t){const e=Util.applyTransform([t[0],t[1]],this.transform),i=Util.applyTransform([t[2],t[3]],this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){return Util.applyInverseTransform([t,e],this.transform)}}class RenderingCancelledException extends Q{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let a=parseInt(e[4],10);a=a>=0&&a<=23?a:0;let r=parseInt(e[5],10);r=r>=0&&r<=59?r:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){a+=h;r+=d}else if("+"===l){a-=h;r-=d}return new Date(Date.UTC(i,s,n,a,r,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,a=[0,0,parseInt(s),parseInt(n)];return new PageViewport({viewBox:a,userUnit:1,scale:e,rotation:i})}function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);warn(`Not a valid color format: "${t}"`);return[0,0,0]}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform();return[e,i,s,n,a,r]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform().invertSelf();return[e,i,s,n,a,r]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:a}=t,r=util_FeatureTest.isCSSRoundSupported,o=`var(--scale-factor) * ${s}px`,l=`var(--scale-factor) * ${n}px`,h=r?`round(down, ${o}, var(--scale-round-x, 1px))`:`calc(${o})`,d=r?`round(down, ${l}, var(--scale-round-y, 1px))`:`calc(${l})`;if(i&&e.rotation%180!=0){a.width=d;a.height=h}else{a.width=h;a.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const t=window.devicePixelRatio||1;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}}class EditorToolbar{#s=null;#n=null;#a;#r=null;#o=null;static#l=null;constructor(t){this.#a=t;EditorToolbar.#l||=Object.freeze({freetext:"pdfjs-editor-remove-freetext-button",highlight:"pdfjs-editor-remove-highlight-button",ink:"pdfjs-editor-remove-ink-button",stamp:"pdfjs-editor-remove-stamp-button"})}render(){const t=this.#s=document.createElement("div");t.classList.add("editToolbar","hidden");t.setAttribute("role","toolbar");const e=this.#a._uiManager._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.addEventListener("pointerdown",EditorToolbar.#h,{signal:e});const i=this.#r=document.createElement("div");i.className="buttons";t.append(i);const s=this.#a.toolbarPosition;if(s){const{style:e}=t,i="ltr"===this.#a._uiManager.direction?1-s[0]:s[0];e.insetInlineEnd=100*i+"%";e.top=`calc(${100*s[1]}% + var(--editor-toolbar-vert-offset))`}this.#d();return t}get div(){return this.#s}static#h(t){t.stopPropagation()}#c(t){this.#a._focusEventsAllowed=!1;stopEvent(t)}#u(t){this.#a._focusEventsAllowed=!0;stopEvent(t)}#p(t){const e=this.#a._uiManager._signal;t.addEventListener("focusin",this.#c.bind(this),{capture:!0,signal:e});t.addEventListener("focusout",this.#u.bind(this),{capture:!0,signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e})}hide(){this.#s.classList.add("hidden");this.#n?.hideDropdown()}show(){this.#s.classList.remove("hidden");this.#o?.shown()}#d(){const{editorType:t,_uiManager:e}=this.#a,i=document.createElement("button");i.className="delete";i.tabIndex=0;i.setAttribute("data-l10n-id",EditorToolbar.#l[t]);this.#p(i);i.addEventListener("click",(t=>{e.delete()}),{signal:e._signal});this.#r.append(i)}get#g(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#p(e);this.#r.prepend(e,this.#g);this.#o=t}addColorPicker(t){this.#n=t;const e=t.renderButton();this.#p(e);this.#r.prepend(e,this.#g)}remove(){this.#s.remove();this.#n?.destroy();this.#n=null}}class HighlightToolbar{#r=null;#s=null;#m;constructor(t){this.#m=t}#f(){const t=this.#s=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");t.addEventListener("contextmenu",noContextMenu,{signal:this.#m._signal});const e=this.#r=document.createElement("div");e.className="buttons";t.append(e);this.#b();return t}#A(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=a;i=t}else e?a>s&&(s=a):a{this.#m.highlightSelection("floating_button")}),{signal:i});this.#r.append(t)}}function bindEvents(t,e,i){for(const s of i)e.addEventListener(s,t[s].bind(t))}class IdManager{#w=0;get id(){return"pdfjs_internal_editor_"+this.#w++}}class ImageManager{#v=function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return bytesToString(t)}();#w=0;#y=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then((()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]})))}async#x(t,e){this.#y||=new Map;let i=this.#y.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#v}_${this.#w++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,a=new Promise(((t,a)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=a}));s.readAsDataURL(t);await a}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#y.set(t,i);i&&this.#y.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#x(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#x(t,t)}async getFromBlob(t,e){const i=await e;return this.#x(t,i)}async getFromId(t){this.#y||=new Map;const e=this.#y.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#y||=new Map;let i=this.#y.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#v}_${this.#w++}`,refCounter:1,isSvg:!1};this.#y.set(t,i);this.#y.set(i.id,i);return i}getSvgUrl(t){const e=this.#y.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#y||=new Map;const e=this.#y.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#v}_`)}}class CommandManager{#_=[];#E=!1;#S;#C=-1;constructor(t=128){this.#S=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:a=!1,keepUndo:r=!1}){s&&t();if(this.#E)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#C){this.#_.length>0&&(this.#_.length=0);this.#C=0;this.#_.push(o);return}if(a&&this.#_[this.#C].type===n){r&&(o.undo=this.#_[this.#C].undo);this.#_[this.#C]=o;return}const l=this.#C+1;if(l===this.#S)this.#_.splice(0,1);else{this.#C=l;l=0;e--)if(this.#_[e].type!==t){this.#_.splice(e+1,this.#C-e);this.#C=e;return}this.#_.length=0;this.#C=-1}}destroy(){this.#_=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=util_FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#T(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#T(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:a=[],checker:r=null}}=i;if(!r||r(t,e)){s.bind(t,...a,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every(((t,i)=>t===e[i])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#M=new AbortController;#P=null;#D=new Map;#k=new Map;#R=null;#I=null;#F=null;#L=new CommandManager;#O=null;#N=null;#B=0;#H=new Set;#z=null;#U=null;#G=new Set;_editorUndoBar=null;#$=!1;#V=!1;#j=!1;#W=null;#q=null;#X=null;#K=null;#Y=!1;#Q=null;#J=new IdManager;#Z=!1;#tt=!1;#et=null;#it=null;#st=null;#nt=null;#at=g.NONE;#rt=new Set;#ot=null;#lt=null;#ht=null;#dt={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#ct=[0,0];#ut=null;#pt=null;#gt=null;#mt=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#pt.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#pt.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#pt.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,a,r,o,l,h,d,c,u){const p=this._signal=this.#M.signal;this.#pt=t;this.#gt=e;this.#R=i;this._eventBus=s;s._on("editingaction",this.onEditingAction.bind(this),{signal:p});s._on("pagechanging",this.onPageChanging.bind(this),{signal:p});s._on("scalechanging",this.onScaleChanging.bind(this),{signal:p});s._on("rotationchanging",this.onRotationChanging.bind(this),{signal:p});s._on("setpreference",this.onSetPreference.bind(this),{signal:p});s._on("switchannotationeditorparams",(t=>this.updateParams(t.type,t.value)),{signal:p});this.#ft();this.#bt();this.#At();this.#I=n.annotationStorage;this.#W=n.filterFactory;this.#lt=a;this.#K=r||null;this.#$=o;this.#V=l;this.#j=h;this.#nt=d||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=c||null;this._supportsPinchToZoom=!1!==u}destroy(){this.#mt?.resolve();this.#mt=null;this.#M?.abort();this.#M=null;this._signal=null;for(const t of this.#k.values())t.destroy();this.#k.clear();this.#D.clear();this.#G.clear();this.#P=null;this.#rt.clear();this.#L.destroy();this.#R?.destroy();this.#Q?.hide();this.#Q=null;if(this.#q){clearTimeout(this.#q);this.#q=null}if(this.#ut){clearTimeout(this.#ut);this.#ut=null}this._editorUndoBar?.destroy()}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#nt}get useNewAltTextFlow(){return this.#V}get useNewAltTextWhenAddingImage(){return this.#j}get hcmFilter(){return shadow(this,"hcmFilter",this.#lt?this.#W.addHCMFilter(this.#lt.foreground,this.#lt.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#pt).direction)}get highlightColors(){return shadow(this,"highlightColors",this.#K?new Map(this.#K.split(",").map((t=>t.split("=").map((t=>t.trim()))))):null)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,(t=>t.reverse()))):null)}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#N=t}setMainHighlightColorPicker(t){this.#st=t}editAltText(t,e=!1){this.#R?.editAltText(this,t,e)}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#j=e}onPageChanging({pageNumber:t}){this.#B=t-1}focusMainContainer(){this.#pt.focus()}findParent(t,e){for(const i of this.#k.values()){const{x:s,y:n,width:a,height:r}=i.div.getBoundingClientRect();if(t>=s&&t<=s+a&&e>=n&&e<=n+r)return i}return null}disableUserSelect(t=!1){this.#gt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#G.add(t)}removeShouldRescale(t){this.#G.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#G)t.onScaleChanging();this.#N?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#wt({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#vt(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#k.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t=""){const e=document.getSelection();if(!e||e.isCollapsed)return;const{anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a}=e,r=e.toString(),o=this.#wt(e).closest(".textLayer"),l=this.getSelectionBoxes(o);if(!l)return;e.empty();const h=this.#vt(o),d=this.#at===g.NONE,callback=()=>{h?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:l,anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a,text:r});d&&this.showAllEditors("highlight",!0,!0)};d?this.switchToMode(g.HIGHLIGHT,callback):callback()}#yt(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#wt(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#Q||=new HighlightToolbar(this);this.#Q.show(e,i,"ltr"===this.direction)}}addToAnnotationStorage(t){t.isEmpty()||!this.#I||this.#I.has(t.id)||this.#I.setValue(t.id,t)}#xt(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#ot){this.#Q?.hide();this.#ot=null;this.#_t({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#ot)return;const i=this.#wt(t).closest(".textLayer");if(i){this.#Q?.hide();this.#ot=e;this.#_t({hasSelectedText:!0});if(this.#at===g.HIGHLIGHT||this.#at===g.NONE){this.#at===g.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#Y=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#at===g.HIGHLIGHT?this.#vt(i):null;t?.toggleDrawing();const e=new AbortController,s=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Et("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:s});window.addEventListener("blur",pointerup,{signal:s})}}}else if(this.#ot){this.#Q?.hide();this.#ot=null;this.#_t({hasSelectedText:!1})}}#Et(t=""){this.#at===g.HIGHLIGHT?this.highlightSelection(t):this.#$&&this.#yt()}#ft(){document.addEventListener("selectionchange",this.#xt.bind(this),{signal:this._signal})}#St(){if(this.#X)return;this.#X=new AbortController;const t=this.combinedSignal(this.#X);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Ct(){this.#X?.abort();this.#X=null}blur(){this.isShiftKeyDown=!1;if(this.#Y){this.#Y=!1;this.#Et("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#rt)if(e.div.contains(t)){this.#it=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#it)return;const[t,e]=this.#it;this.#it=null;e.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this._signal});e.focus()}#At(){if(this.#et)return;this.#et=new AbortController;const t=this.combinedSignal(this.#et);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Tt(){this.#et?.abort();this.#et=null}#Mt(){if(this.#O)return;this.#O=new AbortController;const t=this.combinedSignal(this.#O);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#Pt(){this.#O?.abort();this.#O=null}#bt(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#At();this.#Mt()}removeEditListeners(){this.#Tt();this.#Pt()}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#U)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#U)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#P?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#rt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#U)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#Dt(e);this.#kt(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#at===g.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#Y){this.#Y=!1;this.#Et("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu")}}#_t(t){if(Object.entries(t).some((([t,e])=>this.#dt[t]!==e))){this._eventBus.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#dt,t)});this.#at===g.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#Rt([[m.HIGHLIGHT_FREE,!0]])}}#Rt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#St();this.#Mt();this.#_t({isEditing:this.#at!==g.NONE,isEmpty:this.#It(),hasSomethingToUndo:this.#L.hasSomethingToUndo(),hasSomethingToRedo:this.#L.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Ct();this.#Pt();this.#_t({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#U){this.#U=t;for(const t of this.#U)this.#Rt(t.defaultPropertiesToUpdate)}}getId(){return this.#J.id}get currentLayer(){return this.#k.get(this.#B)}getLayer(t){return this.#k.get(t)}get currentPageIndex(){return this.#B}addLayer(t){this.#k.set(t.pageIndex,t);this.#Z?t.enable():t.disable()}removeLayer(t){this.#k.delete(t.pageIndex)}async updateMode(t,e=null,i=!1){if(this.#at!==t){if(this.#mt){await this.#mt.promise;if(!this.#mt)return}this.#mt=Promise.withResolvers();this.#at=t;if(t!==g.NONE){this.setEditingState(!0);await this.#Ft();this.unselectAll();for(const e of this.#k.values())e.updateMode(t);if(e){for(const t of this.#D.values())if(t.annotationElementId===e){this.setSelected(t);t.enterInEditMode()}else t.unselect();this.#mt.resolve()}else{i&&this.addNewEditorFromKeyboard();this.#mt.resolve()}}else{this.setEditingState(!1);this.#Lt();this._editorUndoBar?.hide();this.#mt.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t!==this.#at&&this._eventBus.dispatch("switchannotationeditormode",{source:this,mode:t})}updateParams(t,e){if(this.#U){switch(t){case m.CREATE:this.currentLayer.addNewEditor();return;case m.HIGHLIGHT_DEFAULT_COLOR:this.#st?.updateColor(e);break;case m.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#ht||=new Map).set(t,e);this.showAllEditors("highlight",e)}for(const i of this.#rt)i.updateParams(t,e);for(const i of this.#U)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#D.values())i.editorType===t&&i.show(e);(this.#ht?.get(m.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#Rt([[m.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#tt!==t){this.#tt=t;for(const e of this.#k.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Ft(){if(!this.#Z){this.#Z=!0;const t=[];for(const e of this.#k.values())t.push(e.enable());await Promise.all(t);for(const t of this.#D.values())t.enable()}}#Lt(){this.unselectAll();if(this.#Z){this.#Z=!1;for(const t of this.#k.values())t.disable();for(const t of this.#D.values())t.disable()}}getEditors(t){const e=[];for(const i of this.#D.values())i.pageIndex===t&&e.push(i);return e}getEditor(t){return this.#D.get(t)}addEditor(t){this.#D.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#q&&clearTimeout(this.#q);this.#q=setTimeout((()=>{this.focusMainContainer();this.#q=null}),0)}this.#D.delete(t.id);this.unselect(t);t.annotationElementId&&this.#H.has(t.annotationElementId)||this.#I?.remove(t.id)}addDeletedAnnotationElement(t){this.#H.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#H.has(t)}removeDeletedAnnotationElement(t){this.#H.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#Dt(t){const e=this.#k.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#P!==t){this.#P=t;t&&this.#Rt(t.propertiesToUpdate)}}get#Ot(){let t=null;for(t of this.#rt);return t}updateUI(t){this.#Ot===t&&this.#Rt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#Rt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#rt.has(t)){this.#rt.delete(t);t.unselect();this.#_t({hasSelectedEditor:this.hasSelection})}else{this.#rt.add(t);t.select();this.#Rt(t.propertiesToUpdate);this.#_t({hasSelectedEditor:!0})}}setSelected(t){this.#N?.commitOrRemove();for(const e of this.#rt)e!==t&&e.unselect();this.#rt.clear();this.#rt.add(t);t.select();this.#Rt(t.propertiesToUpdate);this.#_t({hasSelectedEditor:!0})}isSelected(t){return this.#rt.has(t)}get firstSelectedEditor(){return this.#rt.values().next().value}unselect(t){t.unselect();this.#rt.delete(t);this.#_t({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#rt.size}get isEnterHandled(){return 1===this.#rt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#L.undo();this.#_t({hasSomethingToUndo:this.#L.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#It()});this._editorUndoBar?.hide()}redo(){this.#L.redo();this.#_t({hasSomethingToUndo:!0,hasSomethingToRedo:this.#L.hasSomethingToRedo(),isEmpty:this.#It()})}addCommands(t){this.#L.add(t);this.#_t({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#It()})}cleanUndoStack(t){this.#L.cleanType(t)}#It(){if(0===this.#D.size)return!0;if(1===this.#D.size)for(const t of this.#D.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#rt],undo=()=>{for(const t of e)this.#Dt(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#P?.commitOrRemove()}hasSomethingToControl(){return this.#P||this.hasSelection}#kt(t){for(const t of this.#rt)t.unselect();this.#rt.clear();for(const e of t)if(!e.isEmpty()){this.#rt.add(e);e.select()}this.#_t({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#rt)t.commit();this.#kt(this.#D.values())}unselectAll(){if(this.#P){this.#P.commitOrRemove();if(this.#at!==g.NONE)return}if(!this.#N?.commitOrRemove()&&this.hasSelection){for(const t of this.#rt)t.unselect();this.#rt.clear();this.#_t({hasSelectedEditor:!1})}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#ct[0]+=t;this.#ct[1]+=e;const[s,n]=this.#ct,a=[...this.#rt];this.#ut&&clearTimeout(this.#ut);this.#ut=setTimeout((()=>{this.#ut=null;this.#ct[0]=this.#ct[1]=0;this.addCommands({cmd:()=>{for(const t of a)this.#D.has(t.id)&&t.translateInPage(s,n)},undo:()=>{for(const t of a)this.#D.has(t.id)&&t.translateInPage(-s,-n)},mustExec:!1})}),1e3);for(const i of a)i.translateInPage(t,e)}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#z=new Map;for(const t of this.#rt)this.#z.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#z)return!1;this.disableUserSelect(!1);const t=this.#z;this.#z=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},a]of t){a.newX=i;a.newY=s;a.newPageIndex=n;e||=i!==a.savedX||s!==a.savedY||n!==a.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#D.has(t.id)){const n=this.#k.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#z)for(const i of this.#z.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#rt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#P===t}getActive(){return this.#P}getMode(){return this.#at}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/a,y:1-(t+r-i)/n,width:o/a,height:r/n});break;case"180":r=(t,e,r,o)=>({x:1-(t+r-i)/n,y:1-(e+o-s)/a,width:r/n,height:o/a});break;case"270":r=(t,e,r,o)=>({x:1-(e+o-s)/a,y:(t-i)/n,width:o/a,height:r/n});break;default:r=(t,e,r,o)=>({x:(t-i)/n,y:(e-s)/a,width:r/n,height:o/a})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation()),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#Wt&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#Xt}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",(e=>{if(e.target===t&&"Enter"===e.key){this.#Gt=!0;onClick(e)}}),{signal:i});await this.#Kt();return t}get#Xt(){return(this.#o?"added":null===this.#o&&this.guessedText&&"review")||"missing"}finish(){if(this.#Bt){this.#Bt.focus({focusVisible:this.#Gt});this.#Gt=!1}}isEmpty(){return this.#Wt?null===this.#o:!this.#o&&!this.#Nt}hasData(){return this.#Wt?null!==this.#o||!!this.#Vt:this.isEmpty()}get guessedText(){return this.#Vt}async setGuessedText(t){if(null===this.#o){this.#Vt=t;this.#jt=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#Kt()}}toggleAltTextBadge(t=!1){if(this.#Wt&&!this.#o){if(!this.#$t){const t=this.#$t=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#$t.classList.toggle("hidden",!t)}else{this.#$t?.remove();this.#$t=null}}serialize(t){let e=this.#o;t||this.#Vt!==e||(e=this.#jt);return{altText:e,decorative:this.#Nt,guessedText:this.#Vt,textWithDisclaimer:this.#jt}}get data(){return{altText:this.#o,decorative:this.#Nt}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#Vt=i;this.#jt=s}if(this.#o!==t||this.#Nt!==e){if(!n){this.#o=t;this.#Nt=e}this.#Kt()}}toggle(t=!1){if(this.#Bt){if(!t&&this.#Ut){clearTimeout(this.#Ut);this.#Ut=null}this.#Bt.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#Xt}})}destroy(){this.#Bt?.remove();this.#Bt=null;this.#Ht=null;this.#zt=null;this.#$t?.remove();this.#$t=null}async#Kt(){const t=this.#Bt;if(!t)return;if(this.#Wt){t.classList.toggle("done",!!this.#o);t.setAttribute("data-l10n-id",AltText.#qt[this.#Xt]);this.#Ht?.setAttribute("data-l10n-id",AltText.#qt[`${this.#Xt}-label`]);if(!this.#o){this.#zt?.remove();return}}else{if(!this.#o&&!this.#Nt){t.classList.remove("done");this.#zt?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#zt;if(!e){this.#zt=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",(()=>{clearTimeout(this.#Ut);this.#Ut=null}),{once:!0});t.addEventListener("mouseenter",(()=>{this.#Ut=setTimeout((()=>{this.#Ut=null;this.#zt.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})}),i)}),{signal:s});t.addEventListener("mouseleave",(()=>{if(this.#Ut){clearTimeout(this.#Ut);this.#Ut=null}this.#zt?.classList.remove("show")}),{signal:s})}if(this.#Nt)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#o}e.parentNode||t.append(e);const i=this.#a.getImageForAltText();i?.setAttribute("aria-describedby",e.id)}}class TouchManager{#pt;#Yt=!1;#Qt=null;#Jt;#Zt;#te;#ee;#ie;#se=null;#ne;#ae=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:a=null,signal:r}){this.#pt=t;this.#Qt=i;this.#Jt=e;this.#Zt=s;this.#te=n;this.#ee=a;this.#ne=new AbortController;this.#ie=AbortSignal.any([r,this.#ne.signal]);t.addEventListener("touchstart",this.#re.bind(this),{passive:!1,signal:this.#ie})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return shadow(this,"MIN_TOUCH_DISTANCE_TO_PINCH",35/(window.devicePixelRatio||1))}#re(t){if(this.#Jt?.()||t.touches.length<2)return;if(!this.#ae){this.#ae=new AbortController;const t=AbortSignal.any([this.#ie,this.#ae.signal]),e=this.#pt,i={signal:t,passive:!1};e.addEventListener("touchmove",this.#oe.bind(this),i);e.addEventListener("touchend",this.#le.bind(this),i);e.addEventListener("touchcancel",this.#le.bind(this),i);this.#Zt?.()}stopEvent(t);if(2!==t.touches.length||this.#Qt?.()){this.#se=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#se={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#oe(t){if(!this.#se||2!==t.touches.length)return;let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:a,screenY:r}=i,o=this.#se,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=a-s,m=r-n,f=Math.hypot(g,m)||1,b=Math.hypot(u,p)||1;if(!this.#Yt&&Math.abs(b-f)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=a;o.touch1Y=r;t.preventDefault();if(!this.#Yt){this.#Yt=!0;return}const A=[(s+a)/2,(n+r)/2];this.#te?.(A,b,f)}#le(t){this.#ae.abort();this.#ae=null;this.#ee?.();if(this.#se){t.preventDefault();this.#se=null;this.#Yt=!1}}destroy(){this.#ne?.abort();this.#ne=null}}class AnnotationEditor{#he=null;#de=null;#o=null;#ce=!1;#ue=null;#pe="";#ge=!1;#me=null;#fe=null;#be=null;#Ae=null;#we="";#ve=!1;#ye=null;#xe=!1;#_e=!1;#Ee=!1;#Se=null;#Ce=0;#Te=0;#Me=null;#Pe=null;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#De=!1;#ke=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:a}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,a];const[r,o]=this.parentDimensions;this.x=t.x/r;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#De}set _isDraggable(t){this.#De=t;this.div?.classList.toggle("draggable",t)}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#ke}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else this.#Re();this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#ve?this.#ve=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}#Ie([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#Ie(this.parentDimensions,t,e)}translateInPage(t,e){this.#ye||=[this.x,this.y,this.width,this.height];this.#Ie(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}drag(t,e){this.#ye||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:a,y:r}=this;const[o,l]=this.getBaseTranslation();a+=o;r+=l;const{style:h}=i;h.left=`${(100*a).toFixed(2)}%`;h.top=`${(100*r).toFixed(2)}%`;this._onTranslating(a,r);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#ye&&(this.#ye[0]!==this.x||this.#ye[1]!==this.y)}get _hasBeenResized(){return!!this.#ye&&(this.#ye[2]!==this.width||this.#ye[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:a,width:r,height:o}=this;r*=i;o*=s;n*=i;a*=s;if(this._mustFixPosition)switch(t){case 0:n=Math.max(0,Math.min(i-r,n));a=Math.max(0,Math.min(s-o,a));break;case 90:n=Math.max(0,Math.min(i-o,n));a=Math.min(s,Math.max(r,a));break;case 180:n=Math.min(i,Math.max(r,n));a=Math.min(s,Math.max(o,a));break;case 270:n=Math.min(i,Math.max(o,n));a=Math.max(0,Math.min(s-r,a))}this.x=n/=i;this.y=a/=s;const[l,h]=this.getBaseTranslation();n+=l;a+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*a).toFixed(2)}%`;this.moveInDOM()}static#Fe(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#Fe(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#Fe(t,e,360-this.parentRotation)}#Le(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(t,e){const[i,s]=this.parentDimensions,{style:n}=this.div;n.width=`${(100*t/i).toFixed(2)}%`;this.#ge||(n.height=`${(100*e/s).toFixed(2)}%`)}fixDims(){const{style:t}=this.div,{height:e,width:i}=t,s=i.endsWith("%"),n=!this.#ge&&e.endsWith("%");if(s&&n)return;const[a,r]=this.parentDimensions;s||(t.width=`${(100*parseFloat(i)/a).toFixed(2)}%`);this.#ge||n||(t.height=`${(100*parseFloat(e)/r).toFixed(2)}%`)}getInitialTranslation(){return[0,0]}#Oe(){if(this.#me)return;this.#me=document.createElement("div");this.#me.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#me.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#Ne.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#me)}#Ne(t,e){e.preventDefault();const{isMac:i}=util_FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#o?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#fe=[e.screenX,e.screenY];const n=new AbortController,a=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#Be.bind(this,t),{passive:!0,capture:!0,signal:a});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:a});window.addEventListener("contextmenu",noContextMenu,{signal:a});this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const r=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#o?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=r;this.div.style.cursor=o;this.#He()};window.addEventListener("pointerup",pointerUpCallback,{signal:a});window.addEventListener("blur",pointerUpCallback,{signal:a})}#ze(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;const[n,a]=this.parentDimensions;this.setDims(n*i,a*s);this.fixAndSetPosition();this._onResized()}_onResized(){}#He(){if(!this.#be)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#be;this.#be=null;const n=this.x,a=this.y,r=this.width,o=this.height;n===t&&a===e&&r===i&&o===s||this.addCommands({cmd:this.#ze.bind(this,n,a,r,o),undo:this.#ze.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#Be(t,e){const[i,s]=this.parentDimensions,n=this.x,a=this.y,r=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#Le(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#Le(360-this.rotation);let u,p,g=!1,m=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":m=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":m=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const f=u(r,o),b=p(r,o);let A=transf(...b);const w=AnnotationEditor._round(n+A[0]),v=AnnotationEditor._round(a+A[1]);let y,x,_=1,E=1;if(e.fromKeyboard)({deltaX:y,deltaY:x}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#fe;[y,x]=this.screenToPageTranslation(t-s,i-n);this.#fe[0]=t;this.#fe[1]=i}[y,x]=(S=y/i,C=x/s,[c[0]*S+c[2]*C,c[1]*S+c[3]*C]);var S,C;if(g){const t=Math.hypot(r,o);_=E=Math.max(Math.min(Math.hypot(b[0]-f[0]-y,b[1]-f[1]-x)/t,1/r,1/o),l/r,h/o)}else m?_=Math.max(l,Math.min(1,Math.abs(b[0]-f[0]-y)))/r:E=Math.max(h,Math.min(1,Math.abs(b[1]-f[1]-x)))/o;const T=AnnotationEditor._round(r*_),M=AnnotationEditor._round(o*E);A=transf(...p(T,M));const P=w-A[0],D=v-A[1];this.#ye||=[this.x,this.y,this.width,this.height];this.width=T;this.height=M;this.x=P;this.y=D;this.setDims(i*T,s*M);this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#o?.finish()}async addEditToolbar(){if(this._editToolbar||this.#_e)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());this.#o&&await this._editToolbar.addAltText(this.#o);return this._editToolbar}removeEditToolbar(){if(this._editToolbar){this._editToolbar.remove();this._editToolbar=null;this.#o?.destroy()}}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}async addAltTextButton(){if(!this.#o){AltText.initialize(AnnotationEditor._l10n);this.#o=new AltText(this);if(this.#he){this.#o.data=this.#he;this.#he=null}await this.addEditToolbar()}}get altTextData(){return this.#o?.data}set altTextData(t){this.#o&&(this.#o.data=t)}get guessedAltText(){return this.#o?.guessedText}async setGuessedAltText(t){await(this.#o?.setGuessedText(t))}serializeAltText(t){return this.#o?.serialize(t)}hasAltText(){return!!this.#o&&!this.#o.isEmpty()}hasAltTextData(){return this.#o?.hasData()??!1}render(){this.div=document.createElement("div");this.div.setAttribute("data-editor-rotation",(360-this.rotation)%360);this.div.className=this.name;this.div.setAttribute("id",this.id);this.div.tabIndex=this.#ce?-1:0;this._isVisible||this.div.classList.add("hidden");this.setInForeground();this.#Ue();const[t,e]=this.parentDimensions;if(this.parentRotation%180!=0){this.div.style.maxWidth=`${(100*e/t).toFixed(2)}%`;this.div.style.maxHeight=`${(100*t/e).toFixed(2)}%`}const[i,s]=this.getInitialTranslation();this.translate(i,s);bindEvents(this,this.div,["pointerdown"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#Pe||=new TouchManager({container:this.div,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#Ge.bind(this),onPinching:this.#$e.bind(this),onPinchEnd:this.#Ve.bind(this),signal:this._uiManager._signal}));this._uiManager._editorUndoBar?.hide();return this.div}#Ge(){this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#o?.toggle(!1);this.parent.togglePointerEvents(!1)}#$e(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#Le(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[a,r]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/a,u=AnnotationEditor.MIN_SIZE/r;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#ye||=[o,l,h,d];const m=transf(h/2,d/2),f=AnnotationEditor._round(o+m[0]),b=AnnotationEditor._round(l+m[1]),A=transf(p/2,g/2);this.x=f-A[0];this.y=b-A[1];this.width=p;this.height=g;this.setDims(a*p,r*g);this.fixAndSetPosition();this._onResizing()}#Ve(){this.#o?.toggle(!0);this.parent.togglePointerEvents(!0);this.#He()}pointerdown(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#ve=!0;this._isDraggable?this.#je(t):this.#We(t)}}get isSelected(){return this._uiManager.isSelected(this)}#We(t){const{isMac:e}=util_FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#je(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),a={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#ue=null;this.#ve=!1;this._uiManager.endDragSession()||this.#We(t);i&&this._onStopDragging()};if(e){this.#Ce=t.clientX;this.#Te=t.clientY;this.#ue=t.pointerId;this.#pe=t.pointerType;window.addEventListener("pointermove",(t=>{if(!i){i=!0;this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#ue){stopEvent(t);return}const[a,r]=this.screenToPageTranslation(e-this.#Ce,s-this.#Te);this.#Ce=e;this.#Te=s;this._uiManager.dragSelectedEditors(a,r)}),a);window.addEventListener("touchmove",stopEvent,a);window.addEventListener("pointerdown",(t=>{t.pointerType===this.#pe&&(this.#Pe||t.isPrimary)&&cancelDrag(t);stopEvent(t)}),a)}const pointerUpCallback=t=>{this.#ue&&this.#ue!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#Se&&clearTimeout(this.#Se);this.#Se=setTimeout((()=>{this.#Se=null;this.parent?.moveEditorInDOM(this)}),0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,a]=this.pageDimensions,[r,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*a,u=this.width*n,p=this.height*a;switch(i){case 0:return[d+l+r,a-c-h-p+o,d+l+u+r,a-c-h+o];case 90:return[d+h+r,a-c+l+o,d+h+p+r,a-c+l+u+o];case 180:return[d-l-u+r,a-c+h+o,d-l+r,a-c+h+p+o];case 270:return[d-h-p+r,a-c-l-u+o,d-h+r,a-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,a]=t,r=n-i,o=a-s;switch(this.rotation){case 0:return[i,e-a,r,o];case 90:return[i,e-s,o,r];case 180:return[n,e-s,r,o];case 270:return[n,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){this.#_e=!0}disableEditMode(){this.#_e=!1}isInEditMode(){return this.#_e}shouldGetKeyboardEvents(){return this.#Ee}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:a}=window;return e0&&t0}#Ue(){if(this.#Ae||!this.div)return;this.#Ae=new AbortController;const t=this._uiManager.combinedSignal(this.#Ae);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#Ue()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){unreachable("An editor must be serializable")}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:i});s.rotation=t.rotation;s.#he=t.accessibilityData;const[n,a]=s.pageDimensions,[r,o,l,h]=s.getRectInCurrentCoords(t.rect,a);s.x=r/n;s.y=o/a;s.width=l/n;s.height=h/a;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#Ae?.abort();this.#Ae=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);if(this.#Se){clearTimeout(this.#Se);this.#Se=null}this.#Re();this.removeEditToolbar();if(this.#Me){for(const t of this.#Me.values())clearTimeout(t);this.#Me=null}this.parent=null;this.#Pe?.destroy();this.#Pe=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#Oe();this.#me.classList.remove("hidden");bindEvents(this,this.div,["keydown"])}}get toolbarPosition(){return null}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#be={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#me.children;if(!this.#de){this.#de=Array.from(e);const t=this.#qe.bind(this),i=this.#Xe.bind(this),s=this._uiManager._signal;for(const e of this.#de){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#Ke.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#de[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#de.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()}))}unselect(){this.#me?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#o?.toggleAltTextBadge(!0)}updateParams(t,e){}disableEditing(){}enableEditing(){}enterInEditMode(){}getImageForAltText(){return null}get contentDiv(){return this.div}get isEditing(){return this.#xe}set isEditing(t){this.#xe=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}setAspectRatio(t,e){this.#ge=!0;const i=t/e,{style:s}=this.div;s.aspectRatio=i;s.height="auto"}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#Me||=new Map;const{action:e}=t;let i=this.#Me.get(e);i&&clearTimeout(i);i=setTimeout((()=>{this._reportTelemetry(t);this.#Me.delete(e);0===this.#Me.size&&(this.#Me=null)}),AnnotationEditor._telemetryTimeout);this.#Me.set(e,i)}else{t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#ce=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#ce=!0}renderAnnotationElement(t){let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const st=3285377520,nt=4294901760,at=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:st;this.h2=t?4294967295&t:st}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,a=new Uint32Array(e.buffer,0,s);let r=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;r=r*c&nt|r*p&at;l^=r;l=l<<13|l>>>19;l=5*l+3864292196}else{o=a[t];o=o*d&nt|o*u&at;o=o<<15|o>>>17;o=o*c&nt|o*p&at;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}r=0;switch(n){case 3:r^=e[4*s+2]<<16;case 2:r^=e[4*s+1]<<8;case 1:r^=e[4*s];r=r*d&nt|r*u&at;r=r<<15|r>>>17;r=r*c&nt|r*p&at;1&s?l^=r:h^=r}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&nt|36045*t&at;e=4283543511*e&nt|(2950163797*(e<<16|t>>>16)&nt)>>>16;t^=e>>>1;t=444984403*t&nt|60499*t&at;e=3301882366*e&nt|(3120437893*(e<<16|t>>>16)&nt)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const rt=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#Qe=!1;#Je=null;#Ze=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#Ze.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#Ze.get(t)}remove(t){this.#Ze.delete(t);0===this.#Ze.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#Ze.values())if(t instanceof AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#Ze.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#Ze.set(t,e)}s&&this.#ti();e instanceof AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#Ze.has(t)}getAll(){return this.#Ze.size>0?objectFromMap(this.#Ze):null}setAll(t){for(const[e,i]of Object.entries(t))this.setValue(e,i)}get size(){return this.#Ze.size}#ti(){if(!this.#Qe){this.#Qe=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#Qe){this.#Qe=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#Ze.size)return rt;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,a]of this.#Ze){const r=a instanceof AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);n||=!!r.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:rt}get editorStats(){let t=null;const e=new Map;for(const i of this.#Ze.values()){if(!(i instanceof AnnotationEditor))continue;const s=i.telemetryFinalData;if(!s)continue;const{type:n}=s;e.has(n)||e.set(n,Object.getPrototypeOf(i).constructor);t||=Object.create(null);const a=t[n]||=new Map;for(const[t,e]of Object.entries(s)){if("type"===t)continue;let i=a.get(t);if(!i){i=new Map;a.set(t,i)}const s=i.get(e)??0;i.set(e,s+1)}}for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#Je=null}get modifiedIds(){if(this.#Je)return this.#Je;const t=[];for(const e of this.#Ze.values())e instanceof AnnotationEditor&&e.annotationElementId&&e.serialize()&&t.push(e.annotationElementId);return this.#Je={ids:new Set(t),hash:t.join(",")}}}class PrintAnnotationStorage extends AnnotationStorage{#ei;constructor(t){super();const{map:e,hash:i,transfer:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#ei={map:n,hash:i,transfer:s}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#ei}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}class FontLoader{#ii=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ii.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,_inspectFont:e}){if(t&&!this.#ii.has(t.loadedName)){assert(!this.disableFontFace,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:i,src:s,style:n}=t,a=new FontFace(i,s,n);this.addNativeFontFace(a);try{await a.load();this.#ii.add(i);e?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(a)}}else unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)}))}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){let t=!1;(e||"undefined"!=typeof navigator&&"string"==typeof navigator?.userAgent&&/Mozilla\/5.0.*?rv:\d+.*? Gecko/.test(navigator.userAgent))&&(t=!0);return shadow(this,"isSyncFontLoadingSupported",t)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const a=n.getContext("2d");let r=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++r>30){warn("Load test font never loaded.");e();return}a.font="30px "+t;a.fillText(".",0,20);a.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,(()=>{u.remove();e.complete()}))}}class FontFaceObject{constructor(t,{disableFontFace:e=!1,fontExtraProperties:i=!1,inspectFont:s=null}){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this.disableFontFace=!0===e;this.fontExtraProperties=!0===i;this._inspectFont=s}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${function toBase64Util(t){return Uint8Array.prototype.toBase64?t.toBase64():btoa(bytesToString(t))}(this.data)});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=new Path2D(s||"");this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}}const ot=1,lt=2,ht=1,dt=2,ct=3,ut=4,pt=5,gt=6,mt=7,ft=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof MissingPDFException||t instanceof PasswordException||t instanceof UnexpectedResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"MissingPDFException":return new MissingPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"UnexpectedResponseException":return new UnexpectedResponseException(t.message,t.status);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#si=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#ni.bind(this),{signal:this.#si.signal})}#ni({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#ai(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===ot)i.resolve(t.data);else{if(t.callback!==lt)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then((function(e){n.postMessage({sourceName:i,targetName:s,callback:ot,callbackId:t.callbackId,data:e})}),(function(e){n.postMessage({sourceName:i,targetName:s,callback:lt,callbackId:t.callbackId,reason:wrapReason(e)})}))}else t.streamId?this.#ri(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,a=this.sourceName,r=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:a,targetName:r,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:a,targetName:r,stream:gt,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:a,targetName:r,stream:ht,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#ri(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this,r=this.actionHandler[t.action],o={enqueue(t,a=1,r){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=a;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:ut,streamId:e,chunk:t},r)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:ct,streamId:e});delete a.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:pt,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(r,t.data,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:ft,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:ft,streamId:e,reason:wrapReason(t)})}))}#ai(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this.streamControllers[e],r=this.streamSinks[e];switch(t.stream){case ft:t.success?a.startCall.resolve():a.startCall.reject(wrapReason(t.reason));break;case mt:t.success?a.pullCall.resolve():a.pullCall.reject(wrapReason(t.reason));break;case gt:if(!r){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,success:!0});break}r.desiredSize<=0&&t.desiredSize>0&&r.sinkCapability.resolve();r.desiredSize=t.desiredSize;Promise.try(r.onPull||onFn).then((function(){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:mt,streamId:e,reason:wrapReason(t)})}));break;case ut:assert(a,"enqueue should have stream controller");if(a.isClosed)break;a.controller.enqueue(t.chunk);break;case ct:assert(a,"close should have stream controller");if(a.isClosed)break;a.isClosed=!0;a.controller.close();this.#oi(a,e);break;case pt:assert(a,"error should have stream controller");a.controller.error(wrapReason(t.reason));this.#oi(a,e);break;case dt:t.success?a.cancelCall.resolve():a.cancelCall.reject(wrapReason(t.reason));this.#oi(a,e);break;case ht:if(!r)break;const o=wrapReason(t.reason);Promise.try(r.onCancel||onFn,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:dt,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:dt,streamId:e,reason:wrapReason(t)})}));r.sinkCapability.reject(o);r.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#oi(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#si?.abort();this.#si=null}}class BaseCanvasFactory{#li=!1;constructor({enableHWA:t=!1}){this.#li=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#li})}}reset(t,e,i){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=i}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!0}){this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error("Ensure that the `cMapUrl` and `cMapPacked` API parameters are provided.");if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":"");return this._fetch(e).then((t=>({cMapData:t,isCompressed:this.isCompressed}))).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){const e=await fetchData(t,this.isCompressed?"arraybuffer":"text");return e instanceof ArrayBuffer?new Uint8Array(e):stringToBytes(e)}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `standardFontDataUrl` API parameter is provided.");if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}e&&warn("Please use the `legacy` build in Node.js environments.");async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs"),i=await e.promises.readFile(t);return new Uint8Array(i)}const bt="Fill",At="Stroke",wt="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,i,s){let n;if(s===At||s===bt){const a=e.current.getClippedPathBoundingBox(s,getCurrentTransform(t))||[0,0,0,0],r=Math.ceil(a[2]-a[0])||1,o=Math.ceil(a[3]-a[1])||1,l=e.cachedCanvases.getCanvas("pattern",r,o),h=l.context;h.clearRect(0,0,h.canvas.width,h.canvas.height);h.beginPath();h.rect(0,0,h.canvas.width,h.canvas.height);h.translate(-a[0],-a[1]);i=Util.transform(i,[1,0,0,1,a[0],a[1]]);h.transform(...e.baseTransform);this.matrix&&h.transform(...this.matrix);applyBoundingBox(h,this._bbox);h.fillStyle=this._createGradient(h);h.fill();n=t.createPattern(l.canvas,"no-repeat");const d=new DOMMatrix(i);n.setTransform(d)}else{applyBoundingBox(t,this._bbox);n=this._createGradient(t)}return n}}function drawTriangle(t,e,i,s,n,a,r,o){const l=e.coords,h=e.colors,d=t.data,c=4*t.width;let u;if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=r;r=o;o=u}if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}const p=(l[i]+e.offsetX)*e.scaleX,g=(l[i+1]+e.offsetY)*e.scaleY,m=(l[s]+e.offsetX)*e.scaleX,f=(l[s+1]+e.offsetY)*e.scaleY,b=(l[n]+e.offsetX)*e.scaleX,A=(l[n+1]+e.offsetY)*e.scaleY;if(g>=A)return;const w=h[a],v=h[a+1],y=h[a+2],x=h[r],_=h[r+1],E=h[r+2],S=h[o],C=h[o+1],T=h[o+2],M=Math.round(g),P=Math.round(A);let D,k,R,I,F,L,O,N;for(let t=M;t<=P;t++){if(tA?1:f===A?0:(f-t)/(f-A);D=m-(m-b)*e;k=x-(x-S)*e;R=_-(_-C)*e;I=E-(E-T)*e}let e;e=tA?1:(g-t)/(g-A);F=p-(p-b)*e;L=w-(w-S)*e;O=v-(v-C)*e;N=y-(y-T)*e;const i=Math.round(Math.min(D,F)),s=Math.round(Math.max(D,F));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(D-t)/(D-F);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-L)*e|0;d[n++]=R-(R-O)*e|0;d[n++]=I-(I-N)*e|0;d[n++]=255}}}function drawFigure(t,e,i){const s=e.coords,n=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(s.length/o)-1,h=o-1;for(a=0;a=Math.ceil(p*b)?w=o:y=!0;E>=Math.ceil(g*A)?v=l:x=!0;const S=this.getSizeAndScale(w,this.ctx.canvas.width,b),C=this.getSizeAndScale(v,this.ctx.canvas.height,A),T=t.cachedCanvases.getCanvas("pattern",S.size,C.size),M=T.context,P=r.createCanvasGraphics(M);P.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(P,s,a);M.translate(-S.scale*h,-C.scale*d);P.transform(S.scale,0,0,C.scale,0,0);M.save();this.clipBbox(P,h,d,c,u);P.baseTransform=getCurrentTransform(P.ctx);P.executeOperatorList(i);P.endDrawing();M.restore();if(y||x){const e=T.canvas;y&&(w=o);x&&(v=l);const i=this.getSizeAndScale(w,this.ctx.canvas.width,b),s=this.getSizeAndScale(v,this.ctx.canvas.height,A),n=i.size,a=s.size,r=t.cachedCanvases.getCanvas("pattern-workaround",n,a),c=r.context,u=y?Math.floor(p/o):0,m=x?Math.floor(g/l):0;for(let t=0;t<=u;t++)for(let i=0;i<=m;i++)c.drawImage(e,n*t,a*i,n,a,0,0,n,a);return{canvas:r.canvas,scaleX:i.scale,scaleY:s.scale,offsetX:h,offsetY:d}}return{canvas:T.canvas,scaleX:S.scale,scaleY:C.scale,offsetX:h,offsetY:d}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const a=s-e,r=n-i;t.ctx.rect(e,i,a,r);t.current.updateRectMinMax(getCurrentTransform(t.ctx),[e,i,s,n]);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;switch(e){case vt:const t=this.ctx;s.fillStyle=t.fillStyle;s.strokeStyle=t.strokeStyle;n.fillColor=t.fillStyle;n.strokeColor=t.strokeStyle;break;case yt:const a=Util.makeHexColor(i[0],i[1],i[2]);s.fillStyle=a;s.strokeStyle=a;n.fillColor=a;n.strokeColor=a;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}getPattern(t,e,i,s){let n=i;if(s!==wt){n=Util.transform(n,e.baseTransform);this.matrix&&(n=Util.transform(n,this.matrix))}const a=this.createPatternCanvas(e);let r=new DOMMatrix(n);r=r.translate(a.offsetX,a.offsetY);r=r.scale(1/a.scaleX,1/a.scaleY);const o=t.createPattern(a.canvas,"repeat");o.setTransform(r);return o}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:a=4294967295,inverseDecode:r=!1}){const o=util_FeatureTest.isLittleEndian?4278190080:255,[l,h]=r?[a,o]:[o,a],d=s>>3,c=7&s,u=t.length;i=new Uint32Array(i.buffer);let p=0;for(let s=0;s>2),m=i.length,f=s+7>>3,b=4294967295,A=util_FeatureTest.isLittleEndian?4278190080:255;for(u=0;uf?s:8*t-7,r=-8&a;let o=0,c=0;for(;n>=1}}for(;l=a){g=n;m=s*g}l=0;for(p=m;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*xt)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%xt,a=(i-n)/xt,r=0===n?a:a+1,o=t.createImageData(s,xt);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,h=l?Date.now()+15:0;let d=0;const c=this.commonObjs,u=this.objs;let p;for(;;){if(void 0!==s&&r===s.nextBreakPoint){s.breakIt(r,i);return r}p=a[r];if(p!==X.dependency)this[p].apply(this,n[r]);else for(const t of n[r]){const e=t.startsWith("g_")?c:u;if(!e.has(t)){e.get(t,i);return r}}r++;if(r===o)return r;if(l&&++d>10){if(Date.now()>h){i();return r}d=0}}}#hi(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#hi();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#di()}#di(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,d="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(d,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;d="prescale1"===d?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:s}=t,n=this.current.fillColor,a=this.current.patternFill,r=getCurrentTransform(e);let o,l,h,d;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;l=JSON.stringify(a?r:[r.slice(0,4),n]);o=this._cachedBitmapsMap.get(e);if(!o){o=new Map;this._cachedBitmapsMap.set(e,o)}const i=o.get(l);if(i&&!a){return{canvas:i,offsetX:Math.round(Math.min(r[0],r[2])+r[4]),offsetY:Math.round(Math.min(r[1],r[3])+r[5])}}h=i}if(!h){d=this.cachedCanvases.getCanvas("maskCanvas",i,s);putBinaryImageMask(d.context,t)}let c=Util.transform(r,[1/i,0,0,-1/s,0,0]);c=Util.transform(c,[1,0,0,1,0,-s]);const[u,p,g,m]=Util.getAxialAlignedBoundingBox([0,0,i,s],c),f=Math.round(g-u)||1,b=Math.round(m-p)||1,A=this.cachedCanvases.getCanvas("fillCanvas",f,b),w=A.context,v=u,y=p;w.translate(-v,-y);w.transform(...c);if(!h){h=this._scaleImage(d.canvas,getCurrentTransformInverse(w));h=h.img;o&&a&&o.set(l,h)}w.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(w),t.interpolate);drawImageAtIntegerCoords(w,h,0,0,h.width,h.height,0,0,i,s);w.globalCompositeOperation="source-in";const x=Util.transform(getCurrentTransformInverse(w),[1,0,0,1,-v,-y]);w.fillStyle=a?n.getPattern(e,this,x,bt):n;w.fillRect(0,0,i,s);if(o&&!a){this.cachedCanvases.delete("fillCanvas");o.set(l,A.canvas)}return{canvas:A.canvas,offsetX:Math.round(v),offsetY:Math.round(y)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=_t[t]}setLineJoin(t){this.ctx.lineJoin=Et[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.current.fillAlpha=i;this.ctx.globalAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;this.ctx=s.context;const n=this.ctx;n.setTransform(...getCurrentTransform(this.suspendedCtx));copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function ctxSave(){e.save();this.__originalSave()};t.restore=function ctxRestore(){e.restore();this.__originalRestore()};t.translate=function ctxTranslate(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function ctxScale(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function ctxTransform(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function ctxSetTransform(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function ctxResetTransform(){e.resetTransform();this.__originalResetTransform()};t.rotate=function ctxRotate(t){e.rotate(t);this.__originalRotate(t)};t.clip=function ctxRotate(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this.setGState([["BM","source-over"],["ca",1],["CA",1]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){this.genericComposeSMask(e.context,i,r,o,e.subtype,e.backdrop,e.transferMap,n,a,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation="source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,0,0);t.restore()}}genericComposeSMask(t,e,i,s,n,a,r,o,l,h,d){let c=t.canvas,u=o-h,p=l-d;if(a){const e=Util.makeHexColor(...a);if(u<0||p<0||u+i>c.width||p+s>c.height){const t=this.cachedCanvases.getCanvas("maskExtension",i,s),n=t.context;n.drawImage(c,-u,-p);n.globalCompositeOperation="destination-atop";n.fillStyle=e;n.fillRect(0,0,i,s);n.globalCompositeOperation="source-over";c=t.canvas;u=p=0}else{t.save();t.globalAlpha=1;t.setTransform(1,0,0,1,0,0);const n=new Path2D;n.rect(u,p,i,s);t.clip(n);t.globalCompositeOperation="destination-atop";t.fillStyle=e;t.fillRect(u,p,i,s);t.restore()}}e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);"Alpha"===n&&r?e.filter=this.filterFactory.addAlphaFilter(r):"Luminosity"===n&&(e.filter=this.filterFactory.addLuminosityFilter(r));const g=new Path2D;g.rect(o,l,i,s);e.clip(g);e.globalCompositeOperation="destination-in";e.drawImage(c,u,p,i,s,o,l,i,s);e.restore()}save(){if(this.inSMaskMode){copyCtxState(this.ctx,this.suspendedCtx);this.suspendedCtx.save()}else this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){0===this.stateStack.length&&this.inSMaskMode&&this.endSMaskMode();if(0!==this.stateStack.length){this.current=this.stateStack.pop();if(this.inSMaskMode){this.suspendedCtx.restore();copyCtxState(this.suspendedCtx,this.ctx)}else this.ctx.restore();this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){const s=this.ctx,n=this.current;let a,r,o=n.x,l=n.y;const h=getCurrentTransform(s),d=0===h[0]&&0===h[3]||0===h[1]&&0===h[2],c=d?i.slice(0):null;for(let i=0,u=0,p=t.length;i100&&(h=100);this.current.fontSizeScale=e/h;this.ctx.font=`${l} ${o} ${h}px ${r}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t,e,i,s,n,a){this.current.textMatrix=[t,e,i,s,n,a];this.current.textMatrixScale=Math.hypot(t,e);this.current.x=this.current.lineX=0;this.current.y=this.current.lineY=0}nextLine(){this.moveText(0,this.current.leading)}#ci(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n){const a=this.ctx,r=this.current,o=r.font,l=r.textRenderingMode,h=r.fontSize/r.fontSizeScale,d=l&y,c=!!(l&x),u=r.patternFill&&!o.missingFile,p=r.patternStroke&&!o.missingFile;let g;(o.disableFontFace||c||u||p)&&(g=o.getPathGenerator(this.commonObjs,t));if(o.disableFontFace||u||p){a.save();a.translate(e,i);a.scale(h,-h);if(d===b||d===w)if(s){const t=a.getTransform();a.setTransform(...s);a.fill(this.#ci(g,t,s))}else a.fill(g);if(d===A||d===w)if(n){const t=a.getTransform();a.setTransform(...n);a.stroke(this.#ci(g,t,n))}else{a.lineWidth/=h;a.stroke(g)}a.restore()}else{d!==b&&d!==w||a.fillText(t,e,i);d!==A&&d!==w||a.strokeText(t,e,i)}if(c){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:e,y:i,fontSize:h,path:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let i=!1;for(let t=3;t0&&e[t]<255){i=!0;break}return shadow(this,"isFontSubpixelAAEnabled",i)}showText(t){const e=this.current,i=e.font;if(i.isType3Font)return this.showType3Text(t);const s=e.fontSize;if(0===s)return;const n=this.ctx,a=e.fontSizeScale,r=e.charSpacing,o=e.wordSpacing,l=e.fontDirection,h=e.textHScale*l,d=t.length,c=i.vertical,u=c?1:-1,p=i.defaultVMetrics,g=s*e.fontMatrix[0],m=e.textRenderingMode===b&&!i.disableFontFace&&!e.patternFill;n.save();n.transform(...e.textMatrix);n.translate(e.x,e.y+e.textRise);l>0?n.scale(h,-1):n.scale(h,1);let f,v;if(e.patternFill){n.save();const t=e.fillColor.getPattern(n,this,getCurrentTransformInverse(n),bt);f=getCurrentTransform(n);n.restore();n.fillStyle=t}if(e.patternStroke){n.save();const t=e.strokeColor.getPattern(n,this,getCurrentTransformInverse(n),At);v=getCurrentTransform(n);n.restore();n.strokeStyle=t}let x=e.lineWidth;const _=e.textMatrixScale;if(0===_||0===x){const t=e.textRenderingMode&y;t!==A&&t!==w||(x=this.getSinglePixelWidth())}else x/=_;if(1!==a){n.scale(a,a);x/=a}n.lineWidth=x;if(i.isInvalidPDFjsFont){const i=[];let s=0;for(const e of t){i.push(e.unicode);s+=e.width}n.fillText(i.join(""),0,0);e.x+=s*g*h;n.restore();this.compose();return}let E,S=0;for(E=0;E0){const t=1e3*n.measureText(b).width/s*a;if(xnew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new TilingPattern(t,i,this.ctx,n,s)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments);this.current.patternStroke=!0}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t,e,i){this.ctx.strokeStyle=this.current.strokeColor=Util.makeHexColor(t,e,i);this.current.patternStroke=!1}setStrokeTransparent(){this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t,e,i){this.ctx.fillStyle=this.current.fillColor=Util.makeHexColor(t,e,i);this.current.patternFill=!1}setFillTransparent(){this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1}_getPattern(t,e=null){let i;if(this.cachedPatterns.has(t))i=this.cachedPatterns.get(t);else{i=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t));this.cachedPatterns.set(t,i)}e&&(i.matrix=e);return i}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const i=this._getPattern(t);e.fillStyle=i.getPattern(e,this,getCurrentTransformInverse(e),wt);const s=getCurrentTransformInverse(e);if(s){const{width:t,height:i}=e.canvas,[n,a,r,o]=Util.getAxialAlignedBoundingBox([0,0,t,i],s);this.ctx.fillRect(n,a,r-n,o-a)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);t&&this.transform(...t);this.baseTransform=getCurrentTransform(this.ctx);if(e){const t=e[2]-e[0],i=e[3]-e[1];this.ctx.rect(e[0],e[1],t,i);this.current.updateRectMinMax(getCurrentTransform(this.ctx),e);this.clip();this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||info("TODO: Support non-isolated groups.");t.knockout&&warn("Knockout groups not supported.");const i=getCurrentTransform(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let s=Util.getAxialAlignedBoundingBox(t.bbox,getCurrentTransform(e));const n=[0,0,e.canvas.width,e.canvas.height];s=Util.intersect(s,n)||[0,0,0,0];const a=Math.floor(s[0]),r=Math.floor(s[1]),o=Math.max(Math.ceil(s[2])-a,1),l=Math.max(Math.ceil(s[3])-r,1);this.current.startNewPathAndClipBox([0,0,o,l]);let h="groupAt"+this.groupLevel;t.smask&&(h+="_smask_"+this.smaskCounter++%2);const d=this.cachedCanvases.getCanvas(h,o,l),c=d.context;c.translate(-a,-r);c.transform(...i);if(t.smask)this.smaskStack.push({canvas:d.canvas,context:c,offsetX:a,offsetY:r,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(a,r);e.save()}copyCtxState(e,c);this.ctx=c;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,i=this.groupStack.pop();this.ctx=i;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=getCurrentTransform(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const i=Util.getAxialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(i)}}beginAnnotation(t,e,i,s,n){this.#hi();resetCtxToDefault(this.ctx);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(e){const s=e[2]-e[0],a=e[3]-e[1];if(n&&this.annotationCanvasMap){(i=i.slice())[4]-=e[0];i[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=s;e[3]=a;const[n,r]=Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx)),{viewportScale:o}=this,l=Math.ceil(s*this.outputScaleX*o),h=Math.ceil(a*this.outputScaleY*o);this.annotationCanvas=this.canvasFactory.create(l,h);const{canvas:d,context:c}=this.annotationCanvas;this.annotationCanvasMap.set(t,d);this.annotationCanvas.savedCtx=this.ctx;this.ctx=c;this.ctx.save();this.ctx.setTransform(n,0,0,-r,0,a*r);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath();this.ctx.rect(e[0],e[1],s,a);this.ctx.clip();this.ctx.beginPath()}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...i);this.transform(...s)}endAnnotation(){if(this.annotationCanvas){this.ctx.restore();this.#di();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const i=this.ctx,s=this.processingType3;if(s){void 0===s.compiled&&(s.compiled=function compileType3Glyph(t){const{width:e,height:i}=t;if(e>1e3||i>1e3)return null;const s=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),n=e+1;let a,r,o,l=new Uint8Array(n*(i+1));const h=e+7&-8;let d=new Uint8Array(h*i),c=0;for(const e of t.data){let t=128;for(;t>0;){d[c++]=e&t?0:255;t>>=1}}let u=0;c=0;if(0!==d[c]){l[0]=1;++u}for(r=1;r>2)+(d[c+1]?4:0)+(d[c-h+1]?8:0);if(s[t]){l[o+r]=s[t];++u}c++}if(d[c-h]!==d[c]){l[o+r]=d[c]?2:4;++u}if(u>1e3)return null}c=h*(i-1);o=a*n;if(0!==d[c]){l[o]=8;++u}for(r=1;r1e3)return null;const p=new Int32Array([0,n,-1,0,-n,0,0,0,1]),g=new Path2D;for(a=0;u&&a<=i;a++){let t=a*n;const i=t+e;for(;t>4;l[t]&=r>>2|r<<2}g.lineTo(t%n,t/n|0);l[t]||--u}while(s!==t);--a}d=null;l=null;return function(t){t.save();t.scale(1/e,-1/i);t.translate(0,-i);t.fill(g);t.beginPath();t.restore()}}(t));if(s.compiled){s.compiled(i);return}}const n=this._createMaskCanvas(t),a=n.canvas;i.save();i.setTransform(1,0,0,1,0,0);i.drawImage(a,n.offsetX,n.offsetY);i.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,i=0,s=0,n,a){if(!this.contentVisible)return;t=this.getObject(t.data,t);const r=this.ctx;r.save();const o=getCurrentTransform(r);r.transform(e,i,s,n,0,0);const l=this._createMaskCanvas(t);r.setTransform(1,0,0,1,l.offsetX-o[4],l.offsetY-o[5]);for(let t=0,h=a.length;te?h/e:1;r=l>e?l/e:1}}this._cachedScaleForStroking[0]=a;this._cachedScaleForStroking[1]=r}return this._cachedScaleForStroking}rescaleAndStroke(t){const{ctx:e}=this,{lineWidth:i}=this.current,[s,n]=this.getScaleForStroking();e.lineWidth=i||1;if(1===s&&1===n){e.stroke();return}const a=e.getLineDash();t&&e.save();e.scale(s,n);if(a.length>0){const t=Math.max(s,n);e.setLineDash(a.map((e=>e/t)));e.lineDashOffset/=t}e.stroke();t&&e.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in X)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[X[t]]=CanvasGraphics.prototype[t]);class GlobalWorkerOptions{static#ui=null;static#pi="";static get workerPort(){return this.#ui}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#ui=t}static get workerSrc(){return this.#pi}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#pi=t}}class Metadata{#gi;#mi;constructor({parsedData:t,rawData:e}){this.#gi=t;this.#mi=e}getRaw(){return this.#mi}get(t){return this.#gi.get(t)??null}getAll(){return objectFromMap(this.#gi)}has(t){return this.#gi.has(t)}}const Tt=Symbol("INTERNAL");class OptionalContentGroup{#fi=!1;#bi=!1;#Ai=!1;#wi=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:n}){this.#fi=!!(t&r);this.#bi=!!(t&o);this.name=e;this.intent=i;this.usage=s;this.rbGroups=n}get visible(){if(this.#Ai)return this.#wi;if(!this.#wi)return!1;const{print:t,view:e}=this.usage;return this.#fi?"OFF"!==e?.viewState:!this.#bi||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==Tt&&unreachable("Internal method `_setVisible` called.");this.#Ai=i;this.#wi=e}}class OptionalContentConfig{#vi=null;#yi=new Map;#xi=null;#_i=null;constructor(t,e=r){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#_i=t.order;for(const i of t.groups)this.#yi.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#yi.values())t._setVisible(Tt,!1);for(const e of t.on)this.#yi.get(e)._setVisible(Tt,!0);for(const e of t.off)this.#yi.get(e)._setVisible(Tt,!1);this.#xi=this.getHash()}}#Ei(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;s0?objectFromMap(this.#yi):null}getGroup(t){return this.#yi.get(t)||null}getHash(){if(null!==this.#vi)return this.#vi;const t=new MurmurHash3_64;for(const[e,i]of this.#yi)t.update(`${e}:${i.visible}`);return this.#vi=t.hexdigest()}}class PDFDataTransportStream{constructor(t,{disableRange:e=!1,disableStream:i=!1}){assert(t,'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.');const{length:s,initialData:n,progressiveDone:a,contentDispositionFilename:r}=t;this._queuedChunks=[];this._progressiveDone=a;this._contentDispositionFilename=r;if(n?.length>0){const t=n instanceof Uint8Array&&n.byteLength===n.buffer.byteLength?n.buffer:new Uint8Array(n).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=t;this._isStreamingSupported=!i;this._isRangeSupported=!e;this._contentLength=s;this._fullRequestReader=null;this._rangeReaders=[];t.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));t.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));t.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));t.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));t.transportReady()}_onReceiveData({begin:t,chunk:e}){const i=e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer;if(void 0===t)this._fullRequestReader?this._fullRequestReader._enqueue(i):this._queuedChunks.push(i);else{assert(this._rangeReaders.some((function(e){if(e._begin!==t)return!1;e._enqueue(i);return!0})),"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader{constructor(t,e,i=!1,s=null){this._stream=t;this._done=i||!1;this._filename=isPdfFile(s)?s:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,i){this._stream=t;this._begin=e;this._end=i;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){try{return new URL(t).origin}catch{}return null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(a))return n;n.suggestedLength=a;if(a<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;if("identity"!==(t.get("Content-Encoding")||"identity"))return n;n.allowRangeRequests=!0;return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(e.url);if(!validateResponseStatus(e.status))throw createResponseStatusError(e.status,s);this._reader=e.body.getReader();this._headersCapability.resolve();const i=e.headers,{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:i,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=n;this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(i);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,i){this._stream=t;this._reader=null;this._loaded=0;const s=t.source;this._withCredentials=s.withCredentials||!1;this._readCapability=Promise.withResolvers();this._isStreamingSupported=!s.disableStream;this._abortController=new AbortController;const n=new Headers(t.headers);n.append("Range",`bytes=${e}-${i-1}`);const a=s.url;fetch(a,createFetchOptions(n,this._withCredentials,this._abortController)).then((e=>{const i=getResponseOrigin(e.url);if(i!==t._responseOrigin)throw new Error(`Expected range response-origin "${i}" to match "${t._responseOrigin}".`);if(!validateResponseStatus(e.status))throw createResponseStatusError(e.status,a);this._readCapability.resolve();this._reader=e.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class NetworkManager{_responseOrigin=null;constructor({url:t,httpHeaders:e,withCredentials:i}){this.url=t;this.isHttp=/^https?:/i.test(t);this.headers=createHeaders(this.isHttp,e);this.withCredentials=i||!1;this.currXhrId=0;this.pendingRequests=Object.create(null)}request(t){const e=new XMLHttpRequest,i=this.currXhrId++,s=this.pendingRequests[i]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);s.expectedStatus=206}else s.expectedStatus=200;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>{t.onError(e.status)};e.onreadystatechange=this.onStateChange.bind(this,i);e.onprogress=this.onProgress.bind(this,i);s.onHeadersReceived=t.onHeadersReceived;s.onDone=t.onDone;s.onError=t.onError;s.onProgress=t.onProgress;e.send(null);return i}onProgress(t,e){const i=this.pendingRequests[t];i&&i.onProgress?.(e)}onStateChange(t,e){const i=this.pendingRequests[t];if(!i)return;const s=i.xhr;if(s.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==s.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===s.status&&this.isHttp){i.onError(s.status);return}const n=s.status||200;if(!(200===n&&206===i.expectedStatus)&&n!==i.expectedStatus){i.onError(s.status);return}const a=function network_getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:stringToBytes(e).buffer}(s);if(206===n){const t=s.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);if(e)i.onDone({begin:parseInt(e[1],10),chunk:a});else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else a?i.onDone({begin:0,chunk:a}):i.onError(s.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t);this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const i=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);i.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;this._url=e.url;this._fullRequestId=t.request({onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._headersCapability=Promise.withResolvers();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t);this._manager._responseOrigin=getResponseOrigin(e.responseURL);const i=e.getAllResponseHeaders(),s=new Headers(i?i.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map((t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]})):[]),{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:s,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});n&&(this._isRangeSupported=!0);this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(s);this._isRangeSupported&&this._manager.abortRequest(t);this._headersCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=createResponseStatusError(t,this._url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersCapability.promise}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,i){this._manager=t;this._url=t.url;this._requestId=t.request({begin:e,end:i,onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_onHeadersReceived(){const t=getResponseOrigin(this._manager.getRequestXhr(this._requestId)?.responseURL);if(t!==this._manager._responseOrigin){this._storedError=new Error(`Expected range response-origin "${t}" to match "${this._manager._responseOrigin}".`);this._onError(0)}}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError??=createResponseStatusError(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}const Mt=/^[a-z][a-z0-9\-+.]+:/i;class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrlOrPath(t){if(Mt.test(t))return new URL(t);const e=process.getBuiltinModule("url");return new URL(e.pathToFileURL(t))}(t.url);assert("file:"===this.url.protocol,"PDFNodeStream only supports file:// URLs.");this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){assert(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNodeStreamFsFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFNodeStreamFsRangeReader(this,t,e);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNodeStreamFsFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=Promise.withResolvers();this._headersCapability=Promise.withResolvers();const i=process.getBuiltinModule("fs");i.promises.lstat(this._url).then((t=>{this._contentLength=t.size;this._setReadableStream(i.createReadStream(this._url));this._headersCapability.resolve()}),(t=>{"ENOENT"===t.code&&(t=new MissingPDFException(`Missing PDF "${this._url}".`));this._storedError=t;this._headersCapability.reject(t)}))}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class PDFNodeStreamFsRangeReader{constructor(t,e,i){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=Promise.withResolvers();const s=t.source;this._isStreamingSupported=!s.disableStream;const n=process.getBuiltinModule("fs");this._setReadableStream(n.createReadStream(this._url,{start:e,end:i-1}))}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}const Pt=30;class TextLayer{#Si=Promise.withResolvers();#pt=null;#Ci=!1;#Ti=!!globalThis.FontInspector?.enabled;#Mi=null;#Pi=null;#Di=0;#ki=0;#Ri=null;#Ii=null;#Fi=0;#Li=0;#Oi=Object.create(null);#Ni=[];#Bi=null;#Hi=[];#zi=new WeakMap;#Ui=null;static#Gi=new Map;static#$i=new Map;static#Vi=new WeakMap;static#ji=null;static#Wi=new Set;constructor({textContentSource:t,container:e,viewport:i}){if(t instanceof ReadableStream)this.#Bi=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#Bi=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#pt=this.#Ii=e;this.#Li=i.scale*(globalThis.devicePixelRatio||1);this.#Fi=i.rotation;this.#Pi={div:null,properties:null,ctx:null};const{pageWidth:s,pageHeight:n,pageX:a,pageY:r}=i.rawDims;this.#Ui=[1,0,0,-1,-a,r+n];this.#ki=s;this.#Di=n;TextLayer.#qi();setLayerDimensions(e,i);this.#Si.promise.finally((()=>{TextLayer.#Wi.delete(this);this.#Pi=null;this.#Oi=null})).catch((()=>{}))}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=util_FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){const pump=()=>{this.#Ri.read().then((({value:t,done:e})=>{if(e)this.#Si.resolve();else{this.#Mi??=t.lang;Object.assign(this.#Oi,t.styles);this.#Xi(t.items);pump()}}),this.#Si.reject)};this.#Ri=this.#Bi.getReader();TextLayer.#Wi.add(this);pump();return this.#Si.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*(globalThis.devicePixelRatio||1),s=t.rotation;if(s!==this.#Fi){e?.();this.#Fi=s;setLayerDimensions(this.#Ii,{rotation:s})}if(i!==this.#Li){e?.();this.#Li=i;const t={div:null,properties:null,ctx:TextLayer.#Ki(this.#Mi)};for(const e of this.#Hi){t.properties=this.#zi.get(e);t.div=e;this.#Yi(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#Ri?.cancel(t).catch((()=>{}));this.#Ri=null;this.#Si.reject(t)}get textDivs(){return this.#Hi}get textContentItemsStr(){return this.#Ni}#Xi(t){if(this.#Ci)return;this.#Pi.ctx??=TextLayer.#Ki(this.#Mi);const e=this.#Hi,i=this.#Ni;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#Ci=!0;return}if(void 0!==s.str){i.push(s.str);this.#Qi(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#pt;this.#pt=document.createElement("span");this.#pt.classList.add("markedContent");null!==s.id&&this.#pt.setAttribute("id",`${s.id}`);t.append(this.#pt)}else"endMarkedContent"===s.type&&(this.#pt=this.#pt.parentNode)}}#Qi(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#Hi.push(e);const s=Util.transform(this.#Ui,t.transform);let n=Math.atan2(s[1],s[0]);const a=this.#Oi[t.fontName];a.vertical&&(n+=Math.PI/2);let r=this.#Ti&&a.fontSubstitution||a.fontFamily;r=TextLayer.fontFamilyMap.get(r)||r;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#Ji(r,this.#Mi);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c="calc(var(--scale-factor)*",u=e.style;if(this.#pt===this.#Ii){u.left=`${(100*h/this.#ki).toFixed(2)}%`;u.top=`${(100*d/this.#Di).toFixed(2)}%`}else{u.left=`${c}${h.toFixed(2)}px)`;u.top=`${c}${d.toFixed(2)}px)`}u.fontSize=`${c}${(TextLayer.#ji*o).toFixed(2)}px)`;u.fontFamily=r;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#Ti&&(e.dataset.fontName=a.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let p=!1;if(t.str.length>1)p=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(p=!0)}p&&(i.canvasWidth=a.vertical?t.height:t.width);this.#zi.set(e,i);this.#Pi.div=e;this.#Pi.properties=i;this.#Yi(this.#Pi);i.hasText&&this.#pt.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#pt.append(t)}}#Yi(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;let a="";TextLayer.#ji>1&&(a=`scale(${1/TextLayer.#ji})`);if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:o}=i;TextLayer.#Zi(s,o*this.#Li,t);const{width:l}=s.measureText(e.textContent);l>0&&(a=`scaleX(${r*this.#Li/l}) ${a}`)}0!==i.angle&&(a=`rotate(${i.angle}deg) ${a}`);a.length>0&&(n.transform=a)}static cleanup(){if(!(this.#Wi.size>0)){this.#Gi.clear();for(const{canvas:t}of this.#$i.values())t.remove();this.#$i.clear()}}static#Ki(t=null){let e=this.#$i.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#$i.set(t,e);this.#Vi.set(e,{size:0,family:""})}return e}static#Zi(t,e,i){const s=this.#Vi.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#qi(){if(null!==this.#ji)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#ji=t.getBoundingClientRect().height;t.remove()}static#Ji(t,e){const i=this.#Gi.get(t);if(i)return i;const s=this.#Ki(e);s.canvas.width=s.canvas.height=Pt;this.#Zi(s,Pt,t);const n=s.measureText("");let a=n.fontBoundingBoxAscent,r=Math.abs(n.fontBoundingBoxDescent);if(a){const e=a/(a+r);this.#Gi.set(t,e);s.canvas.width=s.canvas.height=0;return e}s.strokeStyle="red";s.clearRect(0,0,Pt,Pt);s.strokeText("g",0,0);let o=s.getImageData(0,0,Pt,Pt).data;r=0;for(let t=o.length-1-3;t>=0;t-=4)if(o[t]>0){r=Math.ceil(t/4/Pt);break}s.clearRect(0,0,Pt,Pt);s.strokeText("A",0,Pt);o=s.getImageData(0,0,Pt,Pt).data;a=0;for(let t=0,e=o.length;t0){a=Pt-Math.floor(t/4/Pt);break}s.canvas.width=s.canvas.height=0;const l=a?a/(a+r):.8;this.#Gi.set(t,l);return l}}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}const Dt=65536,kt=e?class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}:class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}},Rt=e?class NodeCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){return node_utils_fetchData(t)}}:DOMCMapReaderFactory,It=e?class NodeFilterFactory extends BaseFilterFactory{}:class DOMFilterFactory extends BaseFilterFactory{#ts;#es;#is;#ss;#ns;#as;#w=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#ss=t;this.#ns=e}get#y(){return this.#es||=new Map}get#rs(){return this.#as||=new Map}get#os(){if(!this.#is){const t=this.#ns.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#ns.createElementNS(it,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#is=this.#ns.createElementNS(it,"defs");t.append(i);i.append(this.#is);this.#ns.body.append(t)}return this.#is}#ls(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),a=new Array(256),r=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;a[t]=i[t]/255;r[t]=s[t]/255}return[n.join(","),a.join(","),r.join(",")]}#hs(t){if(void 0===this.#ts){this.#ts="";const t=this.#ns.URL;t!==this.#ns.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#ts=t.split("#",1)[0])}return`url(${this.#ts}#${t})`}addFilter(t){if(!t)return"none";let e=this.#y.get(t);if(e)return e;const[i,s,n]=this.#ls(t),a=1===t.length?i:`${i}${s}${n}`;e=this.#y.get(a);if(e){this.#y.set(t,e);return e}const r=`g_${this.#ss}_transfer_map_${this.#w++}`,o=this.#hs(r);this.#y.set(t,o);this.#y.set(a,o);const l=this.#ds(r);this.#cs(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#rs.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#rs.set(s,n)}if(!t||!e)return n.url;const a=this.#us(t);t=Util.makeHexColor(...a);const r=this.#us(e);e=Util.makeHexColor(...r);this.#os.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#ss}_hcm_filter`,d=n.filter=this.#ds(h);this.#cs(l,l,l,d);this.#ps(d);const getSteps=(t,e)=>{const i=a[t]/255,s=r[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#cs(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#hs(h);return n.url}addAlphaFilter(t){let e=this.#y.get(t);if(e)return e;const[i]=this.#ls([t]),s=`alpha_${i}`;e=this.#y.get(s);if(e){this.#y.set(t,e);return e}const n=`g_${this.#ss}_alpha_map_${this.#w++}`,a=this.#hs(n);this.#y.set(t,a);this.#y.set(s,a);const r=this.#ds(n);this.#gs(i,r);return a}addLuminosityFilter(t){let e,i,s=this.#y.get(t||"luminosity");if(s)return s;if(t){[e]=this.#ls([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#y.get(i);if(s){this.#y.set(t,s);return s}const n=`g_${this.#ss}_luminosity_map_${this.#w++}`,a=this.#hs(n);this.#y.set(t,a);this.#y.set(i,a);const r=this.#ds(n);this.#ms(r);t&&this.#gs(e,r);return a}addHighlightHCMFilter(t,e,i,s,n){const a=`${e}-${i}-${s}-${n}`;let r=this.#rs.get(t);if(r?.key===a)return r.url;if(r){r.filter?.remove();r.key=a;r.url="none";r.filter=null}else{r={key:a,url:"none",filter:null};this.#rs.set(t,r)}if(!e||!i)return r.url;const[o,l]=[e,i].map(this.#us.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#us.bind(this));d{const s=new Array(256),n=(d-h)/i,a=t/255,r=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=a+t*r;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#ss}_hcm_${t}_filter`,g=r.filter=this.#ds(p);this.#ps(g);this.#cs(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);r.url=this.#hs(p);return r.url}destroy(t=!1){if(!t||!this.#as?.size){this.#is?.parentNode.parentNode.remove();this.#is=null;this.#es?.clear();this.#es=null;this.#as?.clear();this.#as=null;this.#w=0}}#ms(t){const e=this.#ns.createElementNS(it,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#ps(t){const e=this.#ns.createElementNS(it,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#ds(t){const e=this.#ns.createElementNS(it,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#os.append(e);return e}#fs(t,e,i){const s=this.#ns.createElementNS(it,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#cs(t,e,i,s){const n=this.#ns.createElementNS(it,"feComponentTransfer");s.append(n);this.#fs(n,"feFuncR",t);this.#fs(n,"feFuncG",e);this.#fs(n,"feFuncB",i)}#gs(t,e){const i=this.#ns.createElementNS(it,"feComponentTransfer");e.append(i);this.#fs(i,"feFuncA",t)}#us(t){this.#os.style.color=t;return getRGB(getComputedStyle(this.#os).getPropertyValue("color"))}},Ft=e?class NodeStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){return node_utils_fetchData(t)}}:DOMStandardFontDataFactory;function getDocument(t={}){"string"==typeof t||t instanceof URL?t={url:t}:(t instanceof ArrayBuffer||ArrayBuffer.isView(t))&&(t={data:t});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=t.url?function getUrlProp(t){if(t instanceof URL)return t.href;try{return new URL(t,window.location).href}catch{if(e&&"string"==typeof t)return t}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(t.url):null,a=t.data?function getDataProp(t){if(e&&"undefined"!=typeof Buffer&&t instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(t instanceof Uint8Array&&t.byteLength===t.buffer.byteLength)return t;if("string"==typeof t)return stringToBytes(t);if(t instanceof ArrayBuffer||ArrayBuffer.isView(t)||"object"==typeof t&&!isNaN(t?.length))return new Uint8Array(t);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(t.data):null,r=t.httpHeaders||null,o=!0===t.withCredentials,l=t.password??null,h=t.range instanceof PDFDataRangeTransport?t.range:null,d=Number.isInteger(t.rangeChunkSize)&&t.rangeChunkSize>0?t.rangeChunkSize:Dt;let c=t.worker instanceof PDFWorker?t.worker:null;const u=t.verbosity,p="string"!=typeof t.docBaseUrl||isDataScheme(t.docBaseUrl)?null:t.docBaseUrl,g="string"==typeof t.cMapUrl?t.cMapUrl:null,m=!1!==t.cMapPacked,f=t.CMapReaderFactory||Rt,b="string"==typeof t.standardFontDataUrl?t.standardFontDataUrl:null,A=t.StandardFontDataFactory||Ft,w=!0!==t.stopAtErrors,v=Number.isInteger(t.maxImageSize)&&t.maxImageSize>-1?t.maxImageSize:-1,y=!1!==t.isEvalSupported,x="boolean"==typeof t.isOffscreenCanvasSupported?t.isOffscreenCanvasSupported:!e,_="boolean"==typeof t.isImageDecoderSupported?t.isImageDecoderSupported:!e&&(util_FeatureTest.platform.isFirefox||!globalThis.chrome),E=Number.isInteger(t.canvasMaxAreaInBytes)?t.canvasMaxAreaInBytes:-1,S="boolean"==typeof t.disableFontFace?t.disableFontFace:e,C=!0===t.fontExtraProperties,T=!0===t.enableXfa,M=t.ownerDocument||globalThis.document,P=!0===t.disableRange,D=!0===t.disableStream,k=!0===t.disableAutoFetch,R=!0===t.pdfBug,I=t.CanvasFactory||kt,F=t.FilterFactory||It,L=!0===t.enableHWA,O=h?h.length:t.length??NaN,N="boolean"==typeof t.useSystemFonts?t.useSystemFonts:!e&&!S,B="boolean"==typeof t.useWorkerFetch?t.useWorkerFetch:f===DOMCMapReaderFactory&&A===DOMStandardFontDataFactory&&g&&b&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(b,document.baseURI);setVerbosityLevel(u);const H={canvasFactory:new I({ownerDocument:M,enableHWA:L}),filterFactory:new F({docId:s,ownerDocument:M}),cMapReaderFactory:B?null:new f({baseUrl:g,isCompressed:m}),standardFontDataFactory:B?null:new A({baseUrl:b})};if(!c){const t={verbosity:u,port:GlobalWorkerOptions.workerPort};c=t.port?PDFWorker.fromPort(t):new PDFWorker(t);i._worker=c}const z={docId:s,apiVersion:"4.10.38",data:a,password:l,disableAutoFetch:k,rangeChunkSize:d,length:O,docBaseUrl:p,enableXfa:T,evaluatorOptions:{maxImageSize:v,disableFontFace:S,ignoreErrors:w,isEvalSupported:y,isOffscreenCanvasSupported:x,isImageDecoderSupported:_,canvasMaxAreaInBytes:E,fontExtraProperties:C,useSystemFonts:N,cMapUrl:B?g:null,standardFontDataUrl:B?b:null}},U={disableFontFace:S,fontExtraProperties:C,ownerDocument:M,pdfBug:R,styleElement:null,loadingParams:{disableAutoFetch:k,enableXfa:T}};c.promise.then((function(){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const t=c.messageHandler.sendWithPromise("GetDocRequest",z,a?[a.buffer]:null);let l;if(h)l=new PDFDataTransportStream(h,{disableRange:P,disableStream:D});else if(!a){if(!n)throw new Error("getDocument - no `url` parameter provided.");let t;if(e)if(isValidFetchUrl(n)){if("undefined"==typeof fetch||"undefined"==typeof Response||!("body"in Response.prototype))throw new Error("getDocument - the Fetch API was disabled in Node.js, see `--no-experimental-fetch`.");t=PDFFetchStream}else t=PDFNodeStream;else t=isValidFetchUrl(n)?PDFFetchStream:PDFNetworkStream;l=new t({url:n,length:O,httpHeaders:r,withCredentials:o,rangeChunkSize:d,disableRange:P,disableStream:D})}return t.then((t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,l,U,H);i._transport=n;e.send("Ready",null)}))})).catch(i._capability.reject);return i}function isRefProxy(t){return"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0}class PDFDocumentLoadingTask{static#ss=0;constructor(){this._capability=Promise.withResolvers();this._transport=null;this._worker=null;this.docId="d"+PDFDocumentLoadingTask.#ss++;this.destroyed=!1;this.onPassword=null;this.onProgress=null}get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}}class PDFDataRangeTransport{constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s;this._rangeListeners=[];this._progressListeners=[];this._progressiveReadListeners=[];this._progressiveDoneListeners=[];this._readyCapability=Promise.withResolvers()}addRangeListener(t){this._rangeListeners.push(t)}addProgressListener(t){this._progressListeners.push(t)}addProgressiveReadListener(t){this._progressiveReadListeners.push(t)}addProgressiveDoneListener(t){this._progressiveDoneListeners.push(t)}onDataRange(t,e){for(const i of this._rangeListeners)i(t,e)}onDataProgress(t,e){this._readyCapability.promise.then((()=>{for(const i of this._progressListeners)i(t,e)}))}onDataProgressiveRead(t){this._readyCapability.promise.then((()=>{for(const e of this._progressiveReadListeners)e(t)}))}onDataProgressiveDone(){this._readyCapability.promise.then((()=>{for(const t of this._progressiveDoneListeners)t()}))}transportReady(){this._readyCapability.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#bs=null;#As=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._maybeCleanupAfterRender=!1;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=p.ENABLE,transform:n=null,background:a=null,optionalContentConfigPromise:r=null,annotationCanvasMap:l=null,pageColors:h=null,printAnnotationStorage:d=null,isEditing:c=!1}){this._stats?.time("Overall");const u=this._transport.getRenderingIntent(i,s,d,c),{renderingIntent:g,cacheKey:m}=u;this.#As=!1;this.#ws();r||=this._transport.getOptionalContentConfig(g);let f=this._intentStates.get(m);if(!f){f=Object.create(null);this._intentStates.set(m,f)}if(f.streamReaderCancelTimeout){clearTimeout(f.streamReaderCancelTimeout);f.streamReaderCancelTimeout=null}const b=!!(g&o);if(!f.displayReadyCapability){f.displayReadyCapability=Promise.withResolvers();f.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(u)}const complete=t=>{f.renderTasks.delete(A);(this._maybeCleanupAfterRender||b)&&(this.#As=!0);this.#vs(!b);if(t){A.capability.reject(t);this._abortOperatorList({intentState:f,reason:t instanceof Error?t:new Error(t)})}else A.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}},A=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:a},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:f.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!b,pdfBug:this._pdfBug,pageColors:h});(f.renderTasks||=new Set).add(A);const w=A.task;Promise.all([f.displayReadyCapability.promise,r]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&g))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");A.initializeGraphics({transparency:t,optionalContentConfig:e});A.operatorListChanged()}})).catch(complete);return w}getOperatorList({intent:t="display",annotationMode:e=p.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0);let a,r=this._intentStates.get(n.cacheKey);if(!r){r=Object.create(null);this._intentStates.set(n.cacheKey,r)}if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null),lang:null};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{n.lang??=e.lang;Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#As=!1;this.#ws();return Promise.all(t)}cleanup(t=!1){this.#As=!0;const e=this.#vs(!1);t&&e&&(this._stats&&=new StatTimer);return e}#vs(t=!1){this.#ws();if(!this.#As||this.destroyed)return!1;if(t){this.#bs=setTimeout((()=>{this.#bs=null;this.#vs(!1)}),5e3);return!1}for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#As=!1;return!0}#ws(){if(this.#bs){clearTimeout(this.#bs);this.#bs=null}}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{r.read().then((({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}}),(t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#vs(!0)}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class LoopbackPort{#ys=new Map;#xs=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#xs.then((()=>{for(const[t]of this.#ys)t.call(this,i)}))}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#ys.set(e,s)}removeEventListener(t,e){const i=this.#ys.get(e);i?.();this.#ys.delete(e)}terminate(){for(const[,t]of this.#ys)t?.();this.#ys.clear()}}class PDFWorker{static#_s=0;static#Es=!1;static#Ss;static{if(e){this.#Es=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{let i;try{i=new URL(t);if(!i.origin||"null"===i.origin)return!1}catch{return!1}const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;this._readyCapability=Promise.withResolvers();this._port=null;this._webWorker=null;this._messageHandler=null;if(e){if(PDFWorker.#Ss?.has(e))throw new Error("Cannot use more than one PDFWorker per port.");(PDFWorker.#Ss||=new WeakMap).set(e,this);this._initializeFromPort(e)}else this._initialize()}get promise(){return this._readyCapability.promise}#Cs(){this._readyCapability.resolve();this._messageHandler.send("configure",{verbosity:this.verbosity})}get port(){return this._port}get messageHandler(){return this._messageHandler}_initializeFromPort(t){this._port=t;this._messageHandler=new MessageHandler("main","worker",t);this._messageHandler.on("ready",(function(){}));this.#Cs()}_initialize(){if(PDFWorker.#Es||PDFWorker.#Ts){this._setupFakeWorker();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location.href,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this._readyCapability.reject(new Error("Worker was destroyed")):this._setupFakeWorker()},s=new AbortController;e.addEventListener("error",(()=>{this._webWorker||terminateEarly()}),{signal:s.signal});i.on("test",(t=>{s.abort();if(!this.destroyed&&t){this._messageHandler=i;this._port=e;this._webWorker=e;this.#Cs()}else terminateEarly()}));i.on("ready",(t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this._setupFakeWorker()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this._setupFakeWorker()}_setupFakeWorker(){if(!PDFWorker.#Es){warn("Setting up fake worker.");PDFWorker.#Es=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this._readyCapability.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this._port=e;const i="fake"+PDFWorker.#_s++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this._messageHandler=new MessageHandler(i,i+"_worker",e);this.#Cs()})).catch((t=>{this._readyCapability.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;this._webWorker?.terminate();this._webWorker=null;PDFWorker.#Ss?.delete(this._port);this._port=null;this._messageHandler?.destroy();this._messageHandler=null}static fromPort(t){if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");const e=this.#Ss?.get(t.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.fromPort - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#Ts(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#Ts)return this.#Ts;return(await import(this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{#Ms=new Map;#Ps=new Map;#Ds=new Map;#ks=new Map;#Rs=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=Promise.withResolvers();this.setupMessageHandler()}#Is(t,e=null){const i=this.#Ms.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#Ms.set(t,s);return s}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=p.ENABLE,i=null,s=!1,n=!1){let g=r,m=rt;switch(t){case"any":g=a;break;case"display":break;case"print":g=o;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const f=g&o&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case p.DISABLE:g+=d;break;case p.ENABLE:break;case p.ENABLE_FORMS:g+=l;break;case p.ENABLE_STORAGE:g+=h;m=f.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}s&&(g+=c);n&&(g+=u);const{ids:b,hash:A}=f.modifiedIds;return{renderingIntent:g,cacheKey:[g,m.hash,A].join("_"),annotationStorageSerializable:m,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#Rs?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#Ps.values())t.push(e._destroy());this.#Ps.clear();this.#Ds.clear();this.#ks.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#Ms.clear();this.filterFactory.destroy();TextLayer.cleanup();this._networkStream?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{assert(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(async t=>{await this._fullReader.headersReady;const{isStreamingSupported:i,isRangeSupported:s,contentLength:n}=this._fullReader;if(!i||!s){this._lastProgress&&e.onProgress?.(this._lastProgress);this._fullReader.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}return{isStreamingSupported:i,isRangeSupported:s,contentLength:n}}));t.on("GetRangeReader",((t,e)=>{assert(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(t=>{e._capability.reject(wrapReason(t))}));t.on("PasswordRequest",(t=>{this.#Rs=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#Rs.reject(t):this.#Rs.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#Rs.reject(t)}return this.#Rs.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#Ps.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":const{disableFontFace:n,fontExtraProperties:a,pdfBug:r}=this._params;if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const o=r&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,l=new FontFaceObject(s,{disableFontFace:n,fontExtraProperties:a,inspectFont:o});this.fontLoader.bind(l).catch((()=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!a&&l.data&&(l.data=null);this.commonObjs.resolve(e,l)}));break;case"CopyLocalImage":const{imageRef:h}=s;assert(h,"The imageRef must be defined.");for(const t of this.#Ps.values())for(const[,i]of t.objs)if(i?.ref===h){if(!i.dataLen)return null;this.commonObjs.resolve(e,structuredClone(i));return i.dataLen}break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}return null}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#Ps.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":n.objs.resolve(t,s);s?.dataLen>1e7&&(n._maybeCleanupAfterRender=!0);break;case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBuiltInCMap",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");if(!this.cMapReaderFactory)throw new Error("CMapReaderFactory not initialized, see the `useWorkerFetch` parameter.");return this.cMapReaderFactory.fetch(t)}));t.on("FetchStandardFontData",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");if(!this.standardFontDataFactory)throw new Error("StandardFontDataFactory not initialized, see the `useWorkerFetch` parameter.");return this.standardFontDataFactory.fetch(t)}))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#Ds.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((i=>{if(this.destroyed)throw new Error("Transport destroyed");i.refStr&&this.#ks.set(i.refStr,t);const s=new PDFPageProxy(e,i,this,this._params.pdfBug);this.#Ps.set(e,s);return s}));this.#Ds.set(e,s);return s}getPageIndex(t){return isRefProxy(t)?this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}):Promise.reject(new Error("Invalid pageIndex request."))}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#Is("GetFieldObjects")}hasJSActions(){return this.#Is("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#Is("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#Is("GetOptionalContentConfig").then((e=>new OptionalContentConfig(e,t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#Ms.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#Ms.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#Ps.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#Ms.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`;return this.#ks.get(e)??null}}const Lt=Symbol("INITIAL_DATA");class PDFObjects{#Fs=Object.create(null);#Ls(t){return this.#Fs[t]||={...Promise.withResolvers(),data:Lt}}get(t,e=null){if(e){const i=this.#Ls(t);i.promise.then((()=>e(i.data)));return null}const i=this.#Fs[t];if(!i||i.data===Lt)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#Fs[t];return!!e&&e.data!==Lt}delete(t){const e=this.#Fs[t];if(!e||e.data===Lt)return!1;delete this.#Fs[t];return!0}resolve(t,e=null){const i=this.#Ls(t);i.data=e;i.resolve()}clear(){for(const t in this.#Fs){const{data:e}=this.#Fs[t];e?.bitmap?.close()}this.#Fs=Object.create(null)}*[Symbol.iterator](){for(const t in this.#Fs){const{data:e}=this.#Fs[t];e!==Lt&&(yield[t,e])}}}class RenderTask{#Os=null;constructor(t){this.#Os=t;this.onContinue=null}get promise(){return this.#Os.capability.promise}cancel(t=0){this.#Os.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#Os.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#Os;return t.form||t.canvas&&e?.size>0}}class InternalRenderTask{#Ns=null;static#Bs=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#Bs.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#Bs.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#Ns){window.cancelAnimationFrame(this.#Ns);this.#Ns=null}InternalRenderTask.#Bs.delete(this._canvas);this.callback(t||new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e))}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#Ns=window.requestAnimationFrame((()=>{this.#Ns=null;this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#Bs.delete(this._canvas);this.callback()}}}}}const Ot="4.10.38",Nt="f9bea397f";function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}function scaleAndClamp(t){return Math.max(0,Math.min(255,255*t))}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,a=1-i;return["CMYK",s,n,a,Math.min(s,n,a)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(it,t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const a=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){a.value===i.attributes.xfaOn?t.setAttribute("checked",!0):a.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",(t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value){t.setAttribute("value",a.value);for(const t of i.children)t.attributes.value===a.value?t.attributes.selected=!0:t.attributes.hasOwnProperty("selected")&&delete t.attributes.selected}t.addEventListener("input",(t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})}))}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${s}`);for(const[e,i]of Object.entries(a))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}r&&n.addLinkAttributes(t,a.href,a.newWindow);i&&a.dataId&&this.setupStorage(t,a.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",a=document.createElement(s.name);s.attributes&&this.setAttributes({html:a,element:s,intent:n,linkService:i});const r="richText"!==n,o=t.div;o.append(a);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}r&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);a.append(t);r&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,a]];for(;h.length>0;){const[t,s,a]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);a.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);a.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);r&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const Bt=1e3,Ht=new WeakSet;function getRectDims(t){return{width:t[2]-t[0],height:t[3]-t[1]}}class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case S:return new LinkAnnotationElement(t);case E:return new TextAnnotationElement(t);case U:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case H:return new PopupAnnotationElement(t);case C:return new FreeTextAnnotationElement(t);case T:return new LineAnnotationElement(t);case M:return new SquareAnnotationElement(t);case P:return new CircleAnnotationElement(t);case k:return new PolylineAnnotationElement(t);case N:return new CaretAnnotationElement(t);case B:return new InkAnnotationElement(t);case D:return new PolygonAnnotationElement(t);case R:return new HighlightAnnotationElement(t);case I:return new UnderlineAnnotationElement(t);case F:return new SquigglyAnnotationElement(t);case L:return new StrikeOutAnnotationElement(t);case O:return new StampAnnotationElement(t);case z:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#Hs=null;#zs=!1;#Us=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;e&&(this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({titleObj:t,contentsObj:e,richText:i}){return!!(t?.str||e?.str||i?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)}updateEdited(t){if(!this.container)return;this.#Hs||={rect:this.data.rect.slice(0)};const{rect:e}=t;e&&this.#Gs(e);this.#Us?.popup.updateEdited(t)}resetEdited(){if(this.#Hs){this.#Gs(this.#Hs.rect);this.#Us?.popup.resetEdited();this.#Hs=null}}#Gs(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:a,pageX:r,pageY:o}}}}=this;i?.splice(0,4,...t);const{width:l,height:h}=getRectDims(t);e.left=100*(t[0]-r)/n+"%";e.top=100*(a-t[3]+o)/a+"%";if(0===s){e.width=100*l/n+"%";e.height=100*h/a+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||(n.tabIndex=Bt);const{style:a}=n;a.zIndex=this.parent.zIndex++;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:r,height:o}=getRectDims(e.rect);if(!t&&e.borderStyle.width>0){a.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--scale-factor)) / calc(${i}px * var(--scale-factor))`;a.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${r}px * var(--scale-factor)) / calc(${o}px * var(--scale-factor))`;a.borderRadius=t}switch(e.borderStyle.style){case G:a.borderStyle="solid";break;case $:a.borderStyle="dashed";break;case V:warn("Unimplemented border style: beveled");break;case j:warn("Unimplemented border style: inset");break;case W:a.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#zs=!0;a.borderColor=Util.makeHexColor(0|s[0],0|s[1],0|s[2])}else a.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;a.left=100*(l[0]-c)/h+"%";a.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.width=100*r/h+"%";a.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims,{width:n,height:a}=getRectDims(this.data.rect);let r,o;if(t%180==0){r=100*n/i;o=100*a/s}else{r=100*a/i;o=100*n/s}e.style.width=`${r}%`;e.style.height=`${o}%`;e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],a=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](a);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](a)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const a=i[s];if(a){a({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map((t=>Math.fround(t)));if(8===t.length){const[a,r,o,l]=t.subarray(2,6);if(s===a&&n===r&&e===o&&i===l)return}const{style:a}=this.container;let r;if(this.#zs){const{borderColor:t,borderWidth:e}=a;a.borderWidth=0;r=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#zs){r.push("')");a.backgroundImage=r.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(){const{data:t}=this,e=this.#Us=new PopupAnnotationElement({data:{color:t.color,titleObj:t.titleObj,modificationDate:t.modificationDate,contentsObj:t.contentsObj,richText:t.richText,parentRect:t.rect,borderStyle:0,id:`popup_${t.id}`,rotation:t.rotation},parent:this.parent,elements:[this]});this.parent.div.append(e.render())}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:a}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof a?a:null,r=document.querySelector(`[data-element-id="${n}"]`);!r||Ht.has(r)?i.push({id:n,exportValue:s,domElement:r}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(Ht.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",(()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e})}))}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action);s=!0}else if(t.attachment){this.#$s(i,t.attachment,t.attachmentDest);s=!0}else if(t.setOCGState){this.#Vs(i,t.setOCGState);s=!0}else if(t.dest){this._bindLink(i,t.dest);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");s&&this.container.append(i);return this.container}#js(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#js()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#js()}#$s(t,e,i=null){t.href=this.linkService.getAnchorUrl("");e.description&&(t.title=e.description);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,i);return!1};this.#js()}#Vs(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#js()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}t.onclick||(t.onclick=()=>!1);this.#js()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#js();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,a=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&a.push(i)}else for(const t of Object.values(this._fieldObjects))a.push(...t);const r=this.annotationStorage,o=[];for(const t of a){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;r.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(Ht.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));!this.data.popupRef&&this.hasPopupData&&this._createPopup();this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return util_FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(i,(t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,i,s){for(const[n,a]of i)if("Action"===a||this.data.actions?.[a]){"Focus"!==a&&"Blur"!==a||(e||={focused:!1});this._setEventListener(t,e,n,a,s);"Focus"!==a||this.data.actions?.Blur?"Blur"!==a||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:i}=this.data.defaultAppearanceData,s=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(n*s))||1);r=Math.min(s,roundToOneDecimal(e/n))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(s,roundToOneDecimal(t/n))}a.fontSize=`calc(${r}px * var(--scale-factor))`;a.color=Util.makeHexColor(i[0],i[1],i[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=i);n.setValue(a.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&n.length>a&&(n=n.slice(0,a));let r=s.formattedValue||this.data.textContent?.join("\n")||null;r&&this.data.comb&&(r=r.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:r,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=r??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type="text";i.setAttribute("value",r??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);Ht.add(i);i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=Bt;this._setRequired(i,this.data.required);a&&(i.maxLength=a);i.addEventListener("input",(s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null}));i.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",(t=>{if(o.focused)return;const{target:e}=t;o.userValue&&(e.value=o.userValue);o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)}));i.addEventListener("updatefromsandbox",(i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);t.setValue(e,{formattedValue:s})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let a=o.userValue;if(a&&!(a.length<=s)){a=a.slice(0,s);n.value=o.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)}));i.addEventListener("keydown",(t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const s=blurListener;blurListener=null;i.addEventListener("blur",(t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{value:i}=t.target;o.userValue=i;o.lastCommittedValue!==i&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)}));this.data.actions?.Keystroke&&i.addEventListener("beforeinput",(t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:a,selectionEnd:r}=s;let l=a,h=r;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(a).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":a===r&&(l-=1);break;case"deleteContentForward":a===r&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})}));this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");Ht.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=Bt;n.addEventListener("change",(s=>{const{name:n,checked:a}=s.target;for(const s of this._getElementsByName(n,i)){const i=a&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:a})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue}));if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");Ht.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=Bt;n.addEventListener("change",(e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",(e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");Ht.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=Bt;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let a=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);a=()=>{t.remove();s.removeEventListener("input",a);a=null};s.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,(t=>t.selected)).map((t=>t[e])):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",(i=>{const n={value(i){a?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,a=i.detail.remove;n[a].selected=!1;s.remove(a);if(n.length>0){-1===Array.prototype.findIndex.call(n,(t=>t.selected))&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(i){const{index:n,displayValue:a,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");h.textContent=a;h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");n.textContent=e;n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)}));s.addEventListener("input",(i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],(t=>t.target.value))}else s.addEventListener("input",(function(i){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i}=t;super(t,{isRenderable:AnnotationElement._hasPopupData(e)});this.elements=i;this.popup=null}render(){this.container.classList.add("popupAnnotation");const t=this.popup=new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open}),e=[];for(const i of this.elements){i.popup=t;i.container.ariaHasPopup="dialog";e.push(i.data.id);i.addHighlightArea()}this.container.setAttribute("aria-controls",e.map((t=>`${et}${t}`)).join(","));return this.container}}class PopupElement{#Ws=this.#qs.bind(this);#Xs=this.#Ks.bind(this);#Ys=this.#Qs.bind(this);#Js=this.#Zs.bind(this);#tn=null;#pt=null;#en=null;#in=null;#sn=null;#nn=null;#an=null;#rn=!1;#on=null;#C=null;#ln=null;#hn=null;#dn=null;#Hs=null;#cn=!1;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:a,richText:r,parent:o,rect:l,parentRect:h,open:d}){this.#pt=t;this.#dn=s;this.#en=a;this.#hn=r;this.#nn=o;this.#tn=e;this.#ln=l;this.#an=h;this.#sn=i;this.#in=PDFDateString.toDateObject(n);this.trigger=i.flatMap((t=>t.getElementsToTriggerPopup()));for(const t of this.trigger){t.addEventListener("click",this.#Js);t.addEventListener("mouseenter",this.#Ys);t.addEventListener("mouseleave",this.#Xs);t.classList.add("popupTriggerArea")}for(const t of i)t.container?.addEventListener("keydown",this.#Ws);this.#pt.hidden=!0;d&&this.#Zs()}render(){if(this.#on)return;const t=this.#on=document.createElement("div");t.className="popup";if(this.#tn){const e=t.style.outlineColor=Util.makeHexColor(...this.#tn);if(CSS.supports("background-color","color-mix(in srgb, red 30%, white)"))t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`;else{const e=.7;t.style.backgroundColor=Util.makeHexColor(...this.#tn.map((t=>Math.floor(e*(255-t)+t))))}}const e=document.createElement("span");e.className="header";const i=document.createElement("h1");e.append(i);({dir:i.dir,str:i.textContent}=this.#dn);t.append(e);if(this.#in){const t=document.createElement("span");t.classList.add("popupDate");t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#in.valueOf()}));e.append(t)}const s=this.#un;if(s){XfaLayer.render({xfaHtml:s,intent:"richText",div:t});t.lastChild.classList.add("richText","popupContent")}else{const e=this._formatContents(this.#en);t.append(e)}this.#pt.append(t)}get#un(){const t=this.#hn,e=this.#en;return!t?.str||e?.str&&e.str!==t.str?null:this.#hn.html||null}get#pn(){return this.#un?.attributes?.style?.fontSize||0}get#gn(){return this.#un?.attributes?.style?.color||null}#mn(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#gn,fontSize:this.#pn?`calc(${this.#pn}px * var(--scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}_formatContents({str:t,dir:e}){const i=document.createElement("p");i.classList.add("popupContent");i.dir=e;const s=t.split(/(?:\r\n?|\n)/);for(let t=0,e=s.length;t=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#xn.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#Sn()}));!e.popupRef&&this.hasPopupData?this._createPopup():i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#En}addHighlightArea(){this.container.classList.add("highlightArea")}#Sn(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Cn=null;#Tn=null;#Mn=new Map;#Pn=null;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:a,structTreeLayer:r}){this.div=t;this.#Cn=e;this.#Tn=i;this.#Pn=r||null;this.page=n;this.viewport=a;this.zIndex=0;this._annotationEditorUIManager=s}hasEditableAnnotations(){return this.#Mn.size>0}async#Dn(t,e){const i=t.firstChild||t,s=i.id=`${et}${e}`,n=await(this.#Pn?.getAriaAttributes(s));if(n)for(const[t,e]of n)i.setAttribute(t,e);this.div.append(t);this.#Cn?.moveElementInDOM(this.div,t,i,!1)}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n={data:null,layer:i,linkService:t.linkService,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:t.annotationStorage||new AnnotationStorage,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===H;if(e){const e=s.get(t.id);if(!e)continue;n.elements=e}else{const{width:e,height:i}=getRectDims(t.rect);if(e<=0||i<=0)continue}n.data=t;const i=AnnotationElementFactory.create(n);if(!i.isRenderable)continue;if(!e&&t.popupRef){const e=s.get(t.popupRef);e?e.push(i):s.set(t.popupRef,[i])}const a=i.render();t.hidden&&(a.style.visibility="hidden");await this.#Dn(a,t.id);if(i._isEditable){this.#Mn.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}this.#kn()}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#kn();e.hidden=!1}#kn(){if(!this.#Tn)return;const t=this.div;for(const[e,i]of this.#Tn){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i)}this.#Tn.clear()}getEditableAnnotations(){return Array.from(this.#Mn.values())}getEditableAnnotation(t){return this.#Mn.get(t)}}const zt=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#tn;#Rn="";#In=`${this.id}-editor`;#Fn=null;#pn;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=g.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.#tn=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#pn=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case m.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case m.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case m.FREETEXT_SIZE:this.#Ln(e);break;case m.FREETEXT_COLOR:this.#On(e)}}static get defaultPropertiesToUpdate(){return[[m.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[m.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[m.FREETEXT_SIZE,this.#pn],[m.FREETEXT_COLOR,this.#tn]]}#Ln(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--scale-factor))`;this.translate(0,-(t-this.#pn)*this.parentScale);this.#pn=t;this.#Nn()},e=this.#pn;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#On(t){const setColor=t=>{this.#tn=this.editorDiv.style.color=t},e=this.#tn;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#pn)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(this.isInEditMode())return;this.parent.setEditingState(!1);this.parent.updateToolbar(g.FREETEXT);super.enableEditMode();this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#Fn=new AbortController;const t=this._uiManager.combinedSignal(this.#Fn);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t})}disableEditMode(){if(this.isInEditMode()){this.parent.setEditingState(!0);super.disableEditMode();this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#In);this._isDraggable=!0;this.#Fn?.abort();this.#Fn=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing")}}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#Bn(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#Hn(i));e=i}return t.join("\n")}#Nn(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#Rn,e=this.#Rn=this.#Bn().trimEnd();if(t===e)return;const setText=t=>{this.#Rn=t;if(t){this.#zn();this._uiManager.rebuild(this);this.#Nn()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#Nn()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}dblclick(t){this.enterInEditMode()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#In);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#pn}px * var(--scale-factor))`;i.color=this.#tn;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);bindEvents(this,this.div,["dblclick","keydown"]);if(this.width){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[a,r]=this.getInitialTranslation();[a,r]=this.pageTranslationToScreen(a,r);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[r,-a];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[-a,-r];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[a,r]=[-r,a]}this.setAt(c*i,u*s,a,r)}else this.setAt(t*i,e*s,this.width*i,this.height*s);this.#zn();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#Hn(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(zt,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#Un(e.getData("text")||"").replaceAll(zt,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const a=n.getRangeAt(0);if(!s.includes("\n")){a.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:r,startOffset:o}=a,l=[],h=[];if(r.nodeType===Node.TEXT_NODE){const t=r.parentElement;h.push(r.nodeValue.slice(o).replaceAll(zt,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#Hn(i)):e=h}l.push(r.nodeValue.slice(0,o).replaceAll(zt,""))}else if(r===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#Hn(i))}}this.#Rn=`${l.join("\n")}${s}${h.join("\n")}`;this.#zn();const d=new Range;let c=l.reduce(((t,e)=>t+e.length),0);for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#zn(){this.editorDiv.replaceChildren();if(this.#Rn)for(const t of this.#Rn.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Gn(){return this.#Rn.replaceAll(" "," ")}static#Un(t){return t.replaceAll(" "," ")}get contentDiv(){return this.editorDiv}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:a,id:r,popupRef:o},textContent:l,textPosition:h,parent:{page:{pageNumber:d}}}=t;if(!l||0===l.length)return null;s=t={annotationType:g.FREETEXT,color:Array.from(i),fontSize:e,value:l.join("\n"),position:h,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,popupRef:o}}const n=await super.deserialize(t,e,i);n.#pn=t.fontSize;n.#tn=Util.makeHexColor(...t.color);n.#Rn=FreeTextEditor.#Un(t.value);n.annotationElementId=t.id||null;n._initialData=s;return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=FreeTextEditor._internalPadding*this.parentScale,i=this.getRect(e,e),s=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#tn),n={annotationType:g.FREETEXT,color:s,fontSize:this.#pn,value:this.#Gn(),pageIndex:this.pageIndex,rect:i,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t)return n;if(this.annotationElementId&&!this.#$n(n))return null;n.id=this.annotationElementId;return n}#$n(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some(((t,e)=>t!==s[e]))||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(this.deleted)return e;const{style:i}=e;i.fontSize=`calc(${this.#pn}px * var(--scale-factor))`;i.color=this.#tn;e.replaceChildren();for(const t of this.#Rn.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}const s=FreeTextEditor._internalPadding*this.parentScale;t.updateEdited({rect:this.getRect(s,s),popupContent:this.#Rn});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,a){a||=new Float32Array(t.length);for(let r=0,o=t.length;r=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#ha(i);return i.join(" ")}#oa(){const[t,e,i,s]=this.#Vn,[n,a,r,o]=this.#ra();return`M${(this.#Kn[2]-t)/i} ${(this.#Kn[3]-e)/s} L${(this.#Kn[4]-t)/i} ${(this.#Kn[5]-e)/s} L${n} ${a} L${r} ${o} L${(this.#Kn[16]-t)/i} ${(this.#Kn[17]-e)/s} L${(this.#Kn[14]-t)/i} ${(this.#Kn[15]-e)/s} Z`}#ha(t){const e=this.#jn;t.push(`L${e[4]} ${e[5]} Z`)}#la(t){const[e,i,s,n]=this.#Vn,a=this.#Kn.subarray(4,6),r=this.#Kn.subarray(16,18),[o,l,h,d]=this.#ra();t.push(`L${(a[0]-e)/s} ${(a[1]-i)/n} L${o} ${l} L${h} ${d} L${(r[0]-e)/s} ${(r[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,a){return new FreeDrawOutline(t,e,i,s,n,a)}getOutlines(){const t=this.#Xn,e=this.#jn,i=this.#Kn,[s,n,a,r]=this.#Vn,o=new Float32Array((this.#ia?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#ua(l,h);return this.newFreeDrawOutline(l,o,this.#Vn,this.#ta,this.#Wn,this.#qn)}#da(t){const e=this.#Kn,[i,s,n,a]=this.#Vn,[r,o,l,h]=this.#ra(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/a,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/a,NaN,NaN,NaN,NaN,r,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/a,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/a],0);return this.newFreeDrawOutline(d,t,this.#Vn,this.#ta,this.#Wn,this.#qn)}#ua(t,e){const i=this.#jn;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#ca(t,e){const i=this.#Kn.subarray(4,6),s=this.#Kn.subarray(16,18),[n,a,r,o]=this.#Vn,[l,h,d,c]=this.#ra();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/r,(i[1]-a)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/r,(s[1]-a)/o],e);return e+24}}class FreeDrawOutline extends Outline{#Vn;#pa=new Float32Array(4);#Wn;#qn;#ia;#ta;#ga;constructor(t,e,i,s,n,a){super();this.#ga=t;this.#ia=e;this.#Vn=i;this.#ta=s;this.#Wn=n;this.#qn=a;this.lastPoint=[NaN,NaN];this.#ma(a);const[r,o,l,h]=this.#pa;for(let e=0,i=t.length;et[0]-e[0]||t[1]-e[1]||t[2]-e[2]));const t=[];for(const e of this.#ba)if(e[3]){t.push(...this.#wa(e));this.#va(e)}else{this.#ya(e);t.push(...this.#wa(e))}return this.#xa(t)}#xa(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort(((t,e)=>t[1]-e[1]||t[0]-e[0]));for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,a,r,o,l]=t;i.delete(t);let h=e,d=a;n=[e,r];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,a,r,o,l]=t;if(h!==e){n.push(h,d,e,d===a?a:r);h=e}d=d===a?r:a}n.push(h,d)}return new HighlightOutline(s,this.#Vn,this.#fa)}#_a(t){const e=this.#Aa;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,a=e[n][0];if(a===t)return n;a=0;s--){const[i,n]=this.#Aa[s];if(i!==t)break;if(i===t&&n===e){this.#Aa.splice(s,1);return}}}#wa(t){const[e,i,s]=t,n=[[e,i,s]],a=this.#_a(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===a)return[];n.splice(t,1);t--;a--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#Vn;#Ea;constructor(t,e,i){super();this.#Ea=t;this.#Vn=e;this.lastPoint=i}toSVGPath(){const t=[];for(const e of this.#Ea){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#Xa=!0;this.#Za(t);this.#tr()}else if(this.#Ua){this.#Ha=t.anchorNode;this.#za=t.anchorOffset;this.#Va=t.focusNode;this.#ja=t.focusOffset;this.#er();this.#tr();this.rotate(this.rotation)}}get telemetryInitialData(){return{action:"added",type:this.#Xa?"free_highlight":"highlight",color:this._uiManager.highlightColorNames.get(this.color),thickness:this.#ea,methodOfCreation:this.#Ja}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.highlightColorNames.get(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#er(){const t=new HighlightOutliner(this.#Ua,.001);this.#qa=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#qa.box;const e=new HighlightOutliner(this.#Ua,.0025,.001,"ltr"===this._uiManager.direction);this.#$a=e.getOutlines();const{lastPoint:i}=this.#$a;this.#fa=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height]}#Za({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#qa=t;this.#$a=t.getNewOutline(this.#ea/2+1.5,.0025);if(e>=0){this.#w=e;this.#Ga=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#Ya=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#$a.box,path:{d:this.#$a.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#w,{bbox:HighlightEditor.#ir(this.#qa.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#Ya,{bbox:HighlightEditor.#ir(this.#$a.box,e),path:{d:this.#$a.toSVGPath()}})}const[s,n,a,r]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=a;this.height=r;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=a*e/t;this.height=r*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=a;this.height=r;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=a*e/t;this.height=r*t/e;break}}const{lastPoint:o}=this.#$a;this.#fa=[(o[0]-s)/a,(o[1]-n)/r]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case m.HIGHLIGHT_DEFAULT_COLOR:HighlightEditor._defaultColor=e;break;case m.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#fa}updateParams(t,e){switch(t){case m.HIGHLIGHT_COLOR:this.#On(e);break;case m.HIGHLIGHT_THICKNESS:this.#sr(e)}}static get defaultPropertiesToUpdate(){return[[m.HIGHLIGHT_DEFAULT_COLOR,HighlightEditor._defaultColor],[m.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[m.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[m.HIGHLIGHT_THICKNESS,this.#ea||HighlightEditor._defaultThickness],[m.HIGHLIGHT_FREE,this.#Xa]]}#On(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.#Ka=e;this.parent?.drawLayer.updateProperties(this.#w,{root:{fill:t,"fill-opacity":e}});this.#n?.updateColor(t)},e=this.color,i=this.#Ka;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.highlightColorNames.get(t)},!0)}#sr(t){const e=this.#ea,setThickness=t=>{this.#ea=t;this.#nr(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:m.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.highlightColors){this.#n=new ColorPicker({editor:this});t.addColorPicker(this.#n)}return t}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#ar())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#ar())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#rr();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#tr();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#rr();else if(t){this.#tr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#nr(t){if(!this.#Xa)return;this.#Za({highlightOutlines:this.#qa.getNewOutline(t/2)});this.fixAndSetPosition();const[e,i]=this.parentDimensions;this.setDims(this.width*e,this.height*i)}#rr(){if(null!==this.#w&&this.parent){this.parent.drawLayer.remove(this.#w);this.#w=null;this.parent.drawLayer.remove(this.#Ya);this.#Ya=null}}#tr(t=this.parent){if(null===this.#w){({id:this.#w,clipPathId:this.#Ga}=t.drawLayer.draw({bbox:this.#qa.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.#Ka},rootClass:{highlight:!0,free:this.#Xa},path:{d:this.#qa.toSVGPath()}},!1,!0));this.#Ya=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#Xa},bbox:this.#$a.box,path:{d:this.#$a.toSVGPath()}},this.#Xa);this.#Wa&&(this.#Wa.style.clipPath=this.#Ga)}}static#ir([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#Xa){t=(t-this.rotation+360)%360;i=HighlightEditor.#ir(this.#qa.box,t)}else i=HighlightEditor.#ir([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#w,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#Ya,{bbox:HighlightEditor.#ir(this.#$a.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#Qa){t.setAttribute("aria-label",this.#Qa);t.setAttribute("role","mark")}this.#Xa?t.classList.add("free"):this.div.addEventListener("keydown",this.#or.bind(this),{signal:this._uiManager._signal});const e=this.#Wa=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Ga;const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);bindEvents(this,this.#Wa,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!1}})}#or(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#lr(!0);break;case 1:case 3:this.#lr(!1)}}#lr(t){if(!this.#Ha)return;const e=window.getSelection();t?e.setPosition(this.#Ha,this.#za):e.setPosition(this.#Va,this.#ja)}select(){super.select();this.#Ya&&this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#Ya){this.parent?.drawLayer.updateProperties(this.#Ya,{rootClass:{selected:!1}});this.#Xa||this.#lr(!1)}}get _mustFixPosition(){return!this.#Xa}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#w,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#Ya,{rootClass:{hidden:!t}})}}#ar(){return this.#Xa?this.rotation:0}#hr(){if(this.#Xa)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#Ua,a=new Float32Array(8*n.length);let r=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;a[r]=a[r+4]=n;a[r+1]=a[r+3]=c;a[r+2]=a[r+6]=n+h*t;a[r+5]=a[r+7]=c-d*e;r+=8}return a}#dr(t){return this.#qa.serialize(t,this.#ar())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:a,y:r,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#cr(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#ur.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[a,r,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#ur(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#cr(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:a,color:r,opacity:o,popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:g.HIGHLIGHT,color:Array.from(r),opacity:o,quadPoints:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:a,color:r,borderStyle:{rawWidth:o},popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:g.HIGHLIGHT,color:Array.from(r),thickness:o,inkLists:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}const{color:n,quadPoints:a,inkLists:r,opacity:o}=t,l=await super.deserialize(t,e,i);l.color=Util.makeHexColor(...n);l.#Ka=o||1;r&&(l.#ea=t.thickness);l.annotationElementId=t.id||null;l._initialData=s;const[h,d]=l.pageDimensions,[c,u]=l.pageTranslation;if(a){const t=l.#Ua=[];for(let e=0;et!==e[i]))}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#pr=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))this.updateProperty(e,i)}updateSVGProperty(t,e){this.#pr[t]=e}toSVGProperties(){const t=this.#pr;this.#pr=Object.create(null);return{root:t}}reset(){this.#pr=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#gr=null;#mr;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#fr=null;static#br=null;static#Ar=null;static#wr=NaN;static#vr=null;static#yr=null;static#xr=NaN;static _INNER_MARGIN=3;constructor(t){super(t);this.#mr=t.mustBeCommitted||!1;if(t.drawOutlines){this.#_r(t);this.#tr()}}#_r({drawOutlines:t,drawId:e,drawingOptions:i}){this.#gr=t;this._drawingOptions||=i;if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#Er(t,this.parent);this.#Sr(t.box)}#Er(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#fr.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=t=>{s.updateProperty(e,t);const i=this.#gr.updateProperty(e,t);i&&this.#Sr(i);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties())};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathResizingSVGProperties(this.#Cr()),{bbox:this.#Tr()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathResizedSVGProperties(this.#Cr()),{bbox:this.#Tr()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#Tr(t,e)})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#gr.getPathTranslatedSVGProperties(this.#Cr(),this.parentDimensions),{bbox:this.#Tr()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#mr){this.#mr=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#rr();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#tr();this.#Sr(this.#gr.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#rr()}else if(t){this._uiManager.addShouldRescale(this);this.#tr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#rr(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#tr(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#Er(this.#gr,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#Mr([t,e,i,s]){const{parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[e,1-t,i*(a/n),s*(n/a)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(a/n),s*(n/a)];default:return[t,e,i,s]}}#Cr(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[1-e,t,i*(n/a),s*(a/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/a),s*(a/n)];default:return[t,e,i,s]}}#Sr(t){[this.x,this.y,this.width,this.height]=this.#Mr(t);if(this.div){this.fixAndSetPosition();const[t,e]=this.parentDimensions;this.setDims(this.width*t,this.height*e)}this._onResized()}#Tr(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:a,parentDimensions:[r,o]}=this;switch((4*n+a)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(r/o),s*(o/r),i*(r/o)];case 5:return[1-e,t,i*(r/o),s*(o/r)];case 6:return[1-t-s*(o/r),1-e,s*(o/r),i*(r/o)];case 7:return[e-i*(r/o),1-t-s*(o/r),i*(r/o),s*(o/r)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/r),e,s*(o/r),i*(r/o)];case 13:return[1-e-i*(r/o),t-s*(o/r),i*(r/o),s*(o/r)];case 14:return[1-t,1-e-i*(r/o),s*(o/r),i*(r/o)];case 15:return[e,1-t,i*(r/o),s*(o/r)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#Tr()},this.#gr.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#Sr(this.#gr.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;const t=super.render();t.classList.add("draw");const e=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);this._uiManager.addShouldRescale(this);this.disableEditing();return t}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:a,offsetY:r,pointerId:o,pointerType:l}=s;if(DrawingEditor.#vr&&DrawingEditor.#vr!==l)return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#br=new AbortController,p=t.combinedSignal(u);DrawingEditor.#wr||=o;DrawingEditor.#vr??=l;window.addEventListener("pointerup",(t=>{DrawingEditor.#wr===t.pointerId?this._endDraw(t):DrawingEditor.#yr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointercancel",(t=>{DrawingEditor.#wr===t.pointerId?this._currentParent.endDrawingSession():DrawingEditor.#yr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointerdown",(t=>{if(DrawingEditor.#vr===t.pointerType){(DrawingEditor.#yr||=new Set).add(t.pointerId);if(DrawingEditor.#fr.isCancellable()){DrawingEditor.#fr.removeLastElement();DrawingEditor.#fr.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}}),{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",(t=>{t.timeStamp===DrawingEditor.#xr&&stopEvent(t)}),{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#fr)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.startNew(a,r,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#fr=this.createDrawerInstance(a,r,d,c,h);DrawingEditor.#Ar=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#Ar.toSVGProperties(),DrawingEditor.#fr.defaultSVGProperties),!0,!1))}}static _drawMove(t){DrawingEditor.#xr=-1;if(!DrawingEditor.#fr)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(DrawingEditor.#wr===s)if(DrawingEditor.#yr?.size>=1)this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.add(e,i));DrawingEditor.#xr=t.timeStamp;stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#fr=null;DrawingEditor.#Ar=null;DrawingEditor.#vr=null;DrawingEditor.#xr=NaN}if(DrawingEditor.#br){DrawingEditor.#br.abort();DrawingEditor.#br=null;DrawingEditor.#wr=NaN;DrawingEditor.#yr=null}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#fr.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#fr,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:m.DRAW_STEP})}else this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(m.DRAW_STEP);if(!DrawingEditor.#fr.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,a=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#fr.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#Ar,mustBeCommitted:!t});this._cleanup(!0);return a}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,a){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=e.viewport,o=this.deserializeDraw(a,r,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#_r({drawOutlines:o});l.#tr();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#gr.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#Kn=new Float64Array(6);#bn;#Pr;#Fi;#ea;#ia;#Dr="";#kr=0;#Ea=new InkDrawOutline;#Rr;#Ir;constructor(t,e,i,s,n,a){this.#Rr=i;this.#Ir=s;this.#Fi=n;this.#ea=a;[t,e]=this.#Fr(t,e);const r=this.#bn=[NaN,NaN,NaN,NaN,t,e];this.#ia=[t,e];this.#Pr=[{line:r,points:this.#ia}];this.#Kn.set(r,0)}updateProperty(t,e){"stroke-width"===t&&(this.#ea=e)}#Fr(t,e){return Outline._normalizePoint(t,e,this.#Rr,this.#Ir,this.#Fi)}isEmpty(){return!this.#Pr||0===this.#Pr.length}isCancellable(){return this.#ia.length<=10}add(t,e){[t,e]=this.#Fr(t,e);const[i,s,n,a]=this.#Kn.subarray(2,6),r=t-n,o=e-a;if(Math.hypot(this.#Rr*r,this.#Ir*o)<=2)return null;this.#ia.push(t,e);if(isNaN(i)){this.#Kn.set([n,a,t,e],2);this.#bn.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#Kn[0])&&this.#bn.splice(6,6);this.#Kn.set([i,s,n,a,t,e],0);this.#bn.push(...Outline.createBezierPoints(i,s,n,a,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#ia.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#Rr=i;this.#Ir=s;this.#Fi=n;[t,e]=this.#Fr(t,e);const a=this.#bn=[NaN,NaN,NaN,NaN,t,e];this.#ia=[t,e];const r=this.#Pr.at(-1);if(r){r.line=new Float32Array(r.line);r.points=new Float32Array(r.points)}this.#Pr.push({line:a,points:this.#ia});this.#Kn.set(a,0);this.#kr=0;this.toSVGPath();return null}getLastElement(){return this.#Pr.at(-1)}setLastElement(t){if(!this.#Pr)return this.#Ea.setLastElement(t);this.#Pr.push(t);this.#bn=t.line;this.#ia=t.points;this.#kr=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#Pr)return this.#Ea.removeLastElement();this.#Pr.pop();this.#Dr="";for(let t=0,e=this.#Pr.length;tt??NaN)),d,c,u,p),points:g(r[t].map((t=>t??NaN)),d,c,u,p)});const m=new InkDrawOutline;m.build(h,i,s,1,o,l,n);return m}#Hr(t=this.#ea){const e=this.#Wn+t/2*this.#Or;return this.#Fi%180==0?[e/this.#Rr,e/this.#Ir]:[e/this.#Ir,e/this.#Rr]}#Br(){const[t,e,i,s]=this.#pa,[n,a]=this.#Hr(0);return[t+n,e+a,i-2*n,s-2*a]}#Nr(){const t=this.#pa=new Float32Array([1/0,1/0,-1/0,-1/0]);for(const{line:e}of this.#Pr){if(e.length<=12){for(let i=4,s=e.length;it!==e[i]))||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e});return null}}class StampEditor extends AnnotationEditor{#Ur=null;#Gr=null;#$r=null;#Vr=null;#jr=null;#Wr="";#qr=null;#Xr=null;#Kr=!1;#Yr=!1;static _type="stamp";static _editorType=g.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#Vr=t.bitmapUrl;this.#jr=t.bitmapFile}static initialize(t,e){AnnotationEditor.initialize(t,e)}static get supportedTypes(){return shadow(this,"supportedTypes",["apng","avif","bmp","gif","jpeg","png","svg+xml","webp","x-icon"].map((t=>`image/${t}`)))}static get supportedTypesStr(){return shadow(this,"supportedTypesStr",this.supportedTypes.join(","))}static isHandlingMimeForPasting(t){return this.supportedTypes.includes(t)}static paste(t,e){e.pasteEditor(g.STAMP,{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#Qr(t,e=!1){if(t){this.#Ur=t.bitmap;if(!e){this.#Gr=t.id;this.#Kr=t.isSvg}t.file&&(this.#Wr=t.file.name);this.#Jr()}else this.remove()}#Zr(){this.#$r=null;this._uiManager.enableWaiting(!1);if(this.#qr)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#Ur){this._editToolbar.hide();this._uiManager.editAltText(this,!0)}else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#Ur){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:a}=t||this.copyCanvas(null,null,!0).imageData,r=await i.guess({name:"altText",request:{data:s,width:n,height:a,channels:s.length/(n*a)}});if(!r)throw new Error("No response from the AI service.");if(r.error)throw new Error("Error from the AI service.");if(r.cancel)return null;if(!r.output)throw new Error("No valid response from the AI service.");const o=r.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#to(){if(this.#Gr){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#Gr).then((t=>this.#Qr(t,!0))).finally((()=>this.#Zr()));return}if(this.#Vr){const t=this.#Vr;this.#Vr=null;this._uiManager.enableWaiting(!0);this.#$r=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#Qr(t))).finally((()=>this.#Zr()));return}if(this.#jr){const t=this.#jr;this.#jr=null;this._uiManager.enableWaiting(!0);this.#$r=this._uiManager.imageManager.getFromFile(t).then((t=>this.#Qr(t))).finally((()=>this.#Zr()));return}const t=document.createElement("input");t.type="file";t.accept=StampEditor.supportedTypesStr;const e=this._uiManager._signal;this.#$r=new Promise((i=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#Qr(e)}else this.remove();i()}),{signal:e});t.addEventListener("cancel",(()=>{this.remove();i()}),{signal:e})})).finally((()=>this.#Zr()));t.click()}remove(){if(this.#Gr){this.#Ur=null;this._uiManager.imageManager.deleteId(this.#Gr);this.#qr?.remove();this.#qr=null;if(this.#Xr){clearTimeout(this.#Xr);this.#Xr=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#Gr&&null===this.#qr&&this.#to();this.isAttachedToDOM||this.parent.add(this)}}else this.#Gr&&this.#to()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#$r||this.#Ur||this.#Vr||this.#jr||this.#Gr)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.div.setAttribute("role","figure");this.addAltTextButton();this.#Ur?this.#Jr():this.#to();if(this.width&&!this.annotationElementId){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s)}this._uiManager.addShouldRescale(this);return this.div}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#Xr&&clearTimeout(this.#Xr);this.#Xr=setTimeout((()=>{this.#Xr=null;this.#eo()}),200)}#Jr(){const{div:t}=this;let{width:e,height:i}=this.#Ur;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#qr=document.createElement("canvas");l.setAttribute("role","img");this.addContainer(l);this.width=e/s;this.height=i/n;this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#eo();if(!this.#Yr){this.parent.addUndoableEditor(this);this.#Yr=!0}this._reportTelemetry({action:"inserted_image"});this.#Wr&&l.setAttribute("aria-label",this.#Wr)}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#Ur,a=new OutputScale;let r=this.#Ur,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*a.sx),i=h.height=Math.ceil(l*a.sy);this.#Kr||(r=this.#io(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(window.matchMedia?.("(prefers-color-scheme: dark)").matches){c="#8f8f9d";u="#42414d"}const p=15,g=p*a.sx,m=p*a.sy,f=new OffscreenCanvas(2*g,2*m),b=f.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*m);b.fillStyle=u;b.fillRect(0,0,g,m);b.fillRect(g,m,g,m);d.fillStyle=d.createPattern(f,"repeat");d.fillRect(0,0,t,i);d.drawImage(r,0,0,r.width,r.height,0,0,t,i)}let d=null;if(i){let e,i;if(a.symmetric&&r.widtht||n>t){const a=Math.min(t/s,t/n);e=Math.floor(s*a);i=Math.floor(n*a);this.#Kr||(r=this.#io(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(r,0,0,r.width,r.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#io(t,e){const{width:i,height:s}=this.#Ur;let n=i,a=s,r=this.#Ur;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#eo(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,a=Math.ceil(i*t*n.sx),r=Math.ceil(s*e*n.sy),o=this.#qr;if(!o||o.width===a&&o.height===r)return;o.width=a;o.height=r;const l=this.#Kr?this.#Ur:this.#io(a,r),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,a,r)}getImageForAltText(){return this.#qr}#so(t){if(t){if(this.#Kr){const t=this._uiManager.imageManager.getSvgUrl(this.#Gr);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#Ur);t.getContext("2d").drawImage(this.#Ur,0,0);return t.toDataURL()}if(this.#Kr){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#Ur,0,0,this.#Ur.width,this.#Ur.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#Ur)}static async deserialize(t,e,i){let s=null;if(t instanceof StampAnnotationElement){const{data:{rect:n,rotation:a,id:r,structParent:o,popupRef:l},container:h,parent:{page:{pageNumber:d}}}=t,c=h.querySelector("canvas"),u=i.imageManager.getFromCanvas(h.id,c);c.remove();const p=(await e._structTree.getAriaAttributes(`${et}${r}`))?.get("aria-label")||"";s=t={annotationType:g.STAMP,bitmapId:u.id,bitmap:u.bitmap,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,accessibilityData:{decorative:!1,altText:p},isSvg:!1,structParent:o,popupRef:l}}const n=await super.deserialize(t,e,i),{rect:a,bitmap:r,bitmapUrl:o,bitmapId:l,isSvg:h,accessibilityData:d}=t;if(l&&i.imageManager.isValidId(l)){n.#Gr=l;r&&(n.#Ur=r)}else n.#Vr=o;n.#Kr=h;const[c,u]=n.pageDimensions;n.width=(a[2]-a[0])/c;n.height=(a[3]-a[1])/u;n.annotationElementId=t.id||null;d&&(n.altTextData=d);n._initialData=s;n.#Yr=!!s;return n}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i={annotationType:g.STAMP,bitmapId:this.#Gr,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#Kr,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#so(!0);i.accessibilityData=this.serializeAltText(!0);return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#$n(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1}i.id=this.annotationElementId;if(null===e)return i;e.stamps||=new Map;const a=this.#Kr?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#Gr)){if(this.#Kr){const t=e.stamps.get(this.#Gr);if(a>t.area){t.area=a;t.serialized.bitmap.close();t.serialized.bitmap=this.#so(!1)}}}else{e.stamps.set(this.#Gr,{area:a,serialized:i});i.bitmap=this.#so(!1)}return i}#$n(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}}class AnnotationEditorLayer{#Cn;#no=!1;#ao=null;#ro=null;#oo=null;#lo=new Map;#ho=!1;#do=!1;#co=!1;#uo=null;#po=null;#go=null;#mo=null;#m;static _initialized=!1;static#U=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor].map((t=>[t._editorType,t])));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:a,drawLayer:r,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#U.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#m=t;this.pageIndex=e;this.div=i;this.#Cn=n;this.#ao=a;this.viewport=l;this.#go=o;this.drawLayer=r;this._structTree=s;this.#m.addLayer(this)}get isEmpty(){return 0===this.#lo.size}get isInvisible(){return this.isEmpty&&this.#m.getMode()===g.NONE}updateToolbar(t){this.#m.updateToolbar(t)}updateMode(t=this.#m.getMode()){this.#fo();switch(t){case g.NONE:this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case g.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case g.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;for(const i of AnnotationEditorLayer.#U.values())e.toggle(`${i._type}Editing`,t===i._editorType);this.div.hidden=!1}hasTextLayer(t){return t===this.#go?.div}setEditingState(t){this.#m.setEditingState(t)}addCommands(t){this.#m.addCommands(t)}cleanUndoStack(t){this.#m.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#ao?.div.classList.toggle("disabled",!t)}async enable(){this.#co=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);const t=new Set;for(const e of this.#lo.values()){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#m.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}if(!this.#ao){this.#co=!1;return}const e=this.#ao.getEditableAnnotations();for(const i of e){i.hide();if(this.#m.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#co=!1}disable(){this.#do=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);const t=new Map,e=new Map;for(const i of this.#lo.values()){i.disableEditing();if(i.annotationElementId)if(null===i.serialize()){e.set(i.annotationElementId,i);this.getEditableAnnotation(i.annotationElementId)?.show();i.remove()}else t.set(i.annotationElementId,i)}if(this.#ao){const i=this.#ao.getEditableAnnotations();for(const s of i){const{id:i}=s.data;if(this.#m.isDeletedAnnotationElement(i))continue;let n=e.get(i);if(n){n.resetAnnotationElement(s);n.show(!1);s.show()}else{n=t.get(i);if(n){this.#m.addChangedExistingAnnotation(n);n.renderAnnotationElement(s)&&n.show(!1)}s.show()}}}this.#fo();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#U.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);this.#do=!1}getEditableAnnotation(t){return this.#ao?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#m.getActive()!==t&&this.#m.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#go?.div&&!this.#mo){this.#mo=new AbortController;const t=this.#m.combinedSignal(this.#mo);this.#go.div.addEventListener("pointerdown",this.#bo.bind(this),{signal:t});this.#go.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#go?.div&&this.#mo){this.#mo.abort();this.#mo=null;this.#go.div.classList.remove("highlighting")}}#bo(t){this.#m.unselectAll();const{target:e}=t;if(e===this.#go.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent"))&&this.#go.div.contains(e)){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#m.showAllEditors("highlight",!0,!0);this.#go.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#m.direction,{target:this.#go.div,x:t.x,y:t.y});this.#go.div.addEventListener("pointerup",(()=>{this.#go.div.classList.remove("free");this.toggleDrawing(!0)}),{once:!0,signal:this.#m._signal});t.preventDefault()}}enableClick(){if(this.#ro)return;this.#ro=new AbortController;const t=this.#m.combinedSignal(this.#ro);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#ro?.abort();this.#ro=null}attach(t){this.#lo.set(t.id,t);const{annotationElementId:e}=t;e&&this.#m.isDeletedAnnotationElement(e)&&this.#m.removeDeletedAnnotationElement(t)}detach(t){this.#lo.delete(t.id);this.#Cn?.removePointerInTextLayer(t.contentDiv);!this.#do&&t.annotationElementId&&this.#m.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#m.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#m.addDeletedAnnotationElement(t.annotationElementId);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#m.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#co);this.#m.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#oo){t._focusEventsAllowed=!1;this.#oo=setTimeout((()=>{this.#oo=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this.#m._signal});e.focus()}}),0)}t._structTreeParentId=this.#Cn?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#m.getId()}get#Ao(){return AnnotationEditorLayer.#U.get(this.#m.getMode())}combinedSignal(t){return this.#m.combinedSignal(t)}#wo(t){const e=this.#Ao;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#Ao?.canCreateNewEmptyEditor()}pasteEditor(t,e){this.#m.updateToolbar(t);this.#m.updateMode(t);const{offsetX:i,offsetY:s}=this.#vo(),n=this.getNextId(),a=this.#wo({parent:this,id:n,x:i,y:s,uiManager:this.#m,isCentered:!0,...e});a&&this.add(a)}async deserialize(t){return await(AnnotationEditorLayer.#U.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#m))||null}createAndAddNewEditor(t,e,i={}){const s=this.getNextId(),n=this.#wo({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#m,isCentered:e,...i});n&&this.add(n);return n}#vo(){const{x:t,y:e,width:i,height:s}=this.div.getBoundingClientRect(),n=Math.max(0,t),a=Math.max(0,e),r=(n+Math.min(window.innerWidth,t+i))/2-t,o=(a+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[r,o]:[o,r];return{offsetX:l,offsetY:h}}addNewEditor(){this.createAndAddNewEditor(this.#vo(),!0)}setSelected(t){this.#m.setSelected(t)}toggleSelected(t){this.#m.toggleSelected(t)}unselect(t){this.#m.unselect(t)}pointerup(t){const{isMac:e}=util_FeatureTest.platform;if(!(0!==t.button||t.ctrlKey&&e)&&t.target===this.div&&this.#ho){this.#ho=!1;this.#Ao?.isDrawer&&this.#Ao.supportMultipleDrawings||(this.#no?this.#m.getMode()!==g.STAMP?this.createAndAddNewEditor(t,!1):this.#m.unselectAll():this.#no=!0)}}pointerdown(t){this.#m.getMode()===g.HIGHLIGHT&&this.enableTextSelection();if(this.#ho){this.#ho=!1;return}const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#ho=!0;if(this.#Ao?.isDrawer){this.startDrawingSession(t);return}const i=this.#m.getActive();this.#no=!i||i.isEmpty()}startDrawingSession(t){this.div.focus();if(this.#uo){this.#Ao.startDrawing(this,this.#m,!1,t);return}this.#m.setCurrentDrawingSession(this);this.#uo=new AbortController;const e=this.#m.combinedSignal(this.#uo);this.div.addEventListener("blur",(({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#po=null;this.commitOrRemove()}}),{signal:e});this.#Ao.startDrawing(this,this.#m,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#po=t)}else this.#po&&setTimeout((()=>{this.#po?.focus();this.#po=null}),0)}endDrawingSession(t=!1){if(!this.#uo)return null;this.#m.setCurrentDrawingSession(null);this.#uo.abort();this.#uo=null;this.#po=null;return this.#Ao.endDrawing(t)}findNewParent(t,e,i){const s=this.#m.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#uo){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#uo&&this.#Ao.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#m.getActive()?.parent===this){this.#m.commitOrRemove();this.#m.setActiveEditor(null)}if(this.#oo){clearTimeout(this.#oo);this.#oo=null}for(const t of this.#lo.values()){this.#Cn?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#lo.clear();this.#m.removeLayer(this)}#fo(){for(const t of this.#lo.values())t.isEmpty()&&t.remove()}render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#m.getEditors(this.pageIndex)){this.add(t);t.rebuild()}this.updateMode()}update({viewport:t}){this.#m.commitOrRemove();this.#fo();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#lo.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#m.viewParameters.realScale}}class DrawLayer{#nn=null;#w=0;#yo=new Map;#xo=new Map;constructor({pageIndex:t}){this.pageIndex=t}setParent(t){if(this.#nn){if(this.#nn!==t){if(this.#yo.size>0)for(const e of this.#yo.values()){e.remove();t.append(e)}this.#nn=t}}else this.#nn=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#_o(t,[e,i,s,n]){const{style:a}=t;a.top=100*i+"%";a.left=100*e+"%";a.width=100*s+"%";a.height=100*n+"%"}#Eo(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#nn.append(t);t.setAttribute("aria-hidden",!0);return t}#So(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#Co(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=this.#w++,n=this.#Eo(),a=DrawLayer._svgFactory.createElement("defs");n.append(a);const r=DrawLayer._svgFactory.createElement("path");a.append(r);const o=`path_p${this.pageIndex}_${s}`;r.setAttribute("id",o);r.setAttribute("vector-effect","non-scaling-stroke");e&&this.#xo.set(s,r);const l=i?this.#So(a,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#yo.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=this.#w++,s=this.#Eo(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const a=DrawLayer._svgFactory.createElement("path");n.append(a);const r=`path_p${this.pageIndex}_${i}`;a.setAttribute("id",r);a.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_p${this.pageIndex}_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${r}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${r}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#yo.set(i,s);return i}finalizeDraw(t,e){this.#xo.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:a}=e,r="number"==typeof t?this.#yo.get(t):t;if(r){i&&this.#Co(r,i);s&&DrawLayer.#_o(r,s);if(n){const{classList:t}=r;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(a){const t=r.firstChild.firstChild;this.#Co(t,a)}}}updateParent(t,e){if(e===this)return;const i=this.#yo.get(t);if(i){e.#nn.append(i);this.#yo.delete(t);e.#yo.set(t,i)}}remove(t){this.#xo.delete(t);if(null!==this.#nn){this.#yo.get(t).remove();this.#yo.delete(t)}}destroy(){this.#nn=null;for(const t of this.#yo.values())t.remove();this.#yo.clear();this.#xo.clear()}}globalThis.pdfjsTestingUtils={HighlightOutliner};var Ut=__webpack_exports__.AbortException,Gt=__webpack_exports__.AnnotationEditorLayer,$t=__webpack_exports__.AnnotationEditorParamsType,Vt=__webpack_exports__.AnnotationEditorType,jt=__webpack_exports__.AnnotationEditorUIManager,Wt=__webpack_exports__.AnnotationLayer,qt=__webpack_exports__.AnnotationMode,Xt=__webpack_exports__.ColorPicker,Kt=__webpack_exports__.DOMSVGFactory,Yt=__webpack_exports__.DrawLayer,Qt=__webpack_exports__.FeatureTest,Jt=__webpack_exports__.GlobalWorkerOptions,Zt=__webpack_exports__.ImageKind,te=__webpack_exports__.InvalidPDFException,ee=__webpack_exports__.MissingPDFException,ie=__webpack_exports__.OPS,se=__webpack_exports__.OutputScale,ne=__webpack_exports__.PDFDataRangeTransport,ae=__webpack_exports__.PDFDateString,re=__webpack_exports__.PDFWorker,oe=__webpack_exports__.PasswordResponses,le=__webpack_exports__.PermissionFlag,he=__webpack_exports__.PixelsPerInch,de=__webpack_exports__.RenderingCancelledException,ce=__webpack_exports__.TextLayer,ue=__webpack_exports__.TouchManager,pe=__webpack_exports__.UnexpectedResponseException,ge=__webpack_exports__.Util,me=__webpack_exports__.VerbosityLevel,fe=__webpack_exports__.XfaLayer,be=__webpack_exports__.build,Ae=__webpack_exports__.createValidAbsoluteUrl,we=__webpack_exports__.fetchData,ve=__webpack_exports__.getDocument,ye=__webpack_exports__.getFilenameFromUrl,xe=__webpack_exports__.getPdfFilenameFromUrl,_e=__webpack_exports__.getXfaPageViewport,Ee=__webpack_exports__.isDataScheme,Se=__webpack_exports__.isPdfFile,Ce=__webpack_exports__.noContextMenu,Te=__webpack_exports__.normalizeUnicode,Me=__webpack_exports__.setLayerDimensions,Pe=__webpack_exports__.shadow,De=__webpack_exports__.stopEvent,ke=__webpack_exports__.version;export{Ut as AbortException,Gt as AnnotationEditorLayer,$t as AnnotationEditorParamsType,Vt as AnnotationEditorType,jt as AnnotationEditorUIManager,Wt as AnnotationLayer,qt as AnnotationMode,Xt as ColorPicker,Kt as DOMSVGFactory,Yt as DrawLayer,Qt as FeatureTest,Jt as GlobalWorkerOptions,Zt as ImageKind,te as InvalidPDFException,ee as MissingPDFException,ie as OPS,se as OutputScale,ne as PDFDataRangeTransport,ae as PDFDateString,re as PDFWorker,oe as PasswordResponses,le as PermissionFlag,he as PixelsPerInch,de as RenderingCancelledException,ce as TextLayer,ue as TouchManager,pe as UnexpectedResponseException,ge as Util,me as VerbosityLevel,fe as XfaLayer,be as build,Ae as createValidAbsoluteUrl,we as fetchData,ve as getDocument,ye as getFilenameFromUrl,xe as getPdfFilenameFromUrl,_e as getXfaPageViewport,Ee as isDataScheme,Se as isPdfFile,Ce as noContextMenu,Te as normalizeUnicode,Me as setLayerDimensions,Pe as shadow,De as stopEvent,ke as version}; \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs new file mode 100644 index 00000000000..ee4038504a6 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/build/pdf.worker.min.mjs @@ -0,0 +1,21 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */var e={d:(t,i)=>{for(var a in i)e.o(i,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:i[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},__webpack_exports__ = globalThis.pdfjsWorker = {};e.d(__webpack_exports__,{WorkerMessageHandler:()=>WorkerMessageHandler});const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),i=[1,0,0,1,0,0],a=[.001,0,0,.001,0,0],s=1.35,r=.35,n=.25925925925925924,g=1,o=2,c=4,C=8,h=16,l=64,Q=128,E=256,u="pdfjs_internal_editor_",d=3,f=9,p=13,m=15,y={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},w=0,D=4,b=1,F=2,S=3,k=1,R=2,N=3,G=4,M=5,U=6,x=7,L=8,H=9,J=10,Y=11,v=12,K=13,T=14,q=15,O=16,W=17,j=20,X="Group",Z="R",V=1,z=2,_=4,$=16,AA=32,eA=128,tA=512,iA=1,aA=2,sA=4096,rA=8192,nA=32768,gA=65536,oA=131072,IA=1048576,cA=2097152,CA=8388608,hA=16777216,lA=1,BA=2,QA=3,EA=4,uA=5,dA={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},fA={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},pA={O:"PageOpen",C:"PageClose"},mA=1,yA=5,wA=1,DA=2,bA=3,FA=4,SA=5,kA=6,RA=7,NA=8,GA=9,MA=10,UA=11,xA=12,LA=13,HA=14,JA=15,YA=16,vA=17,KA=18,TA=19,qA=20,OA=21,PA=22,WA=23,jA=24,XA=25,ZA=26,VA=27,zA=28,_A=29,$A=30,Ae=31,ee=32,te=33,ie=34,ae=35,se=36,re=37,ne=38,ge=39,oe=40,Ie=41,ce=42,Ce=43,he=44,le=45,Be=46,Qe=47,Ee=48,ue=49,de=50,fe=51,pe=52,me=53,ye=54,we=55,De=56,be=57,Fe=58,Se=59,ke=60,Re=61,Ne=62,Ge=63,Me=64,Ue=65,xe=66,Le=67,He=68,Je=69,Ye=70,ve=71,Ke=72,Te=73,qe=74,Oe=75,Pe=76,We=77,je=80,Xe=81,Ze=83,Ve=84,ze=85,_e=86,$e=87,At=88,et=89,tt=90,it=91,at=92,st=93,rt=1,nt=2;let gt=mA;function getVerbosityLevel(){return gt}function info(e){gt>=yA&&console.log(`Info: ${e}`)}function warn(e){gt>=mA&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,i=null){if(!e)return null;try{if(i&&"string"==typeof e){if(i.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(i.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const a=t?new URL(e,t):new URL(e);if(function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(a))return a}catch{}return null}function shadow(e,t,i,a=!1){Object.defineProperty(e,t,{value:i,enumerable:!a,configurable:!0,writable:!1});return i}const ot=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends ot{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends ot{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends ot{constructor(e){super(e,"InvalidPDFException")}}class MissingPDFException extends ot{constructor(e){super(e,"MissingPDFException")}}class UnexpectedResponseException extends ot{constructor(e,t){super(e,"UnexpectedResponseException");this.status=t}}class FormatError extends ot{constructor(e){super(e,"FormatError")}}class AbortException extends ot{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,i=8192;if(t>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){return"undefined"!=typeof navigator&&"string"==typeof navigator?.platform?shadow(this,"platform",{isMac:navigator.platform.includes("Mac"),isWindows:navigator.platform.includes("Win"),isFirefox:"string"==typeof navigator?.userAgent&&navigator.userAgent.includes("Firefox")}):shadow(this,"platform",{isMac:!1,isWindows:!1,isFirefox:!1})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const It=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,i){return`#${It[e]}${It[t]}${It[i]}`}static scaleMinMax(e,t){let i;if(e[0]){if(e[0]<0){i=t[0];t[0]=t[2];t[2]=i}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){i=t[1];t[1]=t[3];t[3]=i}t[1]*=e[3];t[3]*=e[3]}else{i=t[0];t[0]=t[1];t[1]=i;i=t[2];t[2]=t[3];t[3]=i;if(e[1]<0){i=t[1];t[1]=t[3];t[3]=i}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){i=t[0];t[0]=t[2];t[2]=i}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static applyTransform(e,t){return[e[0]*t[0]+e[1]*t[2]+t[4],e[0]*t[1]+e[1]*t[3]+t[5]]}static applyInverseTransform(e,t){const i=t[0]*t[3]-t[1]*t[2];return[(e[0]*t[3]-e[1]*t[2]+t[2]*t[5]-t[4]*t[3])/i,(-e[0]*t[1]+e[1]*t[0]+t[4]*t[1]-t[5]*t[0])/i]}static getAxialAlignedBoundingBox(e,t){const i=this.applyTransform(e,t),a=this.applyTransform(e.slice(2,4),t),s=this.applyTransform([e[0],e[3]],t),r=this.applyTransform([e[2],e[1]],t);return[Math.min(i[0],a[0],s[0],r[0]),Math.min(i[1],a[1],s[1],r[1]),Math.max(i[0],a[0],s[0],r[0]),Math.max(i[1],a[1],s[1],r[1])]}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e){const t=[e[0],e[2],e[1],e[3]],i=e[0]*t[0]+e[1]*t[2],a=e[0]*t[1]+e[1]*t[3],s=e[2]*t[0]+e[3]*t[2],r=e[2]*t[1]+e[3]*t[3],n=(i+r)/2,g=Math.sqrt((i+r)**2-4*(i*r-s*a))/2,o=n+g||1,c=n-g||1;return[Math.sqrt(o),Math.sqrt(c)]}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const i=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),a=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(i>a)return null;const s=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),r=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return s>r?null:[i,s,a,r]}static#A(e,t,i,a,s,r,n,g,o,c){if(o<=0||o>=1)return;const C=1-o,h=o*o,l=h*o,Q=C*(C*(C*e+3*o*t)+3*h*i)+l*a,E=C*(C*(C*s+3*o*r)+3*h*n)+l*g;c[0]=Math.min(c[0],Q);c[1]=Math.min(c[1],E);c[2]=Math.max(c[2],Q);c[3]=Math.max(c[3],E)}static#e(e,t,i,a,s,r,n,g,o,c,C,h){if(Math.abs(o)<1e-12){Math.abs(c)>=1e-12&&this.#A(e,t,i,a,s,r,n,g,-C/c,h);return}const l=c**2-4*C*o;if(l<0)return;const Q=Math.sqrt(l),E=2*o;this.#A(e,t,i,a,s,r,n,g,(-c+Q)/E,h);this.#A(e,t,i,a,s,r,n,g,(-c-Q)/E,h)}static bezierBoundingBox(e,t,i,a,s,r,n,g,o){if(o){o[0]=Math.min(o[0],e,n);o[1]=Math.min(o[1],t,g);o[2]=Math.max(o[2],e,n);o[3]=Math.max(o[3],t,g)}else o=[Math.min(e,n),Math.min(t,g),Math.max(e,n),Math.max(t,g)];this.#e(e,i,s,n,t,a,r,g,3*(3*(i-s)-e+n),6*(e-2*i+s),3*(i-e),o);this.#e(e,i,s,n,t,a,r,g,3*(3*(a-r)-t+g),6*(t-2*a+r),3*(a-t),o);return o}}const ct=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e){if(e[0]>="ï"){let t;if("þ"===e[0]&&"ÿ"===e[1]){t="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("ÿ"===e[0]&&"þ"===e[1]){t="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"ï"===e[0]&&"»"===e[1]&&"¿"===e[2]&&(t="utf-8");if(t)try{const i=new TextDecoder(t,{fatal:!0}),a=stringToBytes(e),s=i.decode(a);return s.includes("")?s.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,""):s}catch(e){warn(`stringToPDFString: "${e}".`)}}const t=[];for(let i=0,a=e.length;iIt[e])).join("")}"function"!=typeof Promise.try&&(Promise.try=function(e,...t){return new Promise((i=>{i(e(...t))}))});const lt=Symbol("CIRCULAR_REF"),Bt=Symbol("EOF");let Qt=Object.create(null),Et=Object.create(null),ut=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return Et[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return Qt[e]||=new Cmd(e)}}const dt=function nonSerializableClosure(){return dt};class Dict{constructor(e=null){this._map=new Map;this.xref=e;this.objId=null;this.suppressEncryption=!1;this.__nonSerializable__=dt}assignXref(e){this.xref=e}get size(){return this._map.size}get(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}return a instanceof Ref&&this.xref?this.xref.fetch(a,this.suppressEncryption):a}async getAsync(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}return a instanceof Ref&&this.xref?this.xref.fetchAsync(a,this.suppressEncryption):a}getArray(e,t,i){let a=this._map.get(e);if(void 0===a&&void 0!==t){a=this._map.get(t);void 0===a&&void 0!==i&&(a=this._map.get(i))}a instanceof Ref&&this.xref&&(a=this.xref.fetch(a,this.suppressEncryption));if(Array.isArray(a)){a=a.slice();for(let e=0,t=a.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:i=!1}){const a=new Dict(e),s=new Map;for(const e of t)if(e instanceof Dict)for(const[t,a]of e._map){let e=s.get(t);if(void 0===e){e=[];s.set(t,e)}else if(!(i&&a instanceof Dict))continue;e.push(a)}for(const[t,i]of s){if(1===i.length||!(i[0]instanceof Dict)){a._map.set(t,i[0]);continue}const s=new Dict(e);for(const e of i)for(const[t,i]of e._map)s._map.has(t)||s._map.set(t,i);s.size>0&&a._map.set(t,s)}s.clear();return a.size>0?a:Dict.empty}clone(){const e=new Dict(this.xref);for(const t of this.getKeys())e.set(t,this.getRaw(t));return e}delete(e){delete this._map[e]}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=ut[e];if(t)return t;const i=/^(\d+)R(\d*)$/.exec(e);return i&&"0"!==i[1]?ut[e]=new Ref(parseInt(i[1]),i[2]?parseInt(i[2]):0):null}static get(e,t){const i=0===t?`${e}R`:`${e}R${t}`;return ut[i]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{constructor(){this._map=new Map}get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,i=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}}const ft=/^[1-9]\.\d$/,pt=2**31-1;function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends ot{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends ot{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends ot{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends ot{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let i=0;for(let a=0;a0,"The number should be a positive integer.");const i="M".repeat(e/1e3|0)+mt[e%1e3/100|0]+mt[10+(e%100/10|0)]+mt[20+e%10];return t?i.toLowerCase():i}function log2(e){return e>0?Math.ceil(Math.log2(e)):0}function readInt8(e,t){return e[t]<<24>>24}function readUint16(e,t){return e[t]<<8|e[t+1]}function readUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every((e=>"number"==typeof e)):ArrayBuffer.isView(e)&&(0===e.length||"number"==typeof e[0])&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map((e=>{const i=e.match(t);return i?{name:i[1],pos:parseInt(i[2],10)}:{name:e,pos:0}}))}function escapePDFName(e){const t=[];let i=0;for(let a=0,s=e.length;a126||35===s||40===s||41===s||60===s||62===s||91===s||93===s||123===s||125===s||47===s||37===s){i"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`))}function _collectJS(e,t,i,a){if(!e)return;let s=null;if(e instanceof Ref){if(a.has(e))return;s=e;a.put(s);e=t.fetch(e)}if(Array.isArray(e))for(const s of e)_collectJS(s,t,i,a);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let a;t instanceof BaseStream?a=t.getString():"string"==typeof t&&(a=t);a&&=stringToPDFString(a).replaceAll("\0","");a&&i.push(a)}_collectJS(e.getRaw("Next"),t,i,a)}s&&a.remove(s)}function collectActions(e,t,i){const a=Object.create(null),s=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(s)for(let t=s.length-1;t>=0;t--){const r=s[t];if(r instanceof Dict)for(const t of r.getKeys()){const s=i[t];if(!s)continue;const n=[];_collectJS(r.getRaw(t),e,n,new RefSet);n.length>0&&(a[s]=n)}}if(t.has("A")){const i=[];_collectJS(t.get("A"),e,i,new RefSet);i.length>0&&(a.Action=i)}return objectSize(a)>0?a:null}const yt={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,i=e.length;t55295&&(i<57344||i>65533)&&t++;yield i}}function encodeToXmlString(e){const t=[];let i=0;for(let a=0,s=e.length;a55295&&(s<57344||s>65533)&&a++;i=a+1}}if(0===t.length)return e;i: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:i,fontWeight:a,italicAngle:s}=e;if(!validateFontName(i,!0))return!1;const r=a?a.toString():"";e.fontWeight=t.has(r)?r:"400";const n=parseFloat(s);e.italicAngle=isNaN(n)||n<-90||n>90?"14":s.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[i,a]of e){if(!i.startsWith(u))continue;let e=t.get(a.pageIndex);if(!e){e=[];t.set(a.pageIndex,e)}e.push(a)}return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return function isAscii(e){return/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let i=0,a=e.length;i>8&255],It[255&a])}return t.join("")}function stringToUTF16String(e,t=!1){const i=[];t&&i.push("þÿ");for(let t=0,a=e.length;t>8&255),String.fromCharCode(255&a))}return i.join("")}function getRotationMatrix(e,t,i){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,i];case 270:return[0,-1,1,0,0,i];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class Stream extends BaseStream{constructor(e,t,i,a){super();this.bytes=e instanceof Uint8Array?e:new Uint8Array(e);this.start=t||0;this.pos=this.start;this.end=t+i||this.bytes.length;this.dict=a}get length(){return this.end-this.start}get isEmpty(){return 0===this.length}getByte(){return this.pos>=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,i=this.pos,a=this.end;if(!e)return t.subarray(i,a);let s=i+e;s>a&&(s=a);this.pos=s;return t.subarray(i,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,i=null){return new Stream(this.bytes.buffer,e,t,i)}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{constructor(e,t,i){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this._loadedChunks=new Set;this.numChunks=Math.ceil(e/t);this.manager=i;this.progressiveDataLength=0;this.lastSuccessfulEnsureByteChunk=-1}getMissingChunks(){const e=[];for(let t=0,i=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=i;ethis.numChunks)&&t!==this.lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this.lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const i=Math.floor(e/this.chunkSize);if(i>this.numChunks)return;const a=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let s=i;s=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,i=this.pos,a=this.end;if(!e){a>this.progressiveDataLength&&this.ensureRange(i,a);return t.subarray(i,a)}let s=i+e;s>a&&(s=a);s>this.progressiveDataLength&&this.ensureRange(i,s);this.pos=s;return t.subarray(i,s)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,i=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),i=Math.floor((this.end-1)/e)+1,a=[];for(let e=t;e{const readChunk=({value:r,done:n})=>{try{if(n){const t=arrayBuffersToBytes(a);a=null;e(t);return}s+=r.byteLength;i.isStreamingSupported&&this.onProgress({loaded:s});a.push(r);i.read().then(readChunk,t)}catch(e){t(e)}};i.read().then(readChunk,t)})).then((t=>{this.aborted||this.onReceiveData({chunk:t,begin:e})}))}requestAllChunks(e=!1){if(!e){const e=this.stream.getMissingChunks();this._requestChunks(e)}return this._loadedStreamCapability.promise}_requestChunks(e){const t=this.currRequestId++,i=new Set;this._chunksNeededByRequest.set(t,i);for(const t of e)this.stream.hasChunk(t)||i.add(t);if(0===i.size)return Promise.resolve();const a=Promise.withResolvers();this._promisesByRequest.set(t,a);const s=[];for(const e of i){let i=this._requestsByChunk.get(e);if(!i){i=[];this._requestsByChunk.set(e,i);s.push(e)}i.push(t)}if(s.length>0){const e=this.groupChunks(s);for(const t of e){const e=t.beginChunk*this.chunkSize,i=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,i).catch(a.reject)}}return a.promise.catch((e=>{if(!this.aborted)throw e}))}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const i=this.getBeginChunk(e),a=this.getEndChunk(t),s=[];for(let e=i;e=0&&a+1!==r){t.push({beginChunk:i,endChunk:a+1});i=r}s+1===e.length&&t.push({beginChunk:i,endChunk:r+1});a=r}return t}onProgress(e){this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize+e.loaded,total:this.length})}onReceiveData(e){const t=e.chunk,i=void 0===e.begin,a=i?this.progressiveDataLength:e.begin,s=a+t.byteLength,r=Math.floor(a/this.chunkSize),n=s0||g.push(i)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===this.stream.numChunksLoaded){const t=this.stream.numChunks-1;this.stream.hasChunk(t)||(e=t)}else e=this.stream.nextEmptyChunk(n);Number.isInteger(e)&&this._requestChunks([e])}for(const e of g){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize,total:this.length})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfNetworkStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}class ColorSpace{constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t){const i=new Uint8ClampedArray(3);this.getRgbItem(e,t,i,0);return i}getRgbItem(e,t,i,a){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,i,a,s,r,n){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,i,a,s,r,n,g,o){const c=t*i;let C=null;const h=1<h&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=n<=8?new Uint8Array(h):new Uint16Array(h);for(let e=0;e=.99554525?1:this.#B(0,1,1.055*e**(1/2.4)-.055)}#B(e,t,i){return Math.max(e,Math.min(t,i))}#Q(e){return e<0?-this.#Q(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#I}#E(e,t,i){if(0===e[0]&&0===e[1]&&0===e[2]){i[0]=t[0];i[1]=t[1];i[2]=t[2];return}const a=this.#Q(0),s=(1-a)/(1-this.#Q(e[0])),r=1-s,n=(1-a)/(1-this.#Q(e[1])),g=1-n,o=(1-a)/(1-this.#Q(e[2])),c=1-o;i[0]=t[0]*s+r;i[1]=t[1]*n+g;i[2]=t[2]*o+c}#u(e,t,i){if(1===e[0]&&1===e[2]){i[0]=t[0];i[1]=t[1];i[2]=t[2];return}const a=i;this.#c(CalRGBCS.#i,t,a);const s=CalRGBCS.#n;this.#C(e,a,s);this.#c(CalRGBCS.#a,s,i)}#d(e,t,i){const a=i;this.#c(CalRGBCS.#i,t,a);const s=CalRGBCS.#n;this.#h(e,a,s);this.#c(CalRGBCS.#a,s,i)}#t(e,t,i,a,s){const r=this.#B(0,1,e[t]*s),n=this.#B(0,1,e[t+1]*s),g=this.#B(0,1,e[t+2]*s),o=1===r?1:r**this.GR,c=1===n?1:n**this.GG,C=1===g?1:g**this.GB,h=this.MXA*o+this.MXB*c+this.MXC*C,l=this.MYA*o+this.MYB*c+this.MYC*C,Q=this.MZA*o+this.MZB*c+this.MZC*C,E=CalRGBCS.#g;E[0]=h;E[1]=l;E[2]=Q;const u=CalRGBCS.#o;this.#u(this.whitePoint,E,u);const d=CalRGBCS.#g;this.#E(this.blackPoint,u,d);const f=CalRGBCS.#o;this.#d(CalRGBCS.#r,d,f);const p=CalRGBCS.#g;this.#c(CalRGBCS.#s,f,p);i[a]=255*this.#l(p[0]);i[a+1]=255*this.#l(p[1]);i[a+2]=255*this.#l(p[2])}getRgbItem(e,t,i,a){this.#t(e,t,i,a,1)}getRgbBuffer(e,t,i,a,s,r,n){const g=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#f(e){return e>=6/29?e**3:108/841*(e-4/29)}#p(e,t,i,a){return i+e*(a-i)/t}#t(e,t,i,a,s){let r=e[t],n=e[t+1],g=e[t+2];if(!1!==i){r=this.#p(r,i,0,100);n=this.#p(n,i,this.amin,this.amax);g=this.#p(g,i,this.bmin,this.bmax)}n>this.amax?n=this.amax:nthis.bmax?g=this.bmax:g>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,i){let a=0;for(let s=i;s>=0;s--){a+=e[s]+t[s];e[s]=255&a;a>>=8}}function incHex(e,t){let i=1;for(let a=t;a>=0&&i>0;a--){i+=e[a];e[a]=255&i;i>>=8}}const wt=16;class BinaryCMapStream{constructor(e){this.buffer=e;this.pos=0;this.end=e.length;this.tmpBuf=new Uint8Array(19)}readByte(){return this.pos>=this.end?-1:this.buffer[this.pos++]}readNumber(){let e,t=0;do{const i=this.readByte();if(i<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&i);t=t<<7|127&i}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.buffer.subarray(this.pos,this.pos+t+1));this.pos+=t+1}readHexNumber(e,t){let i;const a=this.tmpBuf;let s=0;do{const e=this.readByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");i=!(128&e);a[s++]=127&e}while(!i);let r=t,n=0,g=0;for(;r>=0;){for(;g<8&&a.length>0;){n|=a[--s]<>=8;g-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const i=1&e[t]?255:0;let a=0;for(let s=0;s<=t;s++){a=(1&a)<<8|e[s];e[s]=a>>1^i}}readString(){const e=this.readNumber(),t=new Array(e);for(let i=0;i=0;){const e=l>>5;if(7===e){switch(31&l){case 0:a.readString();break;case 1:r=a.readString()}continue}const i=!!(16&l),s=15&l;if(s+1>wt)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const Q=1,E=a.readNumber();switch(e){case 0:a.readHex(n,s);a.readHexNumber(g,s);addHex(g,n,s);t.addCodespaceRange(s+1,hexToInt(n,s),hexToInt(g,s));for(let e=1;es&&(a=s)}else{for(;!this.eof;)this.readBlock(t);a=this.bufferLength}this.pos=a;return this.buffer.subarray(i,a)}async getImageData(e,t=null){if(!this.canAsyncDecodeImageFromBuffer)return this.getBytes(e,t);const i=await this.stream.asyncGetBytes();return this.decodeImage(i,t)}reset(){this.pos=0}makeSubStream(e,t,i=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const i=e+t;for(;this.bufferLength<=i&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,i)}getBaseStreams(){return this.str?this.str.getBaseStreams():null}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter((e=>e instanceof BaseStream));let i=0;for(const t of e)i+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(i);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let i;try{i=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const a=this.bufferLength,s=a+i.length;this.ensureBuffer(s).set(i,a);this.bufferLength=s}getBaseStreams(){const e=[];for(const t of this.streams){const i=t.getBaseStreams();i&&e.push(...i)}return e.length>0?e:null}}class Ascii85Stream extends DecodeStream{constructor(e,t){t&&(t*=.8);super(t);this.str=e;this.dict=e.dict;this.input=new Uint8Array(5)}readBlock(){const e=this.str;let t=e.getByte();for(;isWhiteSpace(t);)t=e.getByte();if(-1===t||126===t){this.eof=!0;return}const i=this.bufferLength;let a,s;if(122===t){a=this.ensureBuffer(i+4);for(s=0;s<4;++s)a[i+s]=0;this.bufferLength+=4}else{const r=this.input;r[0]=t;for(s=1;s<5;++s){t=e.getByte();for(;isWhiteSpace(t);)t=e.getByte();r[s]=t;if(-1===t||126===t)break}a=this.ensureBuffer(i+s-1);this.bufferLength+=s-1;if(s<5){for(;s<5;++s)r[s]=117;this.eof=!0}let n=0;for(s=0;s<5;++s)n=85*n+(r[s]-33);for(s=3;s>=0;--s){a[i+s]=255&n;n>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.str=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.str.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,i=this.ensureBuffer(this.bufferLength+t);let a=this.bufferLength,s=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(s<0)s=e;else{i[a++]=s<<4|e;s=-1}}if(s>=0&&this.eof){i[a++]=s<<4;s=-1}this.firstDigit=s;this.bufferLength=a}}const bt=-1,Ft=[[-1,-1],[-1,-1],[7,8],[7,7],[6,6],[6,6],[6,5],[6,5],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2]],St=[[-1,-1],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[12,1984],[12,2048],[12,2112],[12,2176],[12,2240],[12,2304],[11,1856],[11,1856],[11,1920],[11,1920],[12,2368],[12,2432],[12,2496],[12,2560]],kt=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[8,29],[8,29],[8,30],[8,30],[8,45],[8,45],[8,46],[8,46],[7,22],[7,22],[7,22],[7,22],[7,23],[7,23],[7,23],[7,23],[8,47],[8,47],[8,48],[8,48],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[7,20],[7,20],[7,20],[7,20],[8,33],[8,33],[8,34],[8,34],[8,35],[8,35],[8,36],[8,36],[8,37],[8,37],[8,38],[8,38],[7,19],[7,19],[7,19],[7,19],[8,31],[8,31],[8,32],[8,32],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[8,53],[8,53],[8,54],[8,54],[7,26],[7,26],[7,26],[7,26],[8,39],[8,39],[8,40],[8,40],[8,41],[8,41],[8,42],[8,42],[8,43],[8,43],[8,44],[8,44],[7,21],[7,21],[7,21],[7,21],[7,28],[7,28],[7,28],[7,28],[8,61],[8,61],[8,62],[8,62],[8,63],[8,63],[8,0],[8,0],[8,320],[8,320],[8,384],[8,384],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[7,27],[7,27],[7,27],[7,27],[8,59],[8,59],[8,60],[8,60],[9,1472],[9,1536],[9,1600],[9,1728],[7,18],[7,18],[7,18],[7,18],[7,24],[7,24],[7,24],[7,24],[8,49],[8,49],[8,50],[8,50],[8,51],[8,51],[8,52],[8,52],[7,25],[7,25],[7,25],[7,25],[8,55],[8,55],[8,56],[8,56],[8,57],[8,57],[8,58],[8,58],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[8,448],[8,448],[8,512],[8,512],[9,704],[9,768],[8,640],[8,640],[8,576],[8,576],[9,832],[9,896],[9,960],[9,1024],[9,1088],[9,1152],[9,1216],[9,1280],[9,1344],[9,1408],[7,256],[7,256],[7,256],[7,256],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7]],Rt=[[-1,-1],[-1,-1],[12,-2],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[11,1792],[11,1792],[12,1984],[12,1984],[12,2048],[12,2048],[12,2112],[12,2112],[12,2176],[12,2176],[12,2240],[12,2240],[12,2304],[12,2304],[11,1856],[11,1856],[11,1856],[11,1856],[11,1920],[11,1920],[11,1920],[11,1920],[12,2368],[12,2368],[12,2432],[12,2432],[12,2496],[12,2496],[12,2560],[12,2560],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[12,52],[12,52],[13,640],[13,704],[13,768],[13,832],[12,55],[12,55],[12,56],[12,56],[13,1280],[13,1344],[13,1408],[13,1472],[12,59],[12,59],[12,60],[12,60],[13,1536],[13,1600],[11,24],[11,24],[11,24],[11,24],[11,25],[11,25],[11,25],[11,25],[13,1664],[13,1728],[12,320],[12,320],[12,384],[12,384],[12,448],[12,448],[13,512],[13,576],[12,53],[12,53],[12,54],[12,54],[13,896],[13,960],[13,1024],[13,1088],[13,1152],[13,1216],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64]],Nt=[[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[11,23],[11,23],[12,50],[12,51],[12,44],[12,45],[12,46],[12,47],[12,57],[12,58],[12,61],[12,256],[10,16],[10,16],[10,16],[10,16],[10,17],[10,17],[10,17],[10,17],[12,48],[12,49],[12,62],[12,63],[12,30],[12,31],[12,32],[12,33],[12,40],[12,41],[11,22],[11,22],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[12,128],[12,192],[12,26],[12,27],[12,28],[12,29],[11,19],[11,19],[11,20],[11,20],[12,34],[12,35],[12,36],[12,37],[12,38],[12,39],[11,21],[11,21],[12,42],[12,43],[10,0],[10,0],[10,0],[10,0],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12]],Gt=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[6,9],[6,8],[5,7],[5,7],[4,6],[4,6],[4,6],[4,6],[4,5],[4,5],[4,5],[4,5],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2]];class CCITTFaxDecoder{constructor(e,t={}){if("function"!=typeof e?.next)throw new Error('CCITTFaxDecoder - invalid "source" parameter.');this.source=e;this.eof=!1;this.encoding=t.K||0;this.eoline=t.EndOfLine||!1;this.byteAlign=t.EncodedByteAlign||!1;this.columns=t.Columns||1728;this.rows=t.Rows||0;this.eoblock=t.EndOfBlock??!0;this.black=t.BlackIs1||!1;this.codingLine=new Uint32Array(this.columns+1);this.refLine=new Uint32Array(this.columns+2);this.codingLine[0]=this.columns;this.codingPos=0;this.row=0;this.nextLine2D=this.encoding<0;this.inputBits=0;this.inputBuf=0;this.outputBits=0;this.rowsDone=!1;let i;for(;0===(i=this._lookBits(12));)this._eatBits(1);1===i&&this._eatBits(12);if(this.encoding>0){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}}readNextChar(){if(this.eof)return-1;const e=this.refLine,t=this.codingLine,i=this.columns;let a,s,r,n,g;if(0===this.outputBits){this.rowsDone&&(this.eof=!0);if(this.eof)return-1;this.err=!1;let r,g,o;if(this.nextLine2D){for(n=0;t[n]=64);do{g+=o=this._getWhiteCode()}while(o>=64)}else{do{r+=o=this._getWhiteCode()}while(o>=64);do{g+=o=this._getBlackCode()}while(o>=64)}this._addPixels(t[this.codingPos]+r,s);t[this.codingPos]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]0?--a:++a;for(;e[a]<=t[this.codingPos]&&e[a]=64);else do{r+=o=this._getWhiteCode()}while(o>=64);this._addPixels(t[this.codingPos]+r,s);s^=1}}let c=!1;this.byteAlign&&(this.inputBits&=-8);if(this.eoblock||this.row!==this.rows-1){r=this._lookBits(12);if(this.eoline)for(;r!==bt&&1!==r;){this._eatBits(1);r=this._lookBits(12)}else for(;0===r;){this._eatBits(1);r=this._lookBits(12)}if(1===r){this._eatBits(12);c=!0}else r===bt&&(this.eof=!0)}else this.rowsDone=!0;if(!this.eof&&this.encoding>0&&!this.rowsDone){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}if(this.eoblock&&c&&this.byteAlign){r=this._lookBits(12);if(1===r){this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}if(this.encoding>=0)for(n=0;n<4;++n){r=this._lookBits(12);1!==r&&info("bad rtc code: "+r);this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}}this.eof=!0}}else if(this.err&&this.eoline){for(;;){r=this._lookBits(13);if(r===bt){this.eof=!0;return-1}if(r>>1==1)break;this._eatBits(1)}this._eatBits(12);if(this.encoding>0){this._eatBits(1);this.nextLine2D=!(1&r)}}this.outputBits=t[0]>0?t[this.codingPos=0]:t[this.codingPos=1];this.row++}if(this.outputBits>=8){g=1&this.codingPos?0:255;this.outputBits-=8;if(0===this.outputBits&&t[this.codingPos]r){g<<=r;1&this.codingPos||(g|=255>>8-r);this.outputBits-=r;r=0}else{g<<=this.outputBits;1&this.codingPos||(g|=255>>8-this.outputBits);r-=this.outputBits;this.outputBits=0;if(t[this.codingPos]0){g<<=r;r=0}}}while(r)}this.black&&(g^=255);return g}_addPixels(e,t){const i=this.codingLine;let a=this.codingPos;if(e>i[a]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&a^t&&++a;i[a]=e}this.codingPos=a}_addPixelsNeg(e,t){const i=this.codingLine;let a=this.codingPos;if(e>i[a]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&a^t&&++a;i[a]=e}else if(e0&&e=s){const t=i[e-s];if(t[0]===a){this._eatBits(a);return[!0,t[1],!0]}}}return[!1,0,!1]}_getTwoDimCode(){let e,t=0;if(this.eoblock){t=this._lookBits(7);e=Ft[t];if(e?.[0]>0){this._eatBits(e[0]);return e[1]}}else{const e=this._findTableCode(1,7,Ft);if(e[0]&&e[2])return e[1]}info("Bad two dim code");return bt}_getWhiteCode(){let e,t=0;if(this.eoblock){t=this._lookBits(12);if(t===bt)return 1;e=t>>5?kt[t>>3]:St[t];if(e[0]>0){this._eatBits(e[0]);return e[1]}}else{let e=this._findTableCode(1,9,kt);if(e[0])return e[1];e=this._findTableCode(11,12,St);if(e[0])return e[1]}info("bad white code");this._eatBits(1);return 1}_getBlackCode(){let e,t;if(this.eoblock){e=this._lookBits(13);if(e===bt)return 1;t=e>>7?!(e>>9)&&e>>7?Nt[(e>>1)-64]:Gt[e>>7]:Rt[e];if(t[0]>0){this._eatBits(t[0]);return t[1]}}else{let e=this._findTableCode(2,6,Gt);if(e[0])return e[1];e=this._findTableCode(7,12,Nt,64);if(e[0])return e[1];e=this._findTableCode(10,13,Rt);if(e[0])return e[1]}info("bad black code");this._eatBits(1);return 1}_lookBits(e){let t;for(;this.inputBits>16-e;this.inputBuf=this.inputBuf<<8|t;this.inputBits+=8}return this.inputBuf>>this.inputBits-e&65535>>16-e}_eatBits(e){(this.inputBits-=e)<0&&(this.inputBits=0)}}class CCITTFaxStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;i instanceof Dict||(i=Dict.empty);const a={next:()=>e.getByte()};this.ccittFaxDecoder=new CCITTFaxDecoder(a,{K:i.get("K"),EndOfLine:i.get("EndOfLine"),EncodedByteAlign:i.get("EncodedByteAlign"),Columns:i.get("Columns"),Rows:i.get("Rows"),EndOfBlock:i.get("EndOfBlock"),BlackIs1:i.get("BlackIs1")})}readBlock(){for(;!this.eof;){const e=this.ccittFaxDecoder.readNextChar();if(-1===e){this.eof=!0;return}this.ensureBuffer(this.bufferLength+1);this.buffer[this.bufferLength++]=e}}}const Mt=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Ut=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),xt=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),Lt=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],Ht=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5];class FlateStream extends DecodeStream{constructor(e,t){super(t);this.str=e;this.dict=e.dict;const i=e.getByte(),a=e.getByte();if(-1===i||-1===a)throw new FormatError(`Invalid header in flate stream: ${i}, ${a}`);if(8!=(15&i))throw new FormatError(`Unknown compression method in flate stream: ${i}, ${a}`);if(((i<<8)+a)%31!=0)throw new FormatError(`Bad FCHECK in flate stream: ${i}, ${a}`);if(32&a)throw new FormatError(`FDICT bit set in flate stream: ${i}, ${a}`);this.codeSize=0;this.codeBuf=0}async getImageData(e,t){const i=await this.asyncGetBytes();return i?.subarray(0,e)||this.getBytes(e)}async asyncGetBytes(){this.str.reset();const e=this.str.getBytes();try{const{readable:t,writable:i}=new DecompressionStream("deflate"),a=i.getWriter();await a.ready;a.write(e).then((async()=>{await a.ready;await a.close()})).catch((()=>{}));const s=[];let r=0;for await(const e of t){s.push(e);r+=e.byteLength}const n=new Uint8Array(r);let g=0;for(const e of s){n.set(e,g);g+=e.byteLength}return n}catch{this.str=new Stream(e,2,e.length,this.str.dict);this.reset();return null}}get isAsync(){return!0}getBits(e){const t=this.str;let i,a=this.codeSize,s=this.codeBuf;for(;a>e;this.codeSize=a-=e;return i}getCode(e){const t=this.str,i=e[0],a=e[1];let s,r=this.codeSize,n=this.codeBuf;for(;r>16,c=65535&g;if(o<1||r>o;this.codeSize=r-o;return c}generateHuffmanTable(e){const t=e.length;let i,a=0;for(i=0;ia&&(a=e[i]);const s=1<>=1}for(i=e;i>=1;if(0===t){let t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}let i=t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}i|=t<<8;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}let s=t;if(-1===(t=a.getByte())){this.#m("Bad block header in flate stream");return}s|=t<<8;if(s!==(65535&~i)&&(0!==i||0!==s))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const r=this.bufferLength,n=r+i;e=this.ensureBuffer(n);this.bufferLength=n;if(0===i)-1===a.peekByte()&&(this.eof=!0);else{const t=a.getBytes(i);e.set(t,r);t.length0;)C[g++]=Q}s=this.generateHuffmanTable(C.subarray(0,e));r=this.generateHuffmanTable(C.subarray(e,c))}}e=this.buffer;let n=e?e.length:0,g=this.bufferLength;for(;;){let t=this.getCode(s);if(t<256){if(g+1>=n){e=this.ensureBuffer(g+1);n=e.length}e[g++]=t;continue}if(256===t){this.bufferLength=g;return}t-=257;t=Ut[t];let a=t>>16;a>0&&(a=this.getBits(a));i=(65535&t)+a;t=this.getCode(r);t=xt[t];a=t>>16;a>0&&(a=this.getBits(a));const o=(65535&t)+a;if(g+i>=n){e=this.ensureBuffer(g+i);n=e.length}for(let t=0;t>9&127;this.clow=this.clow<<7&65535;this.ct-=7;this.a=32768}byteIn(){const e=this.data;let t=this.bp;if(255===e[t])if(e[t+1]>143){this.clow+=65280;this.ct=8}else{t++;this.clow+=e[t]<<9;this.ct=7;this.bp=t}else{t++;this.clow+=t65535){this.chigh+=this.clow>>16;this.clow&=65535}}readBit(e,t){let i=e[t]>>1,a=1&e[t];const s=Jt[i],r=s.qe;let n,g=this.a-r;if(this.chigh>15&1;this.clow=this.clow<<1&65535;this.ct--}while(!(32768&g));this.a=g;e[t]=i<<1|a;return n}}class Jbig2Error extends ot{constructor(e){super(e,"Jbig2Error")}}class ContextCache{getContexts(e){return e in this?this[e]:this[e]=new Int8Array(65536)}}class DecodingContext{constructor(e,t,i){this.data=e;this.start=t;this.end=i}get decoder(){return shadow(this,"decoder",new ArithmeticDecoder(this.data,this.start,this.end))}get contextCache(){return shadow(this,"contextCache",new ContextCache)}}function decodeInteger(e,t,i){const a=e.getContexts(t);let s=1;function readBits(e){let t=0;for(let r=0;r>>0}const r=readBits(1),n=readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(32)+4436:readBits(12)+340:readBits(8)+84:readBits(6)+20:readBits(4)+4:readBits(2);let g;0===r?g=n:n>0&&(g=-n);return g>=-2147483648&&g<=pt?g:null}function decodeIAID(e,t,i){const a=e.getContexts("IAID");let s=1;for(let e=0;e=F&&x=S){K=K<<1&d;for(u=0;u=0&&H=0){J=G[L][H];J&&(K|=J<=e?c<<=1:c=c<<1|w[g][o]}for(Q=0;Q=m||o<0||o>=p?c<<=1:c=c<<1|a[g][o]}const E=D.readBit(b,c);t[n]=E}}return w}function decodeTextRegion(e,t,i,a,s,r,n,g,o,c,C,h,l,Q,E,u,d,f,p){if(e&&t)throw new Jbig2Error("refinement with Huffman is not supported");const m=[];let y,w;for(y=0;y1&&(s=e?p.readBits(f):decodeInteger(b,"IAIT",D));const r=n*F+s,S=e?Q.symbolIDTable.decode(p):decodeIAID(b,D,o),k=t&&(e?p.readBit():decodeInteger(b,"IARI",D));let R=g[S],N=R[0].length,G=R.length;if(k){const e=decodeInteger(b,"IARDW",D),t=decodeInteger(b,"IARDH",D);N+=e;G+=t;R=decodeRefinement(N,G,E,R,(e>>1)+decodeInteger(b,"IARDX",D),(t>>1)+decodeInteger(b,"IARDY",D),!1,u,d)}let M=0;c?1&h?M=G-1:a+=G-1:h>1?a+=N-1:M=N-1;const U=r-(1&h?0:G-1),x=a-(2&h?N-1:0);let L,H,J;if(c)for(L=0;L>5&7;const o=[31&n];let c=t+6;if(7===n){g=536870911&readUint32(e,c-1);c+=3;let t=g+7>>3;o[0]=e[c++];for(;--t>0;)o.push(e[c++])}else if(5===n||6===n)throw new Jbig2Error("invalid referred-to flags");i.retainBits=o;let C=4;i.number<=256?C=1:i.number<=65536&&(C=2);const h=[];let l,Q;for(l=0;l>>24&255;r[3]=t.height>>16&255;r[4]=t.height>>8&255;r[5]=255&t.height;for(l=c,Q=e.length;l>2&3;e.huffmanDWSelector=t>>4&3;e.bitmapSizeSelector=t>>6&1;e.aggregationInstancesSelector=t>>7&1;e.bitmapCodingContextUsed=!!(256&t);e.bitmapCodingContextRetained=!!(512&t);e.template=t>>10&3;e.refinementTemplate=t>>12&1;c+=2;if(!e.huffman){o=0===e.template?4:1;n=[];for(g=0;g>2&3;C.stripSize=1<>4&3;C.transposed=!!(64&h);C.combinationOperator=h>>7&3;C.defaultPixelValue=h>>9&1;C.dsOffset=h<<17>>27;C.refinementTemplate=h>>15&1;if(C.huffman){const e=readUint16(a,c);c+=2;C.huffmanFS=3&e;C.huffmanDS=e>>2&3;C.huffmanDT=e>>4&3;C.huffmanRefinementDW=e>>6&3;C.huffmanRefinementDH=e>>8&3;C.huffmanRefinementDX=e>>10&3;C.huffmanRefinementDY=e>>12&3;C.huffmanRefinementSizeSelector=!!(16384&e)}if(C.refinement&&!C.refinementTemplate){n=[];for(g=0;g<2;g++){n.push({x:readInt8(a,c),y:readInt8(a,c+1)});c+=2}C.refinementAt=n}C.numberOfSymbolInstances=readUint32(a,c);c+=4;r=[C,i.referredTo,a,c,s];break;case 16:const l={},Q=a[c++];l.mmr=!!(1&Q);l.template=Q>>1&3;l.patternWidth=a[c++];l.patternHeight=a[c++];l.maxPatternIndex=readUint32(a,c);c+=4;r=[l,i.number,a,c,s];break;case 22:case 23:const E={};E.info=readRegionSegmentInformation(a,c);c+=Ot;const u=a[c++];E.mmr=!!(1&u);E.template=u>>1&3;E.enableSkip=!!(8&u);E.combinationOperator=u>>4&7;E.defaultPixelValue=u>>7&1;E.gridWidth=readUint32(a,c);c+=4;E.gridHeight=readUint32(a,c);c+=4;E.gridOffsetX=4294967295&readUint32(a,c);c+=4;E.gridOffsetY=4294967295&readUint32(a,c);c+=4;E.gridVectorX=readUint16(a,c);c+=2;E.gridVectorY=readUint16(a,c);c+=2;r=[E,i.referredTo,a,c,s];break;case 38:case 39:const d={};d.info=readRegionSegmentInformation(a,c);c+=Ot;const f=a[c++];d.mmr=!!(1&f);d.template=f>>1&3;d.prediction=!!(8&f);if(!d.mmr){o=0===d.template?4:1;n=[];for(g=0;g>2&1;p.combinationOperator=m>>3&3;p.requiresBuffer=!!(32&m);p.combinationOperatorOverride=!!(64&m);r=[p];break;case 49:case 50:case 51:case 62:break;case 53:r=[i.number,a,c,s];break;default:throw new Jbig2Error(`segment type ${i.typeName}(${i.type}) is not implemented`)}const C="on"+i.typeName;C in t&&t[C].apply(t,r)}function processSegments(e,t){for(let i=0,a=e.length;i>3,i=new Uint8ClampedArray(t*e.height);e.defaultPixelValue&&i.fill(255);this.buffer=i}drawBitmap(e,t){const i=this.currentPageInfo,a=e.width,s=e.height,r=i.width+7>>3,n=i.combinationOperatorOverride?e.combinationOperator:i.combinationOperator,g=this.buffer,o=128>>(7&e.x);let c,C,h,l,Q=e.y*r+(e.x>>3);switch(n){case 0:for(c=0;c>=1;if(!h){h=128;l++}}Q+=r}break;case 2:for(c=0;c>=1;if(!h){h=128;l++}}Q+=r}break;default:throw new Jbig2Error(`operator ${n} is not supported`)}}onImmediateGenericRegion(e,t,i,a){const s=e.info,r=new DecodingContext(t,i,a),n=decodeBitmap(e.mmr,s.width,s.height,e.template,e.prediction,null,e.at,r);this.drawBitmap(s,n)}onImmediateLosslessGenericRegion(){this.onImmediateGenericRegion(...arguments)}onSymbolDictionary(e,t,i,a,s,r){let n,g;if(e.huffman){n=function getSymbolDictionaryHuffmanTables(e,t,i){let a,s,r,n,g=0;switch(e.huffmanDHSelector){case 0:case 1:a=getStandardTable(e.huffmanDHSelector+4);break;case 3:a=getCustomHuffmanTable(g,t,i);g++;break;default:throw new Jbig2Error("invalid Huffman DH selector")}switch(e.huffmanDWSelector){case 0:case 1:s=getStandardTable(e.huffmanDWSelector+2);break;case 3:s=getCustomHuffmanTable(g,t,i);g++;break;default:throw new Jbig2Error("invalid Huffman DW selector")}if(e.bitmapSizeSelector){r=getCustomHuffmanTable(g,t,i);g++}else r=getStandardTable(1);n=e.aggregationInstancesSelector?getCustomHuffmanTable(g,t,i):getStandardTable(1);return{tableDeltaHeight:a,tableDeltaWidth:s,tableBitmapSize:r,tableAggregateInstances:n}}(e,i,this.customTables);g=new Reader(a,s,r)}let o=this.symbols;o||(this.symbols=o={});const c=[];for(const e of i){const t=o[e];t&&c.push(...t)}const C=new DecodingContext(a,s,r);o[t]=function decodeSymbolDictionary(e,t,i,a,s,r,n,g,o,c,C,h){if(e&&t)throw new Jbig2Error("symbol refinement with Huffman is not supported");const l=[];let Q=0,E=log2(i.length+a);const u=C.decoder,d=C.contextCache;let f,p;if(e){f=getStandardTable(1);p=[];E=Math.max(E,1)}for(;l.length1)m=decodeTextRegion(e,t,a,Q,0,s,1,i.concat(l),E,0,0,1,0,r,o,c,C,0,h);else{const e=decodeIAID(d,u,E),t=decodeInteger(d,"IARDX",u),s=decodeInteger(d,"IARDY",u);m=decodeRefinement(a,Q,o,e=32){let i,a,n;switch(t){case 32:if(0===e)throw new Jbig2Error("no previous value in symbol ID table");a=s.readBits(2)+3;i=r[e-1].prefixLength;break;case 33:a=s.readBits(3)+3;i=0;break;case 34:a=s.readBits(7)+11;i=0;break;default:throw new Jbig2Error("invalid code length in symbol ID table")}for(n=0;n=0;d--){R=e?decodeMMRBitmap(k,o,c,!0):decodeBitmap(!1,o,c,i,!1,null,F,E);S[d]=R}for(N=0;N=0;f--){M^=S[f][N][G];U|=M<>8;H=h+N*l-G*Q>>8;if(L>=0&&L+w<=a&&H>=0&&H+D<=s)for(d=0;d=s)){Y=u[t];J=x[d];for(f=0;f=0&&e>1&7),o=1+(a>>4&7),c=[];let C,h,l=s;do{C=n.readBits(g);h=n.readBits(o);c.push(new HuffmanLine([l,C,h,0]));l+=1<>t&1;if(t<=0)this.children[i]=new HuffmanTreeNode(e);else{let a=this.children[i];a||(this.children[i]=a=new HuffmanTreeNode(null));a.buildTree(e,t-1)}}decodeNode(e){if(this.isLeaf){if(this.isOOB)return null;const t=e.readBits(this.rangeLength);return this.rangeLow+(this.isLowerRange?-t:t)}const t=this.children[e.readBit()];if(!t)throw new Jbig2Error("invalid Huffman data");return t.decodeNode(e)}}class HuffmanTable{constructor(e,t){t||this.assignPrefixCodes(e);this.rootNode=new HuffmanTreeNode(null);for(let t=0,i=e.length;t0&&this.rootNode.buildTree(i,i.prefixLength-1)}}decode(e){return this.rootNode.decodeNode(e)}assignPrefixCodes(e){const t=e.length;let i=0;for(let a=0;a=this.end)throw new Jbig2Error("end of data while reading bit");this.currentByte=this.data[this.position++];this.shift=7}const e=this.currentByte>>this.shift&1;this.shift--;return e}readBits(e){let t,i=0;for(t=e-1;t>=0;t--)i|=this.readBit()<=this.end?-1:this.data[this.position++]}}function getCustomHuffmanTable(e,t,i){let a=0;for(let s=0,r=t.length;s>i&1;i--}}if(a&&!g){const e=5;for(let t=0;t>2,c=new Uint32Array(e.buffer,t,o);if(FeatureTest.isLittleEndian){for(;n>>24|t<<8|4278190080;i[a+2]=t>>>16|s<<16|4278190080;i[a+3]=s>>>8|4278190080}for(let s=4*n,r=t+g;s>>8|255;i[a+2]=t<<16|s>>>16|255;i[a+3]=s<<8|255}for(let s=4*n,r=t+g;s>3,h=7&a,l=e.length;i=new Uint32Array(i.buffer);let Q=0;for(let a=0;a0&&!e[r-1];)r--;const n=[{children:[],index:0}];let g,o=n[0];for(i=0;i0;)o=n.pop();o.index++;n.push(o);for(;n.length<=i;){n.push(g={children:[],index:0});o.children[o.index]=g.children;o=g}s++}if(i+10){E--;return Q>>E&1}Q=e[t++];if(255===Q){const a=e[t++];if(a){if(220===a&&c){const a=readUint16(e,t+=2);t+=2;if(a>0&&a!==i.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",a)}else if(217===a){if(c){const e=p*(8===i.precision?8:0);if(e>0&&Math.round(i.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(Q<<8|a).toString(16)}`)}}E=7;return Q>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){u--;return}let i=r;const a=n;for(;i<=a;){const a=decodeHuffman(e.huffmanTableAC),s=15&a,r=a>>4;if(0===s){if(r<15){u=receive(r)+(1<>4;if(0===s)if(c<15){u=receive(c)+(1<>4;if(0===a){if(r<15)break;s+=16;continue}s+=r;const n=Wt[s];e.blockData[t+n]=receiveAndExtend(a);s++}};let k,R=0;const N=1===m?a[0].blocksPerLine*a[0].blocksPerColumn:C*i.mcusPerColumn;let G,M;for(;R<=N;){const i=s?Math.min(N-R,s):N;if(i>0){for(w=0;w0?"unexpected":"excessive"} MCU data, current marker is: ${k.invalid}`);t=k.offset}if(!(k.marker>=65488&&k.marker<=65495))break;t+=2}return t-l}function quantizeAndInverse(e,t,i){const a=e.quantizationTable,s=e.blockData;let r,n,g,o,c,C,h,l,Q,E,u,d,f,p,m,y,w;if(!a)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){Q=s[t+e];E=s[t+e+1];u=s[t+e+2];d=s[t+e+3];f=s[t+e+4];p=s[t+e+5];m=s[t+e+6];y=s[t+e+7];Q*=a[e];if(E|u|d|f|p|m|y){E*=a[e+1];u*=a[e+2];d*=a[e+3];f*=a[e+4];p*=a[e+5];m*=a[e+6];y*=a[e+7];r=$t*Q+128>>8;n=$t*f+128>>8;g=u;o=m;c=Ai*(E-y)+128>>8;l=Ai*(E+y)+128>>8;C=d<<4;h=p<<4;r=r+n+1>>1;n=r-n;w=g*_t+o*zt+128>>8;g=g*zt-o*_t+128>>8;o=w;c=c+h+1>>1;h=c-h;l=l+C+1>>1;C=l-C;r=r+o+1>>1;o=r-o;n=n+g+1>>1;g=n-g;w=c*Vt+l*Zt+2048>>12;c=c*Zt-l*Vt+2048>>12;l=w;w=C*Xt+h*jt+2048>>12;C=C*jt-h*Xt+2048>>12;h=w;i[e]=r+l;i[e+7]=r-l;i[e+1]=n+h;i[e+6]=n-h;i[e+2]=g+C;i[e+5]=g-C;i[e+3]=o+c;i[e+4]=o-c}else{w=$t*Q+512>>10;i[e]=w;i[e+1]=w;i[e+2]=w;i[e+3]=w;i[e+4]=w;i[e+5]=w;i[e+6]=w;i[e+7]=w}}for(let e=0;e<8;++e){Q=i[e];E=i[e+8];u=i[e+16];d=i[e+24];f=i[e+32];p=i[e+40];m=i[e+48];y=i[e+56];if(E|u|d|f|p|m|y){r=$t*Q+2048>>12;n=$t*f+2048>>12;g=u;o=m;c=Ai*(E-y)+2048>>12;l=Ai*(E+y)+2048>>12;C=d;h=p;r=4112+(r+n+1>>1);n=r-n;w=g*_t+o*zt+2048>>12;g=g*zt-o*_t+2048>>12;o=w;c=c+h+1>>1;h=c-h;l=l+C+1>>1;C=l-C;r=r+o+1>>1;o=r-o;n=n+g+1>>1;g=n-g;w=c*Vt+l*Zt+2048>>12;c=c*Zt-l*Vt+2048>>12;l=w;w=C*Xt+h*jt+2048>>12;C=C*jt-h*Xt+2048>>12;h=w;Q=r+l;y=r-l;E=n+h;m=n-h;u=g+C;p=g-C;d=o+c;f=o-c;Q<16?Q=0:Q>=4080?Q=255:Q>>=4;E<16?E=0:E>=4080?E=255:E>>=4;u<16?u=0:u>=4080?u=255:u>>=4;d<16?d=0:d>=4080?d=255:d>>=4;f<16?f=0:f>=4080?f=255:f>>=4;p<16?p=0:p>=4080?p=255:p>>=4;m<16?m=0:m>=4080?m=255:m>>=4;y<16?y=0:y>=4080?y=255:y>>=4;s[t+e]=Q;s[t+e+8]=E;s[t+e+16]=u;s[t+e+24]=d;s[t+e+32]=f;s[t+e+40]=p;s[t+e+48]=m;s[t+e+56]=y}else{w=$t*Q+8192>>14;w=w<-2040?0:w>=2024?255:w+2056>>4;s[t+e]=w;s[t+e+8]=w;s[t+e+16]=w;s[t+e+24]=w;s[t+e+32]=w;s[t+e+40]=w;s[t+e+48]=w;s[t+e+56]=w}}}function buildComponentData(e,t){const i=t.blocksPerLine,a=t.blocksPerColumn,s=new Int16Array(64);for(let e=0;e=a)return null;const r=readUint16(e,t);if(r>=65472&&r<=65534)return{invalid:null,marker:r,offset:t};let n=readUint16(e,s);for(;!(n>=65472&&n<=65534);){if(++s>=a)return null;n=readUint16(e,s)}return{invalid:r.toString(16),marker:n,offset:s}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),i=Math.ceil(e.scanLines/8/e.maxV);for(const a of e.components){const s=Math.ceil(Math.ceil(e.samplesPerLine/8)*a.h/e.maxH),r=Math.ceil(Math.ceil(e.scanLines/8)*a.v/e.maxV),n=t*a.h,g=64*(i*a.v)*(n+1);a.blockData=new Int16Array(g);a.blocksPerLine=s;a.blocksPerColumn=r}e.mcusPerLine=t;e.mcusPerColumn=i}function readDataBlock(e,t){const i=readUint16(e,t);let a=(t+=2)+i-2;const s=findNextFileMarker(e,a,t);if(s?.invalid){warn("readDataBlock - incorrect length, current marker is: "+s.invalid);a=s.offset}const r=e.subarray(t,a);return{appData:r,newOffset:t+=r.length}}function skipData(e,t){const i=readUint16(e,t),a=(t+=2)+i-2,s=findNextFileMarker(e,a,t);return s?.invalid?s.offset:a}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){let i=0,a=null,s=readUint16(e,i);i+=2;if(65496!==s)throw new JpegError("SOI not found");s=readUint16(e,i);i+=2;A:for(;65497!==s;){switch(s){case 65472:case 65473:case 65474:a=e[i+7];break A;case 65535:255!==e[i]&&i--}i=skipData(e,i);s=readUint16(e,i);i+=2}return 4!==a&&(3!==a||0!==t)}parse(e,{dnlScanLines:t=null}={}){let i,a,s=0,r=null,n=null,g=0;const o=[],c=[],C=[];let h=readUint16(e,s);s+=2;if(65496!==h)throw new JpegError("SOI not found");h=readUint16(e,s);s+=2;A:for(;65497!==h;){let l,Q,E;switch(h){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:u,newOffset:d}=readDataBlock(e,s);s=d;65504===h&&74===u[0]&&70===u[1]&&73===u[2]&&70===u[3]&&0===u[4]&&(r={version:{major:u[5],minor:u[6]},densityUnits:u[7],xDensity:u[8]<<8|u[9],yDensity:u[10]<<8|u[11],thumbWidth:u[12],thumbHeight:u[13],thumbData:u.subarray(14,14+3*u[12]*u[13])});65518===h&&65===u[0]&&100===u[1]&&111===u[2]&&98===u[3]&&101===u[4]&&(n={version:u[5]<<8|u[6],flags0:u[7]<<8|u[8],flags1:u[9]<<8|u[10],transformCode:u[11]});break;case 65499:const f=readUint16(e,s);s+=2;const p=f+s-2;let m;for(;s>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(Q=0;Q<64;Q++){m=Wt[Q];i[m]=readUint16(e,s);s+=2}}else for(Q=0;Q<64;Q++){m=Wt[Q];i[m]=e[s++]}o[15&t]=i}break;case 65472:case 65473:case 65474:if(i)throw new JpegError("Only single frame JPEGs supported");s+=2;i={};i.extended=65473===h;i.progressive=65474===h;i.precision=e[s++];const y=readUint16(e,s);s+=2;i.scanLines=t||y;i.samplesPerLine=readUint16(e,s);s+=2;i.components=[];i.componentIds={};const w=e[s++];let D=0,b=0;for(l=0;l>4,r=15&e[s+1];D>4?c:C)[15&t]=buildHuffmanTable(i,r)}break;case 65501:s+=2;a=readUint16(e,s);s+=2;break;case 65498:const S=1==++g&&!t;s+=2;const k=e[s++],R=[];for(l=0;l>4];r.huffmanTableAC=c[15&n];R.push(r)}const N=e[s++],G=e[s++],M=e[s++];try{s+=decodeScan(e,s,i,R,a,N,G,M>>4,15&M,S)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break A}throw t}break;case 65500:s+=4;break;case 65535:255!==e[s]&&s--;break;default:const U=findNextFileMarker(e,s-2,s-3);if(U?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+U.invalid);s=U.offset;break}if(!U||s>=e.length-1){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break A}throw new JpegError("JpegImage.parse - unknown marker: "+h.toString(16))}h=readUint16(e,s);s+=2}if(!i)throw new JpegError("JpegImage.parse - no frame data found.");this.width=i.samplesPerLine;this.height=i.scanLines;this.jfif=r;this.adobe=n;this.components=[];for(const e of i.components){const t=o[e.quantizationId];t&&(e.quantizationTable=t);this.components.push({index:e.index,output:buildComponentData(0,e),scaleX:e.h/i.maxH,scaleY:e.v/i.maxV,blocksPerLine:e.blocksPerLine,blocksPerColumn:e.blocksPerColumn})}this.numComponents=this.components.length}_getLinearizedBlockData(e,t,i=!1){const a=this.width/e,s=this.height/t;let r,n,g,o,c,C,h,l,Q,E,u,d=0;const f=this.components.length,p=e*t*f,m=new Uint8ClampedArray(p),y=new Uint32Array(e),w=4294967288;let D;for(h=0;h>8)+b[Q+1];return m}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,i,a;for(let s=0,r=e.length;s4)throw new JpegError("Unsupported color mode");const r=this._getLinearizedBlockData(e,t,s);if(1===this.numComponents&&(i||a)){const e=r.length*(i?4:3),t=new Uint8ClampedArray(e);let a=0;if(i)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let i=0,a=e.length;i0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#w(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const i=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=i;this.bufferLength=i.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const i=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!i)return null;const a=this.#w(i);if(!JpegImage.canUseImageDecoder(a,e.colorTransform))return null;t=new ImageDecoder({data:a,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}}var ei,ti=(ei="undefined"!=typeof document?document.currentScript?.src:void 0,function(e={}){var t,i,a=e;new Promise(((e,a)=>{t=e;i=a}));a.decode=function(e,{numComponents:t=4,isIndexedColormap:i=!1,smaskInData:s=!1}){const r=e.length,n=a._malloc(r);a.HEAPU8.set(e,n);const g=a._jp2_decode(n,r,t>0?t:0,!!i,!!s);a._free(n);if(g){const{errorMessages:e}=a;if(e){delete a.errorMessages;return e}return"Unknown error"}const{imageData:o}=a;a.imageData=null;return o};var s=Object.assign({},a),r="./this.program",quit_=(e,t)=>{throw t},n="";"undefined"!=typeof document&&document.currentScript&&(n=document.currentScript.src);ei&&(n=ei);n=n.startsWith("blob:")?"":n.substr(0,n.replace(/[?#].*/,"").lastIndexOf("/")+1);var g=a.print||console.log.bind(console),o=a.printErr||console.error.bind(console);Object.assign(a,s);s=null;a.arguments&&a.arguments;a.thisProgram&&(r=a.thisProgram);var c,C=a.wasmBinary;function tryParseAsDataURI(e){if(isDataURI(e))return function intArrayFromBase64(e){for(var t=atob(e),i=new Uint8Array(t.length),a=0;ae.startsWith(b);function instantiateSync(e,t){var i,a=function getBinarySync(e){if(e==d&&C)return new Uint8Array(C);var t=tryParseAsDataURI(e);if(t)return t;throw'sync fetching of the wasm failed: you can preload it to Module["wasmBinary"] manually, or emcc.py will do that for you when generating HTML (but not JS)'}(e);i=new WebAssembly.Module(a);return[new WebAssembly.Instance(i,t),i]}class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var F,callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(a)},S=a.noExitRuntime||!0,k=0,R={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return h;quit_(0,e)},keepRuntimeAlive=()=>S||k>0,_proc_exit=e=>{h=e;if(!keepRuntimeAlive()){a.onExit?.(e);u=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{h=e;_proc_exit(e)},callUserCallback=e=>{if(!u)try{e();(()=>{if(!keepRuntimeAlive())try{_exit(h)}catch(e){handleException(e)}})()}catch(e){handleException(e)}},growMemory=e=>{var t=(e-c.buffer.byteLength+65535)/65536|0;try{c.grow(t);updateMemoryViews();return 1}catch(e){}},N={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:r||"./this.program"};for(var t in N)void 0===N[t]?delete e[t]:e[t]=N[t];var i=[];for(var t in e)i.push(`${t}=${e[t]}`);getEnvStrings.strings=i}return getEnvStrings.strings},G=[null,[],[]],M="undefined"!=typeof TextDecoder?new TextDecoder:void 0,UTF8ArrayToString=(e,t=0,i=NaN)=>{for(var a=t+i,s=t;e[s]&&!(s>=a);)++s;if(s-t>16&&e.buffer&&M)return M.decode(e.subarray(t,s));for(var r="";t>10,56320|1023&c)}}else r+=String.fromCharCode((31&n)<<6|g)}else r+=String.fromCharCode(n)}return r},printChar=(e,t)=>{var i=G[e];if(0===t||10===t){(1===e?g:o)(UTF8ArrayToString(i));i.length=0}else i.push(t)},UTF8ToString=(e,t)=>e?UTF8ArrayToString(Q,e,t):"",U={m:()=>function abort(e){a.onAbort?.(e);o(e="Aborted("+e+")");u=!0;e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);i(t);throw t}(""),c:(e,t,i)=>Q.copyWithin(e,t,t+i),l:()=>{S=!1;k=0},n:(e,t)=>{if(R[e]){clearTimeout(R[e].id);delete R[e]}if(!t)return 0;var i=setTimeout((()=>{delete R[e];callUserCallback((()=>L(e,performance.now())))}),t);R[e]={id:i,timeout_ms:t};return 0},g:function _copy_pixels_1(e,t){e>>=2;const i=a.imageData=new Uint8ClampedArray(t),s=a.HEAP32.subarray(e,e+t);i.set(s)},f:function _copy_pixels_3(e,t,i,s){e>>=2;t>>=2;i>>=2;const r=a.imageData=new Uint8ClampedArray(3*s),n=a.HEAP32.subarray(e,e+s),g=a.HEAP32.subarray(t,t+s),o=a.HEAP32.subarray(i,i+s);for(let e=0;e>=2;t>>=2;i>>=2;s>>=2;const n=a.imageData=new Uint8ClampedArray(4*r),g=a.HEAP32.subarray(e,e+r),o=a.HEAP32.subarray(t,t+r),c=a.HEAP32.subarray(i,i+r),C=a.HEAP32.subarray(s,s+r);for(let e=0;e{var t,i,a=Q.length,s=2147483648;if((e>>>=0)>s)return!1;for(var r=1;r<=4;r*=2){var n=a*(1+.2/r);n=Math.min(n,e+100663296);var g=Math.min(s,(t=Math.max(e,n),i=65536,Math.ceil(t/i)*i));if(growMemory(g))return!0}return!1},p:(e,t)=>{var i=0;getEnvStrings().forEach(((a,s)=>{var r=t+i;E[e+4*s>>2]=r;((e,t)=>{for(var i=0;i{var i=getEnvStrings();E[e>>2]=i.length;var a=0;i.forEach((e=>a+=e.length+1));E[t>>2]=a;return 0},r:e=>52,j:function _fd_seek(e,t,i,a,s){return 70},b:(e,t,i,a)=>{for(var s=0,r=0;r>2],g=E[t+4>>2];t+=8;for(var o=0;o>2]=s;return 0},s:function _gray_to_rgba(e,t){e>>=2;const i=a.imageData=new Uint8ClampedArray(4*t),s=a.HEAP32.subarray(e,e+t);for(let e=0;e>=2;t>>=2;const s=a.imageData=new Uint8ClampedArray(4*i),r=a.HEAP32.subarray(e,e+i),n=a.HEAP32.subarray(t,t+i);for(let e=0;e>=2;t>>=2;i>>=2;const r=a.imageData=new Uint8ClampedArray(4*s),n=a.HEAP32.subarray(e,e+s),g=a.HEAP32.subarray(t,t+s),o=a.HEAP32.subarray(i,i+s);for(let e=0;e0)){!function preRun(){if(a.preRun){"function"==typeof a.preRun&&(a.preRun=[a.preRun]);for(;a.preRun.length;)e=a.preRun.shift(),f.unshift(e)}var e;callRuntimeCallbacks(f)}();if(!(y>0))if(a.setStatus){a.setStatus("Running...");setTimeout((()=>{setTimeout((()=>a.setStatus("")),1);doRun()}),1)}else doRun()}function doRun(){if(!F){F=!0;a.calledRun=!0;if(!u){!function initRuntime(){callRuntimeCallbacks(p)}();t(a);a.onRuntimeInitialized?.();!function postRun(){if(a.postRun){"function"==typeof a.postRun&&(a.postRun=[a.postRun]);for(;a.postRun.length;)e=a.postRun.shift(),m.unshift(e)}var e;callRuntimeCallbacks(m)}()}}}}if(a.preInit){"function"==typeof a.preInit&&(a.preInit=[a.preInit]);for(;a.preInit.length>0;)a.preInit.pop()()}run();return a});const ii=ti;class JpxError extends ot{constructor(e){super(e,"JpxError")}}class JpxImage{static#D=null;static decode(e,t){t||={};this.#D||=ii({warn});const i=this.#D.decode(e,t);if("string"==typeof i)throw new JpxError(i);return i}static cleanup(){this.#D=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const i=t;t=e.getByte();if(65361===(i<<8|t)){e.skip(4);const t=e.getInt32()>>>0,i=e.getInt32()>>>0,a=e.getInt32()>>>0,s=e.getInt32()>>>0;e.skip(16);return{width:t-a,height:i-s,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}class JpxStream extends DecodeStream{constructor(e,t,i){super(t);this.stream=e;this.dict=e.dict;this.maybeLength=t;this.params=i}get bytes(){return shadow(this,"bytes",this.stream.getBytes(this.maybeLength))}ensureBuffer(e){}readBlock(e){this.decodeImage(null,e)}decodeImage(e,t){if(this.eof)return this.buffer;e||=this.bytes;this.buffer=JpxImage.decode(e,t);this.bufferLength=this.buffer.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}}class LZWStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;this.cachedData=0;this.bitsCached=0;const a=4096,s={earlyChange:i,codeLength:9,nextCode:258,dictionaryValues:new Uint8Array(a),dictionaryLengths:new Uint16Array(a),dictionaryPrevCodes:new Uint16Array(a),currentSequence:new Uint8Array(a),currentSequenceLength:0};for(let e=0;e<256;++e){s.dictionaryValues[e]=e;s.dictionaryLengths[e]=1}this.lzwState=s}readBits(e){let t=this.bitsCached,i=this.cachedData;for(;t>>t&(1<0;if(e<256){l[0]=e;Q=1}else{if(!(e>=258)){if(256===e){C=9;n=258;Q=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){l[t]=g[i];i=c[i]}}else l[Q++]=l[0]}if(s){c[n]=h;o[n]=o[h]+1;g[n]=l[0];n++;C=n+r&n+r-1?C:0|Math.min(Math.log(n+r)/.6931471805599453+1,12)}h=e;E+=Q;if(a15))throw new FormatError(`Unsupported predictor: ${a}`);this.readBlock=2===a?this.readBlockTiff:this.readBlockPng;this.str=e;this.dict=e.dict;const s=this.colors=i.get("Colors")||1,r=this.bits=i.get("BPC","BitsPerComponent")||8,n=this.columns=i.get("Columns")||1;this.pixBytes=s*r+7>>3;this.rowBytes=n*s*r+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,i=this.ensureBuffer(t+e),a=this.bits,s=this.colors,r=this.str.getBytes(e);this.eof=!r.length;if(this.eof)return;let n,g=0,o=0,c=0,C=0,h=t;if(1===a&&1===s)for(n=0;n>1;e^=e>>2;e^=e>>4;g=(1&e)<<7;i[h++]=e}else if(8===a){for(n=0;n>8&255;i[h++]=255&e}}else{const e=new Uint8Array(s+1),h=(1<>c-a)&h;c-=a;o=o<=8){i[Q++]=o>>C-8&255;C-=8}}C>0&&(i[Q++]=(o<<8-C)+(g&(1<<8-C)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,i=this.str.getByte(),a=this.str.getBytes(e);this.eof=!a.length;if(this.eof)return;const s=this.bufferLength,r=this.ensureBuffer(s+e);let n=r.subarray(s-e,s);0===n.length&&(n=new Uint8Array(e));let g,o,c,C=s;switch(i){case 0:for(g=0;g>1)+a[g];for(;g>1)+a[g]&255;C++}break;case 4:for(g=0;g0){const e=this.str.getBytes(a);t.set(e,i);i+=a}}else{a=257-a;const s=e[1];t=this.ensureBuffer(i+a+1);for(let e=0;e>")&&this.buf1!==Bt;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===Bt)break;a.set(t,this.getObj(e))}if(this.buf1===Bt){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(a,e):a;this.shift();return a;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,i=e.pos;let a,s,r=0;for(;-1!==(a=e.getByte());)if(0===r)r=69===a?1:0;else if(1===r)r=73===a?2:0;else if(32===a||10===a||13===a){s=e.pos;const i=e.peekBytes(15),n=i.length;if(0===n)break;for(let e=0;e127))){r=0;break}}if(2!==r)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const g=new Lexer(new Stream(i.slice()),t);g._hexStringWarn=()=>{};let o=0;for(;;){const e=g.getObj();if(e===Bt){r=0;break}if(e instanceof Cmd){const i=t[e.cmd];if(!i){r=0;break}if(i.variableArgs?o<=i.numArgs:o===i.numArgs)break;o=0}else o++}if(2===r)break}else r=0;if(-1===a){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(s){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-s))}}let n=4;e.skip(-n);a=e.peekByte();e.skip(n);isWhiteSpace(a)||n--;return e.pos-n-i}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let i,a,s=!1;for(;-1!==(i=e.getByte());)if(255===i){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:s=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:a=e.getUint16();a>2?e.skip(a-2):e.skip(-2)}if(s)break}const r=e.pos-t;if(-1===i){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let i;for(;-1!==(i=e.getByte());)if(126===i){const t=e.pos;i=e.peekByte();for(;isWhiteSpace(i);){e.skip();i=e.peekByte()}if(62===i){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const a=e.pos-t;if(-1===i){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-a);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return a}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let i;for(;-1!==(i=e.getByte())&&62!==i;);const a=e.pos-t;if(-1===i){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-a);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return a}inlineStreamSkipEI(e){let t,i=0;for(;-1!==(t=e.getByte());)if(0===i)i=69===t?1:0;else if(1===i)i=73===t?2:0;else if(2===i)break}makeInlineImage(e){const t=this.lexer,i=t.stream,a=Object.create(null);let s;for(;!isCmd(this.buf1,"ID")&&this.buf1!==Bt;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===Bt)break;a[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(s=i.pos-t.beginInlineImagePos);const r=this.xref.fetchIfRef(a.F||a.Filter);let n;if(r instanceof Name)n=r.name;else if(Array.isArray(r)){const e=this.xref.fetchIfRef(r[0]);e instanceof Name&&(n=e.name)}const g=i.pos;let o,c;switch(n){case"DCT":case"DCTDecode":o=this.findDCTDecodeInlineStreamEnd(i);break;case"A85":case"ASCII85Decode":o=this.findASCII85DecodeInlineStreamEnd(i);break;case"AHx":case"ASCIIHexDecode":o=this.findASCIIHexDecodeInlineStreamEnd(i);break;default:o=this.findDefaultInlineStreamEnd(i)}if(o<1e3&&s>0){const e=i.pos;i.pos=t.beginInlineImagePos;c=function getInlineImageCacheKey(e){const t=[],i=e.length;let a=0;for(;a=a){let a=!1;for(const e of s){const t=e.length;let s=0;for(;s=r){a=!0;break}if(s>=t){if(isWhiteSpace(n[o+g+s])){info(`Found "${bytesToString([...i,...e])}" when searching for endstream command.`);a=!0}break}}if(a){t.pos+=o;return t.pos-e}}o++}t.pos+=g}return-1}makeStream(e,t){const i=this.lexer;let a=i.stream;i.skipToNextLine();const s=a.pos-1;let r=e.get("Length");if(!Number.isInteger(r)){info(`Bad length "${r&&r.toString()}" in stream.`);r=0}a.pos=s+r;i.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{r=this.#b(s);if(r<0)throw new FormatError("Missing endstream command.");i.nextChar();this.shift();this.shift()}this.shift();a=a.makeSubStream(s,r,e);t&&(a=t.createStream(a,r));a=this.filter(a,e,r);a.dict=e;return a}filter(e,t,i){let a=t.get("F","Filter"),s=t.get("DP","DecodeParms");if(a instanceof Name){Array.isArray(s)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,a.name,i,s)}let r=i;if(Array.isArray(a)){const t=a,i=s;for(let n=0,g=t.length;n=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=!1,i=0,a=1;if(45===e){a=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){i=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let s=e-48,r=0,n=1;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){const a=e-48;if(t)r=10*r+a;else{0!==i&&(i*=10);s=10*s+a}}else if(46===e){if(0!==i)break;i=1}else if(45===e)warn("Badly formatted number: minus sign in the middle");else{if(69!==e&&101!==e)break;e=this.peekChar();if(43===e||45===e){n=45===e?-1:1;this.nextChar()}else if(e<48||e>57)break;t=!0}0!==i&&(s/=i);t&&(s*=10**(n*r));return a*s}getString(){let e=1,t=!1;const i=this.strBuf;i.length=0;let a=this.nextChar();for(;;){let s=!1;switch(0|a){case-1:warn("Unterminated string");t=!0;break;case 40:++e;i.push("(");break;case 41:if(0==--e){this.nextChar();t=!0}else i.push(")");break;case 92:a=this.nextChar();switch(a){case-1:warn("Unterminated string");t=!0;break;case 110:i.push("\n");break;case 114:i.push("\r");break;case 116:i.push("\t");break;case 98:i.push("\b");break;case 102:i.push("\f");break;case 92:case 40:case 41:i.push(String.fromCharCode(a));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&a;a=this.nextChar();s=!0;if(a>=48&&a<=55){e=(e<<3)+(15&a);a=this.nextChar();if(a>=48&&a<=55){s=!1;e=(e<<3)+(15&a)}}i.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:i.push(String.fromCharCode(a))}break;default:i.push(String.fromCharCode(a))}if(t)break;s||(a=this.nextChar())}return i.join("")}getName(){let e,t;const i=this.strBuf;i.length=0;for(;(e=this.nextChar())>=0&&!ai[e];)if(35===e){e=this.nextChar();if(ai[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");i.push("#");break}const a=toHexDigit(e);if(-1!==a){t=e;e=this.nextChar();const s=toHexDigit(e);if(-1===s){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);i.push("#",String.fromCharCode(t));if(ai[e])break;i.push(String.fromCharCode(e));continue}i.push(String.fromCharCode(a<<4|s))}else i.push("#",String.fromCharCode(e))}else i.push(String.fromCharCode(e));i.length>127&&warn(`Name token is longer than allowed by the spec: ${i.length}`);return Name.get(i.join(""))}_hexStringWarn(e){5!=this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,i=-1,a=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==ai[t]){a=toHexDigit(t);if(-1===a)this._hexStringWarn(t);else if(-1===i)i=a;else{e.push(String.fromCharCode(i<<4|a));i=-1}t=this.nextChar()}else t=this.nextChar()}-1!==i&&e.push(String.fromCharCode(i<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return Bt;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==ai[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let i=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(i)}}const a=this.knownCommands;let s=void 0!==a?.[i];for(;(t=this.nextChar())>=0&&!ai[t];){const e=i+String.fromCharCode(t);if(s&&void 0===a[e])break;if(128===i.length)throw new FormatError(`Command token too long: ${i.length}`);i=e;s=void 0!==a?.[i]}if("true"===i)return!0;if("false"===i)return!1;if("null"===i)return null;"BI"===i&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(i)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,i=!1){const a=e.get(t);if(Number.isInteger(a)&&(i?a>=0:a>0))return a;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),i=t.getObj(),a=t.getObj(),s=t.getObj(),r=t.getObj();let n,g;if(!(Number.isInteger(i)&&Number.isInteger(a)&&isCmd(s,"obj")&&r instanceof Dict&&"number"==typeof(n=r.get("Linearized"))&&n>0))return null;if((g=getInt(r,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:g,hints:function getHints(e){const t=e.get("H");let i;if(Array.isArray(t)&&(2===(i=t.length)||4===i)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(r),objectNumberFirst:getInt(r,"O"),endFirst:getInt(r,"E"),numPages:getInt(r,"N"),mainXRefEntriesOffset:getInt(r,"T"),pageFirst:r.has("P")?getInt(r,"P",!0):0}}}const si=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],ri=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,i){this.codespaceRanges[e-1].push(t,i);this.numCodespaceRanges++}mapCidRange(e,t,i){if(t-e>ri)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=i++}mapBfRange(e,t,i){if(t-e>ri)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const a=i.length-1;for(;e<=t;){this._map[e++]=i;const t=i.charCodeAt(a)+1;t>255?i=i.substring(0,a-1)+String.fromCharCode(i.charCodeAt(a-1)+1)+"\0":i=i.substring(0,a)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,i){if(t-e>ri)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const a=i.length;let s=0;for(;e<=t&&s>>0;const n=s[r];for(let e=0,t=n.length;e=t&&a<=s){i.charcode=a;i.length=r+1;return}}}i.charcode=0;i.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let i=0,a=t.length;i=s&&e<=r)return i+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,i){unreachable("should not call mapCidRange")}mapBfRange(e,t,i){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,i){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let i=0;i>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endbfchar"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=i;e.mapOne(a,s)}}function parseBfRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endbfrange"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=strToInt(i);i=t.getObj();if(Number.isInteger(i)||"string"==typeof i){const t=Number.isInteger(i)?String.fromCharCode(i):i;e.mapBfRange(a,s,t)}else{if(!isCmd(i,"["))break;{i=t.getObj();const r=[];for(;!isCmd(i,"]")&&i!==Bt;){r.push(i);i=t.getObj()}e.mapBfRangeToArray(a,s,r)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcidchar"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectInt(i);const s=i;e.mapOne(a,s)}}function parseCidRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcidrange"))return;expectString(i);const a=strToInt(i);i=t.getObj();expectString(i);const s=strToInt(i);i=t.getObj();expectInt(i);const r=i;e.mapCidRange(a,s,r)}}function parseCodespaceRange(e,t){for(;;){let i=t.getObj();if(i===Bt)break;if(isCmd(i,"endcodespacerange"))return;if("string"!=typeof i)break;const a=strToInt(i);i=t.getObj();if("string"!=typeof i)break;const s=strToInt(i);e.addCodespaceRange(i.length,a,s)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const i=t.getObj();Number.isInteger(i)&&(e.vertical=!!i)}function parseCMapName(e,t){const i=t.getObj();i instanceof Name&&(e.name=i.name)}async function parseCMap(e,t,i,a){let s,r;A:for(;;)try{const i=t.getObj();if(i===Bt)break;if(i instanceof Name){"WMode"===i.name?parseWMode(e,t):"CMapName"===i.name&&parseCMapName(e,t);s=i}else if(i instanceof Cmd)switch(i.cmd){case"endcmap":break A;case"usecmap":s instanceof Name&&(r=s.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!a&&r&&(a=r);return a?extendCMap(e,i,a):e}async function extendCMap(e,t,i){e.useCMap=await createBuiltInCMap(i,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let i=0;iextendCMap(s,t,e)));const r=new Lexer(new Stream(i));return parseCMap(s,r,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:i}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){const a=await parseCMap(new CMap,new Lexer(e),t,i);return a.isIdentityCMap?createBuiltInCMap(a.name,t):a}throw new Error("Encoding required.")}}const ni=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],gi=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],oi=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],Ii=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","","asuperior","bsuperior","centsuperior","dsuperior","esuperior","","","","isuperior","","","lsuperior","msuperior","nsuperior","osuperior","","","rsuperior","ssuperior","tsuperior","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdownsmall","centoldstyle","Lslashsmall","","","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","","Dotaccentsmall","","","Macronsmall","","","figuredash","hypheninferior","","","Ogoneksmall","Ringsmall","Cedillasmall","","","","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],ci=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","centoldstyle","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","","threequartersemdash","","questionsmall","","","","","Ethsmall","","","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","","","","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hypheninferior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","asuperior","centsuperior","","","","","Aacutesmall","Agravesmall","Acircumflexsmall","Adieresissmall","Atildesmall","Aringsmall","Ccedillasmall","Eacutesmall","Egravesmall","Ecircumflexsmall","Edieresissmall","Iacutesmall","Igravesmall","Icircumflexsmall","Idieresissmall","Ntildesmall","Oacutesmall","Ogravesmall","Ocircumflexsmall","Odieresissmall","Otildesmall","Uacutesmall","Ugravesmall","Ucircumflexsmall","Udieresissmall","","eightsuperior","fourinferior","threeinferior","sixinferior","eightinferior","seveninferior","Scaronsmall","","centinferior","twoinferior","","Dieresissmall","","Caronsmall","osuperior","fiveinferior","","commainferior","periodinferior","Yacutesmall","","dollarinferior","","","Thornsmall","","nineinferior","zeroinferior","Zcaronsmall","AEsmall","Oslashsmall","questiondownsmall","oneinferior","Lslashsmall","","","","","","","Cedillasmall","","","","","","OEsmall","figuredash","hyphensuperior","","","","","exclamdownsmall","","Ydieresissmall","","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","ninesuperior","zerosuperior","","esuperior","rsuperior","tsuperior","","","isuperior","ssuperior","dsuperior","","","","","","lsuperior","Ogoneksmall","Brevesmall","Macronsmall","bsuperior","nsuperior","msuperior","commasuperior","periodsuperior","Dotaccentsmall","Ringsmall","","","",""],Ci=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","space","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron"],hi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","","endash","dagger","daggerdbl","periodcentered","","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","","questiondown","","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","","ring","cedilla","","hungarumlaut","ogonek","caron","emdash","","","","","","","","","","","","","","","","","AE","","ordfeminine","","","","","Lslash","Oslash","OE","ordmasculine","","","","","","ae","","","","dotlessi","","","lslash","oslash","oe","germandbls","","","",""],li=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","bullet","Euro","bullet","quotesinglbase","florin","quotedblbase","ellipsis","dagger","daggerdbl","circumflex","perthousand","Scaron","guilsinglleft","OE","bullet","Zcaron","bullet","bullet","quoteleft","quoteright","quotedblleft","quotedblright","bullet","endash","emdash","tilde","trademark","scaron","guilsinglright","oe","bullet","zcaron","Ydieresis","space","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis","copyright","ordfeminine","guillemotleft","logicalnot","hyphen","registered","macron","degree","plusminus","twosuperior","threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright","onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth","Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae","ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis","yacute","thorn","ydieresis"],Bi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","universal","numbersign","existential","percent","ampersand","suchthat","parenleft","parenright","asteriskmath","plus","comma","minus","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","congruent","Alpha","Beta","Chi","Delta","Epsilon","Phi","Gamma","Eta","Iota","theta1","Kappa","Lambda","Mu","Nu","Omicron","Pi","Theta","Rho","Sigma","Tau","Upsilon","sigma1","Omega","Xi","Psi","Zeta","bracketleft","therefore","bracketright","perpendicular","underscore","radicalex","alpha","beta","chi","delta","epsilon","phi","gamma","eta","iota","phi1","kappa","lambda","mu","nu","omicron","pi","theta","rho","sigma","tau","upsilon","omega1","omega","xi","psi","zeta","braceleft","bar","braceright","similar","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Euro","Upsilon1","minute","lessequal","fraction","infinity","florin","club","diamond","heart","spade","arrowboth","arrowleft","arrowup","arrowright","arrowdown","degree","plusminus","second","greaterequal","multiply","proportional","partialdiff","bullet","divide","notequal","equivalence","approxequal","ellipsis","arrowvertex","arrowhorizex","carriagereturn","aleph","Ifraktur","Rfraktur","weierstrass","circlemultiply","circleplus","emptyset","intersection","union","propersuperset","reflexsuperset","notsubset","propersubset","reflexsubset","element","notelement","angle","gradient","registerserif","copyrightserif","trademarkserif","product","radical","dotmath","logicalnot","logicaland","logicalor","arrowdblboth","arrowdblleft","arrowdblup","arrowdblright","arrowdbldown","lozenge","angleleft","registersans","copyrightsans","trademarksans","summation","parenlefttp","parenleftex","parenleftbt","bracketlefttp","bracketleftex","bracketleftbt","bracelefttp","braceleftmid","braceleftbt","braceex","","angleright","integral","integraltp","integralex","integralbt","parenrighttp","parenrightex","parenrightbt","bracketrighttp","bracketrightex","bracketrightbt","bracerighttp","bracerightmid","bracerightbt",""],Qi=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","a1","a2","a202","a3","a4","a5","a119","a118","a117","a11","a12","a13","a14","a15","a16","a105","a17","a18","a19","a20","a21","a22","a23","a24","a25","a26","a27","a28","a6","a7","a8","a9","a10","a29","a30","a31","a32","a33","a34","a35","a36","a37","a38","a39","a40","a41","a42","a43","a44","a45","a46","a47","a48","a49","a50","a51","a52","a53","a54","a55","a56","a57","a58","a59","a60","a61","a62","a63","a64","a65","a66","a67","a68","a69","a70","a71","a72","a73","a74","a203","a75","a204","a76","a77","a78","a79","a81","a82","a83","a84","a97","a98","a99","a100","","a89","a90","a93","a94","a91","a92","a205","a85","a206","a86","a87","a88","a95","a96","","","","","","","","","","","","","","","","","","","","a101","a102","a103","a104","a106","a107","a108","a112","a111","a110","a109","a120","a121","a122","a123","a124","a125","a126","a127","a128","a129","a130","a131","a132","a133","a134","a135","a136","a137","a138","a139","a140","a141","a142","a143","a144","a145","a146","a147","a148","a149","a150","a151","a152","a153","a154","a155","a156","a157","a158","a159","a160","a161","a163","a164","a196","a165","a192","a166","a167","a168","a169","a170","a171","a172","a173","a162","a174","a175","a176","a177","a178","a179","a193","a180","a199","a181","a200","a182","","a201","a183","a184","a197","a185","a194","a198","a186","a195","a187","a188","a189","a190","a191",""];function getEncoding(e){switch(e){case"WinAnsiEncoding":return li;case"StandardEncoding":return hi;case"MacRomanEncoding":return Ci;case"SymbolSetEncoding":return Bi;case"ZapfDingbatsEncoding":return Qi;case"ExpertEncoding":return Ii;case"MacExpertEncoding":return ci;default:return null}}const Ei=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],ui=391,di=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1,undefStack:!0},{id:"return",min:0,undefStack:!0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1,undefStack:!0},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],fi=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,i){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!i}parse(){const e=this.properties,t=new CFF;this.cff=t;const i=this.parseHeader(),a=this.parseIndex(i.endPos),s=this.parseIndex(a.endPos),r=this.parseIndex(s.endPos),n=this.parseIndex(r.endPos),g=this.parseDict(s.obj.get(0)),o=this.createDict(CFFTopDict,g,t.strings);t.header=i.obj;t.names=this.parseNameIndex(a.obj);t.strings=this.parseStringIndex(r.obj);t.topDict=o;t.globalSubrIndex=n.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=o.hasName("ROS");const c=o.getByName("CharStrings"),C=this.parseIndex(c).obj,h=o.getByName("FontMatrix");h&&(e.fontMatrix=h);const l=o.getByName("FontBBox");if(l){e.ascent=Math.max(l[3],l[1]);e.descent=Math.min(l[1],l[3]);e.ascentScaled=!0}let Q,E;if(t.isCIDFont){const e=this.parseIndex(o.getByName("FDArray")).obj;for(let i=0,a=e.count;i=t)throw new FormatError("Invalid CFF header");if(0!==i){info("cff data is shifted");e=e.subarray(i);this.bytes=e}const a=e[0],s=e[1],r=e[2],n=e[3];return{obj:new CFFHeader(a,s,r,n),endPos:r}}parseDict(e){let t=0;function parseOperand(){let i=e[t++];if(30===i)return function parseFloatOperand(){let i="";const a=15,s=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],r=e.length;for(;t>4,g=15&r;if(n===a)break;i+=s[n];if(g===a)break;i+=s[g]}return parseFloat(i)}();if(28===i){i=e[t++];i=(i<<24|e[t++]<<16)>>16;return i}if(29===i){i=e[t++];i=i<<8|e[t++];i=i<<8|e[t++];i=i<<8|e[t++];return i}if(i>=32&&i<=246)return i-139;if(i>=247&&i<=250)return 256*(i-247)+e[t++]+108;if(i>=251&&i<=254)return-256*(i-251)-e[t++]-108;warn('CFFParser_parseDict: "'+i+'" is a reserved command.');return NaN}let i=[];const a=[];t=0;const s=e.length;for(;t10)return!1;let s=e.stackSize;const r=e.stack;let n=t.length;for(let g=0;g>16;g+=2;s++}else if(14===o){if(s>=4){s-=4;if(this.seacAnalysisEnabled){e.seac=r.slice(s,s+4);return!1}}c=di[o]}else if(o>=32&&o<=246){r[s]=o-139;s++}else if(o>=247&&o<=254){r[s]=o<251?(o-247<<8)+t[g]+108:-(o-251<<8)-t[g]-108;g++;s++}else if(255===o){r[s]=(t[g]<<24|t[g+1]<<16|t[g+2]<<8|t[g+3])/65536;g+=4;s++}else if(19===o||20===o){e.hints+=s>>1;if(0===e.hints){t.copyWithin(g-1,g,-1);g-=1;n-=1;continue}g+=e.hints+7>>3;s%=2;c=di[o]}else{if(10===o||29===o){const t=10===o?i:a;if(!t){c=di[o];warn("Missing subrsIndex for "+c.id);return!1}let n=32768;t.count<1240?n=107:t.count<33900&&(n=1131);const g=r[--s]+n;if(g<0||g>=t.count||isNaN(g)){c=di[o];warn("Out of bounds subrIndex for "+c.id);return!1}e.stackSize=s;e.callDepth++;if(!this.parseCharString(e,t.get(g),i,a))return!1;e.callDepth--;s=e.stackSize;continue}if(11===o){e.stackSize=s;return!0}if(0===o&&g===t.length){t[g-1]=14;c=di[14]}else{if(9===o){t.copyWithin(g-1,g,-1);g-=1;n-=1;continue}c=di[o]}}if(c){if(c.stem){e.hints+=s>>1;if(3===o||23===o)e.hasVStems=!0;else if(e.hasVStems&&(1===o||18===o)){warn("CFF stem hints are in wrong order");t[g-1]=1===o?3:23}}if("min"in c&&!e.undefStack&&s=2&&c.stem?s%=2:s>1&&warn("Found too many parameters for stack-clearing command");s>0&&(e.width=r[s-1])}if("stackDelta"in c){"stackFn"in c&&c.stackFn(r,s);s+=c.stackDelta}else if(c.stackClearing)s=0;else if(c.resetStack){s=0;e.undefStack=!1}else if(c.undefStack){s=0;e.undefStack=!0;e.firstStackClearing=!1}}}n=s.length){warn("Invalid fd index for glyph index.");h=!1}if(h){Q=s[e].privateDict;l=Q.subrsIndex}}else t&&(l=t);h&&(h=this.parseCharString(C,o,l,i));if(null!==C.width){const e=Q.getByName("nominalWidthX");g[c]=e+C.width}else{const e=Q.getByName("defaultWidthX");g[c]=e}null!==C.seac&&(n[c]=C.seac);h||e.set(c,new Uint8Array([14]))}return{charStrings:e,seacs:n,widths:g}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const i=t[0],a=t[1];if(0===i||a>=this.bytes.length){this.emptyPrivateDictionary(e);return}const s=a+i,r=this.bytes.subarray(a,s),n=this.parseDict(r),g=this.createDict(CFFPrivateDict,n,e.strings);e.privateDict=g;0===g.getByName("ExpansionFactor")&&g.setByName("ExpansionFactor",.06);if(!g.getByName("Subrs"))return;const o=g.getByName("Subrs"),c=a+o;if(0===o||c>=this.bytes.length){this.emptyPrivateDictionary(e);return}const C=this.parseIndex(c);g.subrsIndex=C.obj}parseCharsets(e,t,i,a){if(0===e)return new CFFCharset(!0,yi.ISO_ADOBE,ni);if(1===e)return new CFFCharset(!0,yi.EXPERT,gi);if(2===e)return new CFFCharset(!0,yi.EXPERT_SUBSET,oi);const s=this.bytes,r=e,n=s[e++],g=[a?0:".notdef"];let o,c,C;t-=1;switch(n){case 0:for(C=0;C=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,i,a){this.major=e;this.minor=t;this.hdrSize=i;this.offSize=a}}class CFFStrings{constructor(){this.strings=[]}get(e){return e>=0&&e<=390?Ei[e]:e-ui<=this.strings.length?this.strings[e-ui]:Ei[0]}getSID(e){let t=Ei.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+ui:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{constructor(){this.objects=[];this.length=0}add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const i of t)if(isNaN(i)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const i=this.types[e];"num"!==i&&"sid"!==i&&"offset"!==i||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const i of e){const e=Array.isArray(i[0])?(i[0][0]<<8)+i[0][1]:i[0];t.keyToNameMap[e]=i[1];t.nameToKeyMap[i[1]]=e;t.types[e]=i[2];t.defaults[e]=i[3];t.opcodes[e]=Array.isArray(i[0])?i[0]:[i[0]];t.order.push(e)}return t}}const pi=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(pi))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const mi=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(mi))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const yi={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,i,a){this.predefined=e;this.format=t;this.charset=i;this.raw=a}}class CFFEncoding{constructor(e,t,i,a){this.predefined=e;this.format=t;this.encoding=i;this.raw=a}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{constructor(){this.offsets=Object.create(null)}isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,i){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const a=i.data,s=this.offsets[e];for(let e=0,i=t.length;e>24&255;a[n]=c>>16&255;a[g]=c>>8&255;a[o]=255&c}}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t={data:[],length:0,add(e){try{this.data.push(...e)}catch{this.data=this.data.concat(e)}this.length=this.data.length}},i=this.compileHeader(e.header);t.add(i);const a=this.compileNameIndex(e.names);t.add(a);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const i of e.fdArray){let e=t.slice(0);i.hasName("FontMatrix")&&(e=Util.transform(e,i.getByName("FontMatrix")));i.setByName("FontMatrix",e)}}const s=e.topDict.getByName("XUID");s?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let r=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(r.output);const n=r.trackers[0],g=this.compileStringIndex(e.strings.strings);t.add(g);const o=this.compileIndex(e.globalSubrIndex);t.add(o);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)n.setEntryLocation("Encoding",[e.encoding.format],t);else{const i=this.compileEncoding(e.encoding);n.setEntryLocation("Encoding",[t.length],t);t.add(i)}const c=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);n.setEntryLocation("charset",[t.length],t);t.add(c);const C=this.compileCharStrings(e.charStrings);n.setEntryLocation("CharStrings",[t.length],t);t.add(C);if(e.isCIDFont){n.setEntryLocation("FDSelect",[t.length],t);const i=this.compileFDSelect(e.fdSelect);t.add(i);r=this.compileTopDicts(e.fdArray,t.length,!0);n.setEntryLocation("FDArray",[t.length],t);t.add(r.output);const a=r.trackers;this.compilePrivateDicts(e.fdArray,a,t)}this.compilePrivateDicts([e.topDict],[n],t);t.add([0]);return t.data}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const i=CFFCompiler.EncodeFloatRegExp.exec(t);if(i){const a=parseFloat("1e"+((i[2]?+i[2]:0)+i[1].length));t=(Math.round(e*a)/a).toString()}let a,s,r="";for(a=0,s=t.length;a=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const i of e){const e=Math.min(i.length,127);let a=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");a[t]=e}a=a.join("");""===a&&(a="Bad_Font_Name");t.add(stringToBytes(a))}return this.compileIndex(t)}compileTopDicts(e,t,i){const a=[];let s=new CFFIndex;for(const r of e){if(i){r.removeByName("CIDFontVersion");r.removeByName("CIDFontRevision");r.removeByName("CIDFontType");r.removeByName("CIDCount");r.removeByName("UIDBase")}const e=new CFFOffsetTracker,n=this.compileDict(r,e);a.push(e);s.add(n);e.offset(t)}s=this.compileIndex(s,a);return{trackers:a,output:s}}compilePrivateDicts(e,t,i){for(let a=0,s=e.length;a>8&255,255&r]);else{s=new Uint8Array(1+2*r);s[0]=0;let t=0;const a=e.charset.length;let n=!1;for(let r=1;r>8&255;s[r+1]=255&g}}return this.compileTypedArray(s)}compileEncoding(e){return this.compileTypedArray(e.raw)}compileFDSelect(e){const t=e.format;let i,a;switch(t){case 0:i=new Uint8Array(1+e.fdSelect.length);i[0]=t;for(a=0;a>8&255,255&s,r];for(a=1;a>8&255,255&a,t);r=t}}const g=(n.length-3)/3;n[1]=g>>8&255;n[2]=255&g;n.push(a>>8&255,255&a);i=new Uint8Array(n)}return this.compileTypedArray(i)}compileTypedArray(e){return Array.from(e)}compileIndex(e,t=[]){const i=e.objects,a=i.length;if(0===a)return[0,0];const s=[a>>8&255,255&a];let r,n,g=1;for(r=0;r>8&255,255&o):3===n?s.push(o>>16&255,o>>8&255,255&o):s.push(o>>>24&255,o>>16&255,o>>8&255,255&o);i[r]&&(o+=i[r].length)}for(r=0;r=5&&t<=7))return-1;a=e.substring(1)}if(a===a.toUpperCase()){i=parseInt(a,16);if(i>=0)return i}}return-1}const Fi=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const i=Fi[t];for(let a=0,s=i.length;a=i[a]&&e<=i[a+1])return t}for(let t=0,i=Fi.length;t=i[a]&&e<=i[a+1])return t}return-1}const Si=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),ki=new Map;const Ri=!0,Ni=1,Gi=2,Mi=4,xi=32,Hi=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const i=getUnicodeForGlyph(e,t);if(-1!==i)for(const e in t)if(t[e]===i)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,i){const a=Object.create(null);let s,r,n;const g=!!(e.flags&Mi);if(e.isInternalFont){n=t;for(r=0;r=0?s:0}}else if(e.baseEncodingName){n=getEncoding(e.baseEncodingName);for(r=0;r=0?s:0}}else if(g)for(r in t)a[r]=t[r];else{n=hi;for(r=0;r=0?s:0}}const o=e.differences;let c;if(o)for(r in o){const e=o[r];s=i.indexOf(e);if(-1===s){c||(c=wi());const t=recoverGlyphName(e,c);t!==e&&(s=i.indexOf(t))}a[r]=s>=0?s:0}return a}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const Ji=getLookupTableFactory((e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080})),Yi=getLookupTableFactory((function(e){e["Times-Roman"]="Times-Roman";e.Helvetica="Helvetica";e.Courier="Courier";e.Symbol="Symbol";e["Times-Bold"]="Times-Bold";e["Helvetica-Bold"]="Helvetica-Bold";e["Courier-Bold"]="Courier-Bold";e.ZapfDingbats="ZapfDingbats";e["Times-Italic"]="Times-Italic";e["Helvetica-Oblique"]="Helvetica-Oblique";e["Courier-Oblique"]="Courier-Oblique";e["Times-BoldItalic"]="Times-BoldItalic";e["Helvetica-BoldOblique"]="Helvetica-BoldOblique";e["Courier-BoldOblique"]="Courier-BoldOblique";e.ArialNarrow="Helvetica";e["ArialNarrow-Bold"]="Helvetica-Bold";e["ArialNarrow-BoldItalic"]="Helvetica-BoldOblique";e["ArialNarrow-Italic"]="Helvetica-Oblique";e.ArialBlack="Helvetica";e["ArialBlack-Bold"]="Helvetica-Bold";e["ArialBlack-BoldItalic"]="Helvetica-BoldOblique";e["ArialBlack-Italic"]="Helvetica-Oblique";e["Arial-Black"]="Helvetica";e["Arial-Black-Bold"]="Helvetica-Bold";e["Arial-Black-BoldItalic"]="Helvetica-BoldOblique";e["Arial-Black-Italic"]="Helvetica-Oblique";e.Arial="Helvetica";e["Arial-Bold"]="Helvetica-Bold";e["Arial-BoldItalic"]="Helvetica-BoldOblique";e["Arial-Italic"]="Helvetica-Oblique";e.ArialMT="Helvetica";e["Arial-BoldItalicMT"]="Helvetica-BoldOblique";e["Arial-BoldMT"]="Helvetica-Bold";e["Arial-ItalicMT"]="Helvetica-Oblique";e["Arial-BoldItalicMT-BoldItalic"]="Helvetica-BoldOblique";e["Arial-BoldMT-Bold"]="Helvetica-Bold";e["Arial-ItalicMT-Italic"]="Helvetica-Oblique";e.ArialUnicodeMS="Helvetica";e["ArialUnicodeMS-Bold"]="Helvetica-Bold";e["ArialUnicodeMS-BoldItalic"]="Helvetica-BoldOblique";e["ArialUnicodeMS-Italic"]="Helvetica-Oblique";e["Courier-BoldItalic"]="Courier-BoldOblique";e["Courier-Italic"]="Courier-Oblique";e.CourierNew="Courier";e["CourierNew-Bold"]="Courier-Bold";e["CourierNew-BoldItalic"]="Courier-BoldOblique";e["CourierNew-Italic"]="Courier-Oblique";e["CourierNewPS-BoldItalicMT"]="Courier-BoldOblique";e["CourierNewPS-BoldMT"]="Courier-Bold";e["CourierNewPS-ItalicMT"]="Courier-Oblique";e.CourierNewPSMT="Courier";e["Helvetica-BoldItalic"]="Helvetica-BoldOblique";e["Helvetica-Italic"]="Helvetica-Oblique";e["HelveticaLTStd-Bold"]="Helvetica-Bold";e["Symbol-Bold"]="Symbol";e["Symbol-BoldItalic"]="Symbol";e["Symbol-Italic"]="Symbol";e.TimesNewRoman="Times-Roman";e["TimesNewRoman-Bold"]="Times-Bold";e["TimesNewRoman-BoldItalic"]="Times-BoldItalic";e["TimesNewRoman-Italic"]="Times-Italic";e.TimesNewRomanPS="Times-Roman";e["TimesNewRomanPS-Bold"]="Times-Bold";e["TimesNewRomanPS-BoldItalic"]="Times-BoldItalic";e["TimesNewRomanPS-BoldItalicMT"]="Times-BoldItalic";e["TimesNewRomanPS-BoldMT"]="Times-Bold";e["TimesNewRomanPS-Italic"]="Times-Italic";e["TimesNewRomanPS-ItalicMT"]="Times-Italic";e.TimesNewRomanPSMT="Times-Roman";e["TimesNewRomanPSMT-Bold"]="Times-Bold";e["TimesNewRomanPSMT-BoldItalic"]="Times-BoldItalic";e["TimesNewRomanPSMT-Italic"]="Times-Italic"})),vi=getLookupTableFactory((function(e){e.Courier="FoxitFixed.pfb";e["Courier-Bold"]="FoxitFixedBold.pfb";e["Courier-BoldOblique"]="FoxitFixedBoldItalic.pfb";e["Courier-Oblique"]="FoxitFixedItalic.pfb";e.Helvetica="LiberationSans-Regular.ttf";e["Helvetica-Bold"]="LiberationSans-Bold.ttf";e["Helvetica-BoldOblique"]="LiberationSans-BoldItalic.ttf";e["Helvetica-Oblique"]="LiberationSans-Italic.ttf";e["Times-Roman"]="FoxitSerif.pfb";e["Times-Bold"]="FoxitSerifBold.pfb";e["Times-BoldItalic"]="FoxitSerifBoldItalic.pfb";e["Times-Italic"]="FoxitSerifItalic.pfb";e.Symbol="FoxitSymbol.pfb";e.ZapfDingbats="FoxitDingbats.pfb";e["LiberationSans-Regular"]="LiberationSans-Regular.ttf";e["LiberationSans-Bold"]="LiberationSans-Bold.ttf";e["LiberationSans-Italic"]="LiberationSans-Italic.ttf";e["LiberationSans-BoldItalic"]="LiberationSans-BoldItalic.ttf"})),Ki=getLookupTableFactory((function(e){e.Calibri="Helvetica";e["Calibri-Bold"]="Helvetica-Bold";e["Calibri-BoldItalic"]="Helvetica-BoldOblique";e["Calibri-Italic"]="Helvetica-Oblique";e.CenturyGothic="Helvetica";e["CenturyGothic-Bold"]="Helvetica-Bold";e["CenturyGothic-BoldItalic"]="Helvetica-BoldOblique";e["CenturyGothic-Italic"]="Helvetica-Oblique";e.ComicSansMS="Comic Sans MS";e["ComicSansMS-Bold"]="Comic Sans MS-Bold";e["ComicSansMS-BoldItalic"]="Comic Sans MS-BoldItalic";e["ComicSansMS-Italic"]="Comic Sans MS-Italic";e.GillSansMT="Helvetica";e["GillSansMT-Bold"]="Helvetica-Bold";e["GillSansMT-BoldItalic"]="Helvetica-BoldOblique";e["GillSansMT-Italic"]="Helvetica-Oblique";e.Impact="Helvetica";e["ItcSymbol-Bold"]="Helvetica-Bold";e["ItcSymbol-BoldItalic"]="Helvetica-BoldOblique";e["ItcSymbol-Book"]="Helvetica";e["ItcSymbol-BookItalic"]="Helvetica-Oblique";e["ItcSymbol-Medium"]="Helvetica";e["ItcSymbol-MediumItalic"]="Helvetica-Oblique";e.LucidaConsole="Courier";e["LucidaConsole-Bold"]="Courier-Bold";e["LucidaConsole-BoldItalic"]="Courier-BoldOblique";e["LucidaConsole-Italic"]="Courier-Oblique";e["LucidaSans-Demi"]="Helvetica-Bold";e["MS-Gothic"]="MS Gothic";e["MS-Gothic-Bold"]="MS Gothic-Bold";e["MS-Gothic-BoldItalic"]="MS Gothic-BoldItalic";e["MS-Gothic-Italic"]="MS Gothic-Italic";e["MS-Mincho"]="MS Mincho";e["MS-Mincho-Bold"]="MS Mincho-Bold";e["MS-Mincho-BoldItalic"]="MS Mincho-BoldItalic";e["MS-Mincho-Italic"]="MS Mincho-Italic";e["MS-PGothic"]="MS PGothic";e["MS-PGothic-Bold"]="MS PGothic-Bold";e["MS-PGothic-BoldItalic"]="MS PGothic-BoldItalic";e["MS-PGothic-Italic"]="MS PGothic-Italic";e["MS-PMincho"]="MS PMincho";e["MS-PMincho-Bold"]="MS PMincho-Bold";e["MS-PMincho-BoldItalic"]="MS PMincho-BoldItalic";e["MS-PMincho-Italic"]="MS PMincho-Italic";e.NuptialScript="Times-Italic";e.SegoeUISymbol="Helvetica"})),Ti=getLookupTableFactory((function(e){e["Adobe Jenson"]=!0;e["Adobe Text"]=!0;e.Albertus=!0;e.Aldus=!0;e.Alexandria=!0;e.Algerian=!0;e["American Typewriter"]=!0;e.Antiqua=!0;e.Apex=!0;e.Arno=!0;e.Aster=!0;e.Aurora=!0;e.Baskerville=!0;e.Bell=!0;e.Bembo=!0;e["Bembo Schoolbook"]=!0;e.Benguiat=!0;e["Berkeley Old Style"]=!0;e["Bernhard Modern"]=!0;e["Berthold City"]=!0;e.Bodoni=!0;e["Bauer Bodoni"]=!0;e["Book Antiqua"]=!0;e.Bookman=!0;e["Bordeaux Roman"]=!0;e["Californian FB"]=!0;e.Calisto=!0;e.Calvert=!0;e.Capitals=!0;e.Cambria=!0;e.Cartier=!0;e.Caslon=!0;e.Catull=!0;e.Centaur=!0;e["Century Old Style"]=!0;e["Century Schoolbook"]=!0;e.Chaparral=!0;e["Charis SIL"]=!0;e.Cheltenham=!0;e["Cholla Slab"]=!0;e.Clarendon=!0;e.Clearface=!0;e.Cochin=!0;e.Colonna=!0;e["Computer Modern"]=!0;e["Concrete Roman"]=!0;e.Constantia=!0;e["Cooper Black"]=!0;e.Corona=!0;e.Ecotype=!0;e.Egyptienne=!0;e.Elephant=!0;e.Excelsior=!0;e.Fairfield=!0;e["FF Scala"]=!0;e.Folkard=!0;e.Footlight=!0;e.FreeSerif=!0;e["Friz Quadrata"]=!0;e.Garamond=!0;e.Gentium=!0;e.Georgia=!0;e.Gloucester=!0;e["Goudy Old Style"]=!0;e["Goudy Schoolbook"]=!0;e["Goudy Pro Font"]=!0;e.Granjon=!0;e["Guardian Egyptian"]=!0;e.Heather=!0;e.Hercules=!0;e["High Tower Text"]=!0;e.Hiroshige=!0;e["Hoefler Text"]=!0;e["Humana Serif"]=!0;e.Imprint=!0;e["Ionic No. 5"]=!0;e.Janson=!0;e.Joanna=!0;e.Korinna=!0;e.Lexicon=!0;e.LiberationSerif=!0;e["Liberation Serif"]=!0;e["Linux Libertine"]=!0;e.Literaturnaya=!0;e.Lucida=!0;e["Lucida Bright"]=!0;e.Melior=!0;e.Memphis=!0;e.Miller=!0;e.Minion=!0;e.Modern=!0;e["Mona Lisa"]=!0;e["Mrs Eaves"]=!0;e["MS Serif"]=!0;e["Museo Slab"]=!0;e["New York"]=!0;e["Nimbus Roman"]=!0;e["NPS Rawlinson Roadway"]=!0;e.NuptialScript=!0;e.Palatino=!0;e.Perpetua=!0;e.Plantin=!0;e["Plantin Schoolbook"]=!0;e.Playbill=!0;e["Poor Richard"]=!0;e["Rawlinson Roadway"]=!0;e.Renault=!0;e.Requiem=!0;e.Rockwell=!0;e.Roman=!0;e["Rotis Serif"]=!0;e.Sabon=!0;e.Scala=!0;e.Seagull=!0;e.Sistina=!0;e.Souvenir=!0;e.STIX=!0;e["Stone Informal"]=!0;e["Stone Serif"]=!0;e.Sylfaen=!0;e.Times=!0;e.Trajan=!0;e["Trinité"]=!0;e["Trump Mediaeval"]=!0;e.Utopia=!0;e["Vale Type"]=!0;e["Bitstream Vera"]=!0;e["Vera Serif"]=!0;e.Versailles=!0;e.Wanted=!0;e.Weiss=!0;e["Wide Latin"]=!0;e.Windsor=!0;e.XITS=!0})),qi=getLookupTableFactory((function(e){e.Dingbats=!0;e.Symbol=!0;e.ZapfDingbats=!0;e.Wingdings=!0;e["Wingdings-Bold"]=!0;e["Wingdings-Regular"]=!0})),Oi=getLookupTableFactory((function(e){e[2]=10;e[3]=32;e[4]=33;e[5]=34;e[6]=35;e[7]=36;e[8]=37;e[9]=38;e[10]=39;e[11]=40;e[12]=41;e[13]=42;e[14]=43;e[15]=44;e[16]=45;e[17]=46;e[18]=47;e[19]=48;e[20]=49;e[21]=50;e[22]=51;e[23]=52;e[24]=53;e[25]=54;e[26]=55;e[27]=56;e[28]=57;e[29]=58;e[30]=894;e[31]=60;e[32]=61;e[33]=62;e[34]=63;e[35]=64;e[36]=65;e[37]=66;e[38]=67;e[39]=68;e[40]=69;e[41]=70;e[42]=71;e[43]=72;e[44]=73;e[45]=74;e[46]=75;e[47]=76;e[48]=77;e[49]=78;e[50]=79;e[51]=80;e[52]=81;e[53]=82;e[54]=83;e[55]=84;e[56]=85;e[57]=86;e[58]=87;e[59]=88;e[60]=89;e[61]=90;e[62]=91;e[63]=92;e[64]=93;e[65]=94;e[66]=95;e[67]=96;e[68]=97;e[69]=98;e[70]=99;e[71]=100;e[72]=101;e[73]=102;e[74]=103;e[75]=104;e[76]=105;e[77]=106;e[78]=107;e[79]=108;e[80]=109;e[81]=110;e[82]=111;e[83]=112;e[84]=113;e[85]=114;e[86]=115;e[87]=116;e[88]=117;e[89]=118;e[90]=119;e[91]=120;e[92]=121;e[93]=122;e[94]=123;e[95]=124;e[96]=125;e[97]=126;e[98]=196;e[99]=197;e[100]=199;e[101]=201;e[102]=209;e[103]=214;e[104]=220;e[105]=225;e[106]=224;e[107]=226;e[108]=228;e[109]=227;e[110]=229;e[111]=231;e[112]=233;e[113]=232;e[114]=234;e[115]=235;e[116]=237;e[117]=236;e[118]=238;e[119]=239;e[120]=241;e[121]=243;e[122]=242;e[123]=244;e[124]=246;e[125]=245;e[126]=250;e[127]=249;e[128]=251;e[129]=252;e[130]=8224;e[131]=176;e[132]=162;e[133]=163;e[134]=167;e[135]=8226;e[136]=182;e[137]=223;e[138]=174;e[139]=169;e[140]=8482;e[141]=180;e[142]=168;e[143]=8800;e[144]=198;e[145]=216;e[146]=8734;e[147]=177;e[148]=8804;e[149]=8805;e[150]=165;e[151]=181;e[152]=8706;e[153]=8721;e[154]=8719;e[156]=8747;e[157]=170;e[158]=186;e[159]=8486;e[160]=230;e[161]=248;e[162]=191;e[163]=161;e[164]=172;e[165]=8730;e[166]=402;e[167]=8776;e[168]=8710;e[169]=171;e[170]=187;e[171]=8230;e[179]=8220;e[180]=8221;e[181]=8216;e[182]=8217;e[200]=193;e[203]=205;e[207]=211;e[210]=218;e[223]=711;e[224]=321;e[225]=322;e[226]=352;e[227]=353;e[228]=381;e[229]=382;e[233]=221;e[234]=253;e[252]=263;e[253]=268;e[254]=269;e[258]=258;e[260]=260;e[261]=261;e[265]=280;e[266]=281;e[267]=282;e[268]=283;e[269]=313;e[275]=323;e[276]=324;e[278]=328;e[283]=344;e[284]=345;e[285]=346;e[286]=347;e[292]=367;e[295]=377;e[296]=378;e[298]=380;e[305]=963;e[306]=964;e[307]=966;e[308]=8215;e[309]=8252;e[310]=8319;e[311]=8359;e[312]=8592;e[313]=8593;e[337]=9552;e[493]=1039;e[494]=1040;e[672]=1488;e[673]=1489;e[674]=1490;e[675]=1491;e[676]=1492;e[677]=1493;e[678]=1494;e[679]=1495;e[680]=1496;e[681]=1497;e[682]=1498;e[683]=1499;e[684]=1500;e[685]=1501;e[686]=1502;e[687]=1503;e[688]=1504;e[689]=1505;e[690]=1506;e[691]=1507;e[692]=1508;e[693]=1509;e[694]=1510;e[695]=1511;e[696]=1512;e[697]=1513;e[698]=1514;e[705]=1524;e[706]=8362;e[710]=64288;e[711]=64298;e[759]=1617;e[761]=1776;e[763]=1778;e[775]=1652;e[777]=1764;e[778]=1780;e[779]=1781;e[780]=1782;e[782]=771;e[783]=64726;e[786]=8363;e[788]=8532;e[790]=768;e[791]=769;e[792]=768;e[795]=803;e[797]=64336;e[798]=64337;e[799]=64342;e[800]=64343;e[801]=64344;e[802]=64345;e[803]=64362;e[804]=64363;e[805]=64364;e[2424]=7821;e[2425]=7822;e[2426]=7823;e[2427]=7824;e[2428]=7825;e[2429]=7826;e[2430]=7827;e[2433]=7682;e[2678]=8045;e[2679]=8046;e[2830]=1552;e[2838]=686;e[2840]=751;e[2842]=753;e[2843]=754;e[2844]=755;e[2846]=757;e[2856]=767;e[2857]=848;e[2858]=849;e[2862]=853;e[2863]=854;e[2864]=855;e[2865]=861;e[2866]=862;e[2906]=7460;e[2908]=7462;e[2909]=7463;e[2910]=7464;e[2912]=7466;e[2913]=7467;e[2914]=7468;e[2916]=7470;e[2917]=7471;e[2918]=7472;e[2920]=7474;e[2921]=7475;e[2922]=7476;e[2924]=7478;e[2925]=7479;e[2926]=7480;e[2928]=7482;e[2929]=7483;e[2930]=7484;e[2932]=7486;e[2933]=7487;e[2934]=7488;e[2936]=7490;e[2937]=7491;e[2938]=7492;e[2940]=7494;e[2941]=7495;e[2942]=7496;e[2944]=7498;e[2946]=7500;e[2948]=7502;e[2950]=7504;e[2951]=7505;e[2952]=7506;e[2954]=7508;e[2955]=7509;e[2956]=7510;e[2958]=7512;e[2959]=7513;e[2960]=7514;e[2962]=7516;e[2963]=7517;e[2964]=7518;e[2966]=7520;e[2967]=7521;e[2968]=7522;e[2970]=7524;e[2971]=7525;e[2972]=7526;e[2974]=7528;e[2975]=7529;e[2976]=7530;e[2978]=1537;e[2979]=1538;e[2980]=1539;e[2982]=1549;e[2983]=1551;e[2984]=1552;e[2986]=1554;e[2987]=1555;e[2988]=1556;e[2990]=1623;e[2991]=1624;e[2995]=1775;e[2999]=1791;e[3002]=64290;e[3003]=64291;e[3004]=64292;e[3006]=64294;e[3007]=64295;e[3008]=64296;e[3011]=1900;e[3014]=8223;e[3015]=8244;e[3017]=7532;e[3018]=7533;e[3019]=7534;e[3075]=7590;e[3076]=7591;e[3079]=7594;e[3080]=7595;e[3083]=7598;e[3084]=7599;e[3087]=7602;e[3088]=7603;e[3091]=7606;e[3092]=7607;e[3095]=7610;e[3096]=7611;e[3099]=7614;e[3100]=7615;e[3103]=7618;e[3104]=7619;e[3107]=8337;e[3108]=8338;e[3116]=1884;e[3119]=1885;e[3120]=1885;e[3123]=1886;e[3124]=1886;e[3127]=1887;e[3128]=1887;e[3131]=1888;e[3132]=1888;e[3135]=1889;e[3136]=1889;e[3139]=1890;e[3140]=1890;e[3143]=1891;e[3144]=1891;e[3147]=1892;e[3148]=1892;e[3153]=580;e[3154]=581;e[3157]=584;e[3158]=585;e[3161]=588;e[3162]=589;e[3165]=891;e[3166]=892;e[3169]=1274;e[3170]=1275;e[3173]=1278;e[3174]=1279;e[3181]=7622;e[3182]=7623;e[3282]=11799;e[3316]=578;e[3379]=42785;e[3393]=1159;e[3416]=8377})),Pi=getLookupTableFactory((function(e){e[227]=322;e[264]=261;e[291]=346})),Wi=getLookupTableFactory((function(e){e[1]=32;e[4]=65;e[5]=192;e[6]=193;e[9]=196;e[17]=66;e[18]=67;e[21]=268;e[24]=68;e[28]=69;e[29]=200;e[30]=201;e[32]=282;e[38]=70;e[39]=71;e[44]=72;e[47]=73;e[48]=204;e[49]=205;e[58]=74;e[60]=75;e[62]=76;e[68]=77;e[69]=78;e[75]=79;e[76]=210;e[80]=214;e[87]=80;e[89]=81;e[90]=82;e[92]=344;e[94]=83;e[97]=352;e[100]=84;e[104]=85;e[109]=220;e[115]=86;e[116]=87;e[121]=88;e[122]=89;e[124]=221;e[127]=90;e[129]=381;e[258]=97;e[259]=224;e[260]=225;e[263]=228;e[268]=261;e[271]=98;e[272]=99;e[273]=263;e[275]=269;e[282]=100;e[286]=101;e[287]=232;e[288]=233;e[290]=283;e[295]=281;e[296]=102;e[336]=103;e[346]=104;e[349]=105;e[350]=236;e[351]=237;e[361]=106;e[364]=107;e[367]=108;e[371]=322;e[373]=109;e[374]=110;e[381]=111;e[382]=242;e[383]=243;e[386]=246;e[393]=112;e[395]=113;e[396]=114;e[398]=345;e[400]=115;e[401]=347;e[403]=353;e[410]=116;e[437]=117;e[442]=252;e[448]=118;e[449]=119;e[454]=120;e[455]=121;e[457]=253;e[460]=122;e[462]=382;e[463]=380;e[853]=44;e[855]=58;e[856]=46;e[876]=47;e[878]=45;e[882]=45;e[894]=40;e[895]=41;e[896]=91;e[897]=93;e[923]=64;e[1004]=48;e[1005]=49;e[1006]=50;e[1007]=51;e[1008]=52;e[1009]=53;e[1010]=54;e[1011]=55;e[1012]=56;e[1013]=57;e[1081]=37;e[1085]=43;e[1086]=45}));function getStandardFontName(e){const t=normalizeFontName(e);return Yi()[t]}function isKnownFontName(e){const t=normalizeFontName(e);return!!(Yi()[t]||Ki()[t]||Ti()[t]||qi()[t])}class ToUnicodeMap{constructor(e=[]){this._map=e}get length(){return this._map.length}forEach(e){for(const t in this._map)e(t,this._map[t].codePointAt(0))}has(e){return void 0!==this._map[e]}get(e){return this._map[e]}charCodeOf(e){const t=this._map;if(t.length<=65536)return t.indexOf(e);for(const i in t)if(t[i]===e)return 0|i;return-1}amend(e){for(const t in e)this._map[t]=e[t]}}class IdentityToUnicodeMap{constructor(e,t){this.firstChar=e;this.lastChar=t}get length(){return this.lastChar+1-this.firstChar}forEach(e){for(let t=this.firstChar,i=this.lastChar;t<=i;t++)e(t,t)}has(e){return this.firstChar<=e&&e<=this.lastChar}get(e){if(this.firstChar<=e&&e<=this.lastChar)return String.fromCharCode(e)}charCodeOf(e){return Number.isInteger(e)&&e>=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const i=new CFFParser(e,t,Ri);this.cff=i.parse();this.cff.duplicateFirstGlyph();const a=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=a.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:i,cMap:a}=t,s=e.charset.charset;let r,n;if(t.composite){let t,g;if(i?.length>0){t=Object.create(null);for(let e=0,a=i.length;e=0){const a=i[t];a&&(s[e]=a)}}s.length>0&&(this.properties.builtInEncoding=s)}}function getUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function getUint16(e,t){return e[t]<<8|e[t+1]}function getInt16(e,t){return(e[t]<<24|e[t+1]<<16)>>16}function getInt8(e,t){return e[t]<<24>>24}function getFloat214(e,t){return getInt16(e,t)/16384}function getSubroutineBias(e){const t=e.length;let i=32768;t<1240?i=107:t<33900&&(i=1131);return i}function parseCmap(e,t,i){const a=1===getUint16(e,t+2)?getUint32(e,t+8):getUint32(e,t+16),s=getUint16(e,t+a);let r,n,g;if(4===s){getUint16(e,t+a+2);const i=getUint16(e,t+a+6)>>1;n=t+a+14;r=[];for(g=0;g>1;i0;)C.push({flags:r})}for(i=0;i>1;p=!0;break;case 4:n+=s.pop();moveTo(r,n);p=!0;break;case 5:for(;s.length>0;){r+=s.shift();n+=s.shift();lineTo(r,n)}break;case 6:for(;s.length>0;){r+=s.shift();lineTo(r,n);if(0===s.length)break;n+=s.shift();lineTo(r,n)}break;case 7:for(;s.length>0;){n+=s.shift();lineTo(r,n);if(0===s.length)break;r+=s.shift();lineTo(r,n)}break;case 8:for(;s.length>0;){c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}break;case 10:d=s.pop();f=null;if(i.isCFFCIDFont){const e=i.fdSelect.getFDIndex(a);if(e>=0&&eMath.abs(n-t)?r+=s.shift():n+=s.shift();bezierCurveTo(c,h,C,l,r,n);break;default:throw new FormatError(`unknown operator: 12 ${m}`)}break;case 14:if(s.length>=4){const e=s.pop(),a=s.pop();n=s.pop();r=s.pop();t.save();t.translate(r,n);let g=lookupCmap(i.cmap,String.fromCharCode(i.glyphNameMap[hi[e]]));compileCharString(i.glyphs[g.glyphId],t,i,g.glyphId);t.restore();g=lookupCmap(i.cmap,String.fromCharCode(i.glyphNameMap[hi[a]]));compileCharString(i.glyphs[g.glyphId],t,i,g.glyphId)}return;case 19:case 20:g+=s.length>>1;o+=g+7>>3;p=!0;break;case 21:n+=s.pop();r+=s.pop();moveTo(r,n);p=!0;break;case 22:r+=s.pop();moveTo(r,n);p=!0;break;case 24:for(;s.length>2;){c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}r+=s.shift();n+=s.shift();lineTo(r,n);break;case 25:for(;s.length>6;){r+=s.shift();n+=s.shift();lineTo(r,n)}c=r+s.shift();h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+s.shift();bezierCurveTo(c,h,C,l,r,n);break;case 26:s.length%2&&(r+=s.shift());for(;s.length>0;){c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C;n=l+s.shift();bezierCurveTo(c,h,C,l,r,n)}break;case 27:s.length%2&&(n+=s.shift());for(;s.length>0;){c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l;bezierCurveTo(c,h,C,l,r,n)}break;case 28:s.push((e[o]<<24|e[o+1]<<16)>>16);o+=2;break;case 29:d=s.pop()+i.gsubrsBias;f=i.gsubrs[d];f&&parse(f);break;case 30:for(;s.length>0;){c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n);if(0===s.length)break;c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();n=l+s.shift();r=C+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n)}break;case 31:for(;s.length>0;){c=r+s.shift();h=n;C=c+s.shift();l=h+s.shift();n=l+s.shift();r=C+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n);if(0===s.length)break;c=r;h=n+s.shift();C=c+s.shift();l=h+s.shift();r=C+s.shift();n=l+(1===s.length?s.shift():0);bezierCurveTo(c,h,C,l,r,n)}break;default:if(m<32)throw new FormatError(`unknown operator: ${m}`);if(m<247)s.push(m-139);else if(m<251)s.push(256*(m-247)+e[o++]+108);else if(m<255)s.push(256*-(m-251)-e[o++]-108);else{s.push((e[o]<<24|e[o+1]<<16|e[o+2]<<8|e[o+3])/65536);o+=4}}p&&(s.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const[i,a,s,r,n,g]=this.currentTransform;for(let e=0,o=t.length;e=0&&e2*getUint16(e,t)}const r=[];let n=s(t,0);for(let i=a;ie+(t.getSize()+3&-4)),0)}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),i=e>131070,a=i?4:2,s=new DataView(new ArrayBuffer((this.glyphs.length+1)*a));i?s.setUint32(0,0):s.setUint16(0,0);let r=0,n=0;for(const e of this.glyphs){r+=e.write(r,t);r=r+3&-4;n+=a;i?s.setUint32(n,r):s.setUint16(n,r>>1)}return{isLocationLong:i,loca:new Uint8Array(s.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,i=this.glyphs.length;te+t.getSize()),0);return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const i=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const i of this.composites)e+=i.write(e,t);return e-i}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const i of this.composites)i.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:i,xMax:a,yMax:s}){this.numberOfContours=e;this.xMin=t;this.yMin=i;this.xMax=a;this.yMax=s}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:i}){this.xCoordinates=t;this.yCoordinates=i;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,i){const a=[];for(let s=0;s255?e+=2:g>0&&(e+=1);t=r;g=Math.abs(n-i);g>255?e+=2:g>0&&(e+=1);i=n}}return e}write(e,t){const i=e,a=[],s=[],r=[];let n=0,g=0;for(const i of this.contours){for(let e=0,t=i.xCoordinates.length;e=0?18:2;a.push(e)}else a.push(c)}n=o;const C=i.yCoordinates[e];c=C-g;if(0===c){t|=32;s.push(0)}else{const e=Math.abs(c);if(e<=255){t|=c>=0?36:4;s.push(e)}else s.push(c)}g=C;r.push(t)}t.setUint16(e,a.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const i of r)t.setUint8(e++,i);for(let i=0,s=a.length;i=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const i=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-i}scale(e,t){}}function writeInt16(e,t,i){e[t]=i>>8&255;e[t+1]=255&i}function writeInt32(e,t,i){e[t]=i>>24&255;e[t+1]=i>>16&255;e[t+2]=i>>8&255;e[t+3]=255&i}function writeData(e,t,i){if(i instanceof Uint8Array)e.set(i,t);else if("string"==typeof i)for(let a=0,s=i.length;ai;){i<<=1;a++}const s=i*t;return{range:s,entry:a,rangeShift:t*e-s}}toArray(){let e=this.sfnt;const t=this.tables,i=Object.keys(t);i.sort();const a=i.length;let s,r,n,g,o,c=12+16*a;const C=[c];for(s=0;s>>0;C.push(c)}const h=new Uint8Array(c);for(s=0;s>>0}writeInt32(h,c+4,e);writeInt32(h,c+8,C[s]);writeInt32(h,c+12,t[o].length);c+=16}return h}addTable(e,t){if(e in this.tables)throw new Error("Table "+e+" already exists");this.tables[e]=t}}const Zi=[4],Vi=[5],zi=[6],_i=[7],$i=[8],Aa=[12,35],ea=[14],ta=[21],ia=[22],aa=[30],sa=[31];class Type1CharString{constructor(){this.width=0;this.lsb=0;this.flexing=!1;this.output=[];this.stack=[]}convert(e,t,i){const a=e.length;let s,r,n,g=!1;for(let o=0;oa)return!0;const s=a-e;for(let e=s;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);i?this.stack.splice(s,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,i){if(i>=e.length)return new Uint8Array(0);let a,s,r=0|t;for(a=0;a>8;r=52845*(t+r)+22719&65535}return g}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,i){if(t){const t=e.getBytes(),i=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(i?decrypt(t,55665,4):function decryptAscii(e,t,i){let a=0|t;const s=e.length,r=new Uint8Array(s>>>1);let n,g;for(n=0,g=0;n>8;a=52845*(e+a)+22719&65535}}return r.slice(i,g)}(t,55665,4))}this.seacAnalysisEnabled=!!i;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let i="";do{i+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return i}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,i=[],a=[],s=Object.create(null);s.lenIV=4;const r={subrs:[],charstrings:[],properties:{privateData:s}};let n,g,o,c;for(;null!==(n=this.getToken());)if("/"===n){n=this.getToken();switch(n){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){n=this.getToken();if(null===n||"end"===n)break;if("/"!==n)continue;const e=this.getToken();g=this.readInt();this.getToken();o=g>0?t.getBytes(g):new Uint8Array(0);c=r.properties.privateData.lenIV;const i=this.readCharStrings(o,c);this.nextChar();n=this.getToken();"noaccess"===n?this.getToken():"/"===n&&this.prevChar();a.push({glyph:e,encoded:i})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();g=this.readInt();this.getToken();o=g>0?t.getBytes(g):new Uint8Array(0);c=r.properties.privateData.lenIV;const a=this.readCharStrings(o,c);this.nextChar();n=this.getToken();"noaccess"===n&&this.getToken();i[e]=a}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":r.properties.privateData[n]=this.readNumberArray();break;case"StdHW":case"StdVW":r.properties.privateData[n]=this.readNumberArray()[0];break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":r.properties.privateData[n]=this.readNumber();break;case"ExpansionFactor":r.properties.privateData[n]=this.readNumber()||.06;break;case"ForceBold":r.properties.privateData[n]=this.readBoolean()}}for(const{encoded:t,glyph:s}of a){const a=new Type1CharString,n=a.convert(t,i,this.seacAnalysisEnabled);let g=a.output;n&&(g=[14]);const o={glyphName:s,charstring:g,width:a.width,lsb:a.lsb,seac:a.seac};".notdef"===s?r.charstrings.unshift(o):r.charstrings.push(o);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(s);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=a.width)}}return r}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const i=this.readNumberArray();e.fontMatrix=i;break;case"Encoding":const a=this.getToken();let s;if(/^\d+$/.test(a)){s=[];const e=0|parseInt(a,10);this.getToken();for(let i=0;i=s){n+=i;for(;n=0&&(a[e]=s)}}return type1FontGlyphMapping(e,a,i)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let i=0,a=e.length;i0;e--)t[e]-=t[e-1];Q.setByName(e,t)}r.topDict.privateDict=Q;const u=new CFFIndex;for(C=0,h=a.length;C0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,i,a,s,r,n,g,o){this.originalCharCode=e;this.fontChar=t;this.unicode=i;this.accent=a;this.width=s;this.vmetric=r;this.operatorListId=n;this.isSpace=g;this.isInFont=o}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=ki.get(e);if(t)return t;const i=e.match(Si),a={isWhitespace:!!i?.[1],isZeroWidthDiacritic:!!i?.[2],isInvisibleFormatMark:!!i?.[3]};ki.set(e,a);return a}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,i){e[t+1]=i;e[t]=i>>>8}function signedInt16(e,t){const i=(e<<8)+t;return 32768&i?i-65536:i}function string16(e){return String.fromCharCode(e>>8&255,255&e)}function safeString16(e){e>32767?e=32767:e<-32768&&(e=-32768);return String.fromCharCode(e>>8&255,255&e)}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:i,composite:a}){let s,r;if(function isTrueTypeFile(e){const t=e.peekBytes(4);return 65536===readUint32(t,0)||"true"===bytesToString(t)}(e)||isTrueTypeCollectionFile(e))s=a?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))s=a?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))s=a?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(a){s="CIDFontType0";r="CIDFontType0C"}else{s="MMType1"===t?"MMType1":"Type1";r="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");s=t;r=i}return[s,r]}function applyStandardFontGlyphMap(e,t){for(const i in t)e[+i]=t[i]}function buildToFontChar(e,t,i){const a=[];let s;for(let i=0,r=e.length;iC){o++;if(o>=ra.length){warn("Ran out of space in font private use area.");break}c=ra[o][0];C=ra[o][1]}const E=c++;0===Q&&(Q=i);let u=a.get(l);"string"==typeof u&&(u=u.codePointAt(0));if(u&&!(h=u,ra[0][0]<=h&&h<=ra[0][1]||ra[1][0]<=h&&h<=ra[1][1])&&!g.has(Q)){r.set(u,Q);g.add(Q)}s[E]=Q;n[l]=E}var h;return{toFontChar:n,charCodeToGlyphId:s,toUnicodeExtraMap:r,nextAvailableFontCharCode:c}}function createCmapTable(e,t,i){const a=function getRanges(e,t,i){const a=[];for(const t in e)e[t]>=i||a.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,s]of t)s>=i||a.push({fontCharCode:e,glyphId:s});0===a.length&&a.push({fontCharCode:0,glyphId:0});a.sort((function fontGetRangesSort(e,t){return e.fontCharCode-t.fontCharCode}));const s=[],r=a.length;for(let e=0;e65535?2:1;let r,n,g,o,c="\0\0"+string16(s)+"\0\0"+string32(4+8*s);for(r=a.length-1;r>=0&&!(a[r][0]<=65535);--r);const C=r+1;a[r][0]<65535&&65535===a[r][1]&&(a[r][1]=65534);const h=a[r][1]<65535?1:0,l=C+h,Q=OpenTypeFileBuilder.getSearchParams(l,2);let E,u,d,f,p="",m="",y="",w="",D="",b=0;for(r=0,n=C;r0){m+="ÿÿ";p+="ÿÿ";y+="\0";w+="\0\0"}const F="\0\0"+string16(2*l)+string16(Q.range)+string16(Q.entry)+string16(Q.rangeShift)+m+"\0\0"+p+y+w+D;let S="",k="";if(s>1){c+="\0\0\n"+string32(4+8*s+4+F.length);S="";for(r=0,n=a.length;re||!g)&&(g=e);o 123 are reserved for internal usage");n|=1<65535&&(o=65535)}else{g=0;o=255}const C=e.bbox||[0,0,0,0],h=i.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),l=e.ascentScaled?1:h/na,Q=i.ascent||Math.round(l*(e.ascent||C[3]));let E=i.descent||Math.round(l*(e.descent||C[1]));E>0&&e.descent>0&&C[1]<0&&(E=-E);const u=i.yMax||Q,d=-i.yMin||-E;return"\0$ô\0\0\0Š»\0\0\0ŒŠ»\0\0ß\x001\0\0\0\0"+String.fromCharCode(e.fixedPitch?9:0)+"\0\0\0\0\0\0"+string32(a)+string32(s)+string32(r)+string32(n)+"*21*"+string16(e.italicAngle?1:0)+string16(g||e.firstChar)+string16(o||e.lastChar)+string16(Q)+string16(E)+"\0d"+string16(u)+string16(d)+"\0\0\0\0\0\0\0\0"+string16(e.xHeight)+string16(e.capHeight)+string16(0)+string16(g||e.firstChar)+"\0"}function createPostTable(e){return"\0\0\0"+string32(Math.floor(65536*e.italicAngle))+"\0\0\0\0"+string32(e.fixedPitch?1:0)+"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||(t=[[],[]]);const i=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],a=[];let s,r,n,g,o;for(s=0,r=i.length;s0;if((n||g)&&"CIDFontType2"===i&&this.cidEncoding.startsWith("Identity-")){const i=e.cidToGidMap,a=[];applyStandardFontGlyphMap(a,Oi());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(a,Pi()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(a,Wi());if(i){for(const e in a){const t=a[e];void 0!==i[t]&&(a[+e]=i[t])}i.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach((function(e,t){const s=a[e];void 0===i[s]&&(a[+e]=t)}))}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(e,t){a[+e]=t}));this.toFontChar=a;this.toUnicode=new ToUnicodeMap(a)}else if(/Symbol/i.test(a))this.toFontChar=buildToFontChar(Bi,wi(),this.differences);else if(/Dingbats/i.test(a))this.toFontChar=buildToFontChar(Qi,Di(),this.differences);else if(n||g){const e=buildToFontChar(this.defaultEncoding,wi(),this.differences);"CIDFontType2"!==i||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(t,i){e[+t]=i}));this.toFontChar=e}else{const e=wi(),i=[];this.toUnicode.forEach(((t,a)=>{if(!this.composite){const i=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==i&&(a=i)}i[+t]=a}));this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(i,Oi());this.toFontChar=i}amendFallbackToUnicode(e);this.loadedName=a.split("-",1)[0]}checkAndRepair(e,t,i){const a=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const i=Object.create(null);i["OS/2"]=null;i.cmap=null;i.head=null;i.hhea=null;i.hmtx=null;i.maxp=null;i.name=null;i.post=null;for(let s=0;s>>0,a=e.getInt32()>>>0,s=e.getInt32()>>>0,r=e.pos;e.pos=e.start||0;e.skip(a);const n=e.getBytes(s);e.pos=r;if("head"===t){n[8]=n[9]=n[10]=n[11]=0;n[17]|=32}return{tag:t,checksum:i,length:s,offset:a,data:n}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,i,a,s,r){const n={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||i>e.length||i-t<=12)return n;const g=e.subarray(t,i),o=signedInt16(g[2],g[3]),c=signedInt16(g[4],g[5]),C=signedInt16(g[6],g[7]),h=signedInt16(g[8],g[9]);if(o>C){writeSignedInt16(g,2,C);writeSignedInt16(g,6,o)}if(c>h){writeSignedInt16(g,4,h);writeSignedInt16(g,8,c)}const l=signedInt16(g[0],g[1]);if(l<0){if(l<-1)return n;a.set(g,s);n.length=g.length;return n}let Q,E=10,u=0;for(Q=0;Qg.length)return n;if(!r&&f>0){a.set(g.subarray(0,d),s);a.set([0,0],s+d);a.set(g.subarray(p,y),s+d+2);y-=f;g.length-y>3&&(y=y+3&-4);n.length=y;return n}if(g.length-y>3){y=y+3&-4;a.set(g.subarray(0,y),s);n.length=y;return n}a.set(g,s);n.length=g.length;return n}function readNameTable(e){const i=(t.start||0)+e.offset;t.pos=i;const a=[[],[]],s=[],r=e.length,n=i+r;if(0!==t.getUint16()||r<6)return[a,s];const g=t.getUint16(),o=t.getUint16();let c,C;for(c=0;cn)continue;t.pos=r;const g=e.name;if(e.encoding){let i="";for(let a=0,s=e.length;a0&&(c+=e-1)}}else{if(d||p){warn("TT: nested FDEFs not allowed");u=!0}d=!0;h=c;n=l.pop();t.functionsDefined[n]={data:o,i:c}}else if(!d&&!p){n=l.at(-1);if(isNaN(n))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[n]=!0;if(n in t.functionsStackDeltas){const e=l.length+t.functionsStackDeltas[n];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}l.length=e}else if(n in t.functionsDefined&&!E.includes(n)){Q.push({data:o,i:c,stackTop:l.length-1});E.push(n);g=t.functionsDefined[n];if(!g){warn("TT: CALL non-existent function");t.hintsValid=!1;return}o=g.data;c=g.i}}}if(!d&&!p){let t=0;e<=142?t=s[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){a=l.pop();isNaN(a)||(t=2*-a)}for(;t<0&&l.length>0;){l.pop();t++}for(;t>0;){l.push(NaN);t--}}}t.tooComplexToFollowFunctions=u;const m=[o];c>o.length&&m.push(new Uint8Array(c-o.length));if(h>C){warn("TT: complementing a missing function tail");m.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let i,a,s=0;for(i=0,a=t.length;i>>0,r=[];for(let t=0;t>>0);const n={ttcTag:t,majorVersion:i,minorVersion:a,numFonts:s,offsetTable:r};switch(i){case 1:return n;case 2:n.dsigTag=e.getInt32()>>>0;n.dsigLength=e.getInt32()>>>0;n.dsigOffset=e.getInt32()>>>0;return n}throw new FormatError(`Invalid TrueType Collection majorVersion: ${i}.`)}(e),s=t.split("+");let r;for(let n=0;n0||!(i.cMap instanceof IdentityCMap));if("OTTO"===r.version&&!t||!n.head||!n.hhea||!n.maxp||!n.post){o=new Stream(n["CFF "].data);g=new CFFFont(o,i);adjustWidths(i);return this.convert(e,g,i)}delete n.glyf;delete n.loca;delete n.fpgm;delete n.prep;delete n["cvt "];this.isOpenType=!0}if(!n.maxp)throw new FormatError('Required "maxp" table is not found');t.pos=(t.start||0)+n.maxp.offset;let C=t.getInt32();const h=t.getUint16();if(65536!==C&&20480!==C){if(6===n.maxp.length)C=20480;else{if(!(n.maxp.length>=32))throw new FormatError('"maxp" table has a wrong version number');C=65536}!function writeUint32(e,t,i){e[t+3]=255&i;e[t+2]=i>>>8;e[t+1]=i>>>16;e[t]=i>>>24}(n.maxp.data,0,C)}if(i.scaleFactors?.length===h&&c){const{scaleFactors:e}=i,t=int16(n.head.data[50],n.head.data[51]),a=new GlyfTable({glyfTable:n.glyf.data,isGlyphLocationsLong:t,locaTable:n.loca.data,numGlyphs:h});a.scale(e);const{glyf:s,loca:r,isLocationLong:g}=a.write();n.glyf.data=s;n.loca.data=r;if(g!==!!t){n.head.data[50]=0;n.head.data[51]=g?1:0}const o=n.hmtx.data;for(let t=0;t>8&255;o[i+1]=255&a;writeSignedInt16(o,i+2,Math.round(e[t]*signedInt16(o[i+2],o[i+3])))}}let l=h+1,Q=!0;if(l>65535){Q=!1;l=h;warn("Not enough space in glyfs to duplicate first glyph.")}let E=0,u=0;if(C>=65536&&n.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){n.maxp.data[14]=0;n.maxp.data[15]=2}t.pos+=4;E=t.getUint16();t.pos+=4;u=t.getUint16()}n.maxp.data[4]=l>>8;n.maxp.data[5]=255&l;const d=function sanitizeTTPrograms(e,t,i,a){const s={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,s);t&&sanitizeTTProgram(t,s);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let i=0,a=e.functionsUsed.length;it){warn("TT: invalid function id: "+i);e.hintsValid=!1;return}if(e.functionsUsed[i]&&!e.functionsDefined[i]){warn("TT: undefined function: "+i);e.hintsValid=!1;return}}}(s,a);if(i&&1&i.length){const e=new Uint8Array(i.length+1);e.set(i.data);i.data=e}return s.hintsValid}(n.fpgm,n.prep,n["cvt "],E);if(!d){delete n.fpgm;delete n.prep;delete n["cvt "]}!function sanitizeMetrics(e,t,i,a,s,r){if(!t){i&&(i.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const n=e.getUint16();e.pos+=8;e.pos+=2;let g=e.getUint16();if(0!==n){if(!(2&int16(a.data[44],a.data[45]))){t.data[22]=0;t.data[23]=0}}if(g>s){info(`The numOfMetrics (${g}) should not be greater than the numGlyphs (${s}).`);g=s;t.data[34]=(65280&g)>>8;t.data[35]=255&g}const o=s-g-(i.length-4*g>>1);if(o>0){const e=new Uint8Array(i.length+2*o);e.set(i.data);if(r){e[i.length]=i.data[2];e[i.length+1]=i.data[3]}i.data=e}}(t,n.hhea,n.hmtx,n.head,l,Q);if(!n.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,i){const a=e.data,s=function int32(e,t,i,a){return(e<<24)+(t<<16)+(i<<8)+a}(a[0],a[1],a[2],a[3]);if(s>>16!=1){info("Attempting to fix invalid version in head table: "+s);a[0]=0;a[1]=1;a[2]=0;a[3]=0}const r=int16(a[50],a[51]);if(r<0||r>1){info("Attempting to fix invalid indexToLocFormat in head table: "+r);const e=t+1;if(i===e<<1){a[50]=0;a[51]=0}else{if(i!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+r);a[50]=0;a[51]=1}}}(n.head,h,c?n.loca.length:0);let f=Object.create(null);if(c){const e=int16(n.head.data[50],n.head.data[51]),t=function sanitizeGlyphLocations(e,t,i,a,s,r,n){let g,o,c;if(a){g=4;o=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};c=function fontItemEncodeLong(e,t,i){e[t]=i>>>24&255;e[t+1]=i>>16&255;e[t+2]=i>>8&255;e[t+3]=255&i}}else{g=2;o=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};c=function fontItemEncode(e,t,i){e[t]=i>>9&255;e[t+1]=i>>1&255}}const C=r?i+1:i,h=g*(1+C),l=new Uint8Array(h);l.set(e.data.subarray(0,h));e.data=l;const Q=t.data,E=Q.length,u=new Uint8Array(E);let d,f;const p=[];for(d=0,f=0;dE&&(e=E);p.push({index:d,offset:e,endOffset:0})}p.sort(((e,t)=>e.offset-t.offset));for(d=0;de.index-t.index));for(d=0;dn&&(n=e.sizeOfInstructions);w+=t;c(l,f,w)}if(0===w){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(d=0,f=g;di+w)t.data=u.subarray(0,i+w);else{t.data=new Uint8Array(i+w);t.data.set(u.subarray(0,w))}t.data.set(u.subarray(0,i),w);c(e.data,l.length-g,w+i)}else t.data=u.subarray(0,w);return{missingGlyphs:y,maxSizeOfInstructions:n}}(n.loca,n.glyf,h,e,d,Q,u);f=t.missingGlyphs;if(C>=65536&&n.maxp.length>=32){n.maxp.data[26]=t.maxSizeOfInstructions>>8;n.maxp.data[27]=255&t.maxSizeOfInstructions}}if(!n.hhea)throw new FormatError('Required "hhea" table is not found');if(0===n.hhea.data[10]&&0===n.hhea.data[11]){n.hhea.data[10]=255;n.hhea.data[11]=255}const p={unitsPerEm:int16(n.head.data[18],n.head.data[19]),yMax:signedInt16(n.head.data[42],n.head.data[43]),yMin:signedInt16(n.head.data[38],n.head.data[39]),ascent:signedInt16(n.hhea.data[4],n.hhea.data[5]),descent:signedInt16(n.hhea.data[6],n.hhea.data[7]),lineGap:signedInt16(n.hhea.data[8],n.hhea.data[9])};this.ascent=p.ascent/p.unitsPerEm;this.descent=p.descent/p.unitsPerEm;this.lineGap=p.lineGap/p.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;n.post&&function readPostScriptTable(e,i,a){const s=(t.start||0)+e.offset;t.pos=s;const r=s+e.length,n=t.getInt32();t.skip(28);let g,o,c=!0;switch(n){case 65536:g=Hi;break;case 131072:const e=t.getUint16();if(e!==a){c=!1;break}const s=[];for(o=0;o=32768){c=!1;break}s.push(e)}if(!c)break;const C=[],h=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let s=-1;t?s=a:void 0!==e[a]&&(s=e[a]);s>=0&&s>>0;let C=!1;if(g?.platformId!==s||g?.encodingId!==r){if(0!==s||0!==r&&1!==r&&3!==r)if(1===s&&0===r)C=!0;else if(3!==s||1!==r||!a&&g){if(i&&3===s&&0===r){C=!0;let i=!0;if(e>3;e.push(a);i=Math.max(a,i)}const a=[];for(let e=0;e<=i;e++)a.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let i=0;i<256;i++)if(0===e[i]){t.pos=a[0].idRangePos+2*i;Q=t.getUint16();h.push({charCode:i,glyphId:Q})}else{const s=a[e[i]];for(l=0;l>1;t.skip(6);const i=[];let a;for(a=0;a>1)-(e-a);s.offsetIndex=n;g=Math.max(g,n+s.end-s.start+1)}else s.offsetIndex=-1}const o=[];for(l=0;l>>0;for(l=0;l>>0,i=t.getInt32()>>>0;let a=t.getInt32()>>>0;for(let t=e;t<=i;t++)h.push({charCode:t,glyphId:a++})}}}h.sort((function(e,t){return e.charCode-t.charCode}));for(let e=1;e=61440&&t<=61695&&(t&=255);m[t]=e.glyphId}else for(const e of r)m[e.charCode]=e.glyphId;if(i.glyphNames&&(g.length||this.differences.length))for(let e=0;e<256;++e){if(!o&&void 0!==m[e])continue;const t=this.differences[e]||g[e];if(!t)continue;const a=i.glyphNames.indexOf(t);a>0&&hasGlyph(a)&&(m[e]=a)}}0===m.length&&(m[0]=0);let y=l-1;Q||(y=0);if(!i.cssFontInfo){const e=adjustMapping(m,hasGlyph,y,this.toUnicode);this.toFontChar=e.toFontChar;n.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,l)};n["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const i=t.getUint16();t.skip(60);const a=t.getUint16();if(i<4&&768&a)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(n["OS/2"],t)||(n["OS/2"]={tag:"OS/2",data:createOS2Table(i,e.charCodeToGlyphId,p)})}if(!c)try{o=new Stream(n["CFF "].data);g=new CFFParser(o,i,Ri).parse();g.duplicateFirstGlyph();const e=new CFFCompiler(g);n["CFF "].data=e.compile()}catch{warn("Failed to compile font "+i.loadedName)}if(n.name){const[t,a]=readNameTable(n.name);n.name.data=createNameTable(e,t);this.psName=t[0][6]||null;i.composite||function adjustTrueTypeToUnicode(e,t,i){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===i.length)return;if(e.defaultEncoding===li)return;for(const e of i)if(!isWinNameRecord(e))return;const a=li,s=[],r=wi();for(const e in a){const t=a[e];if(""===t)continue;const i=r[t];void 0!==i&&(s[e]=String.fromCharCode(i))}s.length>0&&e.toUnicode.amend(s)}(i,this.isSymbolicFont,a)}else n.name={tag:"name",data:createNameTable(this.name)};const w=new OpenTypeFileBuilder(r.version);for(const e in n)w.addTable(e,n[e].data);return w.toArray()}convert(e,t,i){i.fixedPitch=!1;i.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const i=[],a=wi();for(const s in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[s]))continue;const r=getUnicodeForGlyph(t[s],a);-1!==r&&(i[s]=String.fromCharCode(r))}i.length>0&&e.toUnicode.amend(i)}(i,i.builtInEncoding);let s=1;t instanceof CFFFont&&(s=t.numGlyphs-1);const r=t.getGlyphMapping(i);let n=null,g=r,o=null;if(!i.cssFontInfo){n=adjustMapping(r,t.hasGlyphId.bind(t),s,this.toUnicode);this.toFontChar=n.toFontChar;g=n.charCodeToGlyphId;o=n.toUnicodeExtraMap}const c=t.numGlyphs;function getCharCodes(e,t){let i=null;for(const a in e)t===e[a]&&(i||=[]).push(0|a);return i}function createCharCode(e,t){for(const i in e)if(t===e[i])return 0|i;n.charCodeToGlyphId[n.nextAvailableFontCharCode]=t;return n.nextAvailableFontCharCode++}const C=t.seacs;if(n&&C?.length){const e=i.fontMatrix||a,s=t.getCharset(),g=Object.create(null);for(let t in C){t|=0;const i=C[t],a=hi[i[2]],o=hi[i[3]],c=s.indexOf(a),h=s.indexOf(o);if(c<0||h<0)continue;const l={x:i[0]*e[0]+i[1]*e[2]+e[4],y:i[0]*e[1]+i[1]*e[3]+e[5]},Q=getCharCodes(r,t);if(Q)for(const e of Q){const t=n.charCodeToGlyphId,i=createCharCode(t,c),a=createCharCode(t,h);g[e]={baseFontCharCode:i,accentFontCharCode:a,accentOffset:l}}}i.seacMap=g}const h=i.fontMatrix?1/Math.max(...i.fontMatrix.slice(0,4).map(Math.abs)):1e3,l=new OpenTypeFileBuilder("OTTO");l.addTable("CFF ",t.data);l.addTable("OS/2",createOS2Table(i,g));l.addTable("cmap",createCmapTable(g,o,c));l.addTable("head","\0\0\0\0\0\0\0\0\0\0_<õ\0\0"+safeString16(h)+"\0\0\0\0ž\v~'\0\0\0\0ž\v~'\0\0"+safeString16(i.descent)+"ÿ"+safeString16(i.ascent)+string16(i.italicAngle?2:0)+"\0\0\0\0\0\0\0");l.addTable("hhea","\0\0\0"+safeString16(i.ascent)+safeString16(i.descent)+"\0\0ÿÿ\0\0\0\0\0\0"+safeString16(i.capHeight)+safeString16(Math.tan(i.italicAngle)*i.xHeight)+"\0\0\0\0\0\0\0\0\0\0\0\0"+string16(c));l.addTable("hmtx",function fontFieldsHmtx(){const e=t.charstrings,i=t.cff?t.cff.widths:null;let a="\0\0\0\0";for(let t=1,s=c;t=65520&&e<=65535?0:e>=62976&&e<=63743?bi()[e]||e:173===e?45:e}(i)}this.isType3Font&&(s=i);let C=null;if(this.seacMap?.[e]){c=!0;const t=this.seacMap[e];i=t.baseFontCharCode;C={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let h="";"number"==typeof i&&(i<=1114111?h=String.fromCodePoint(i):warn(`charToGlyph - invalid fontCharCode: ${i}`));if(this.missingFile&&this.vertical&&1===h.length){const e=Ji()[h.charCodeAt(0)];e&&(h=o=String.fromCharCode(e))}r=new fonts_Glyph(e,h,o,C,a,g,s,t,c);return this._glyphCache[e]=r}charsToGlyphs(e){let t=this._charsCache[e];if(t)return t;t=[];if(this.cMap){const i=Object.create(null),a=e.length;let s=0;for(;st.length%2==1,a=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let s=0,r=e.length;s55295&&(r<57344||r>65533)&&s++;if(this.toUnicode){const e=a(r);if(-1!==e){if(hasCurrentBufErrors()){t.push(i.join(""));i.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)i.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(i.join(""));i.length=0}i.push(String.fromCodePoint(r))}t.push(i.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(e=!1){return{error:this.error}}}const Ia=2,ca=3,Ca=4,ha=5,la=6,Ba=7;class Pattern{constructor(){unreachable("Cannot initialize Pattern.")}static parseShading(e,t,i,a,s){const r=e instanceof BaseStream?e.dict:e,n=r.get("ShadingType");try{switch(n){case Ia:case ca:return new RadialAxialShading(r,t,i,a,s);case Ca:case ha:case la:case Ba:return new MeshShading(e,t,i,a,s);default:throw new FormatError("Unsupported ShadingType: "+n)}}catch(e){if(e instanceof MissingDataException)throw e;warn(e);return new DummyShading}}}class BaseShading{static SMALL_NUMBER=1e-6;getIR(){unreachable("Abstract method `getIR` called.")}}class RadialAxialShading extends BaseShading{constructor(e,t,i,a,s){super();this.shadingType=e.get("ShadingType");let r=0;this.shadingType===Ia?r=4:this.shadingType===ca&&(r=6);this.coordsArr=e.getArray("Coords");if(!isNumberArray(this.coordsArr,r))throw new FormatError("RadialAxialShading: Invalid /Coords array.");const n=ColorSpace.parse({cs:e.getRaw("CS")||e.getRaw("ColorSpace"),xref:t,resources:i,pdfFunctionFactory:a,localColorSpaceCache:s});this.bbox=lookupNormalRect(e.getArray("BBox"),null);let g=0,o=1;const c=e.getArray("Domain");isNumberArray(c,2)&&([g,o]=c);let C=!1,h=!1;const l=e.getArray("Extend");(function isBooleanArray(e,t){return Array.isArray(e)&&(null===t||e.length===t)&&e.every((e=>"boolean"==typeof e))})(l,2)&&([C,h]=l);if(!(this.shadingType!==ca||C&&h)){const[e,t,i,a,s,r]=this.coordsArr,n=Math.hypot(e-a,t-s);i<=r+n&&r<=i+n&&warn("Unsupported radial gradient.")}this.extendStart=C;this.extendEnd=h;const Q=e.getRaw("Function"),E=a.createFromArray(Q),u=(o-g)/840,d=this.colorStops=[];if(g>=o||u<=0){info("Bad shading domain.");return}const f=new Float32Array(n.numComps),p=new Float32Array(1);let m,y=0;p[0]=g;E(p,0,f,0);let w=n.getRgb(f,0);const D=Util.makeHexColor(w[0],w[1],w[2]);d.push([0,D]);let b=1;p[0]=g+u;E(p,0,f,0);let F=n.getRgb(f,0),S=F[0]-w[0]+1,k=F[1]-w[1]+1,R=F[2]-w[2]+1,N=F[0]-w[0]-1,G=F[1]-w[1]-1,M=F[2]-w[2]-1;for(let e=2;e<840;e++){p[0]=g+e*u;E(p,0,f,0);m=n.getRgb(f,0);const t=e-y;S=Math.min(S,(m[0]-w[0]+1)/t);k=Math.min(k,(m[1]-w[1]+1)/t);R=Math.min(R,(m[2]-w[2]+1)/t);N=Math.max(N,(m[0]-w[0]-1)/t);G=Math.max(G,(m[1]-w[1]-1)/t);M=Math.max(M,(m[2]-w[2]-1)/t);if(!(N<=S&&G<=k&&M<=R)){const e=Util.makeHexColor(F[0],F[1],F[2]);d.push([b/840,e]);S=m[0]-F[0]+1;k=m[1]-F[1]+1;R=m[2]-F[2]+1;N=m[0]-F[0]-1;G=m[1]-F[1]-1;M=m[2]-F[2]-1;y=b;w=F}b=e;F=m}const U=Util.makeHexColor(F[0],F[1],F[2]);d.push([1,U]);let x="transparent";if(e.has("Background")){m=n.getRgb(e.get("Background"),0);x=Util.makeHexColor(m[0],m[1],m[2])}if(!C){d.unshift([0,x]);d[1][0]+=BaseShading.SMALL_NUMBER}if(!h){d.at(-1)[0]-=BaseShading.SMALL_NUMBER;d.push([1,x])}this.colorStops=d}getIR(){const{coordsArr:e,shadingType:t}=this;let i,a,s,r,n;if(t===Ia){a=[e[0],e[1]];s=[e[2],e[3]];r=null;n=null;i="axial"}else if(t===ca){a=[e[0],e[1]];s=[e[3],e[4]];r=e[2];n=e[5];i="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",i,this.bbox,this.colorStops,a,s,r,n]}}class MeshStreamReader{constructor(e,t){this.stream=e;this.context=t;this.buffer=0;this.bufferLength=0;const i=t.numComps;this.tmpCompsBuf=new Float32Array(i);const a=t.colorSpace.numComps;this.tmpCsCompsBuf=t.colorFn?new Float32Array(a):this.tmpCompsBuf}get hasData(){if(this.stream.end)return this.stream.pos0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){let t=this.buffer,i=this.bufferLength;if(32===e){if(0===i)return(this.stream.getByte()<<24|this.stream.getByte()<<16|this.stream.getByte()<<8|this.stream.getByte())>>>0;t=t<<24|this.stream.getByte()<<16|this.stream.getByte()<<8|this.stream.getByte();const e=this.stream.getByte();this.buffer=e&(1<>i)>>>0}if(8===e&&0===i)return this.stream.getByte();for(;i>i}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const e=this.context.bitsPerCoordinate,t=this.readBits(e),i=this.readBits(e),a=this.context.decode,s=e<32?1/((1<r?r:e;t=t>n?n:t;i=ie*s[t])):i;let n,g=-2;const o=[];for(const[e,t]of a.map(((e,t)=>[e,t])).sort((([e],[t])=>e-t)))if(-1!==e)if(e===g+1){n.push(r[t]);g+=1}else{g=e;n=[r[t]];o.push(e,n)}return o}(e),i=new Dict(null);i.set("BaseFont",Name.get(e));i.set("Type",Name.get("Font"));i.set("Subtype",Name.get("CIDFontType2"));i.set("Encoding",Name.get("Identity-H"));i.set("CIDToGIDMap",Name.get("Identity"));i.set("W",t);i.set("FirstChar",t[0]);i.set("LastChar",t.at(-2)+t.at(-1).length-1);const a=new Dict(null);i.set("FontDescriptor",a);const s=new Dict(null);s.set("Ordering","Identity");s.set("Registry","Adobe");s.set("Supplement",0);i.set("CIDSystemInfo",s);return i}class PostScriptParser{constructor(e){this.lexer=e;this.operators=[];this.token=null;this.prev=null}nextToken(){this.prev=this.token;this.token=this.lexer.getToken()}accept(e){if(this.token.type===e){this.nextToken();return!0}return!1}expect(e){if(this.accept(e))return!0;throw new FormatError(`Unexpected symbol: found ${this.token.type} expected ${e}.`)}parse(){this.nextToken();this.expect(as.LBRACE);this.parseBlock();this.expect(as.RBRACE);return this.operators}parseBlock(){for(;;)if(this.accept(as.NUMBER))this.operators.push(this.prev.value);else if(this.accept(as.OPERATOR))this.operators.push(this.prev.value);else{if(!this.accept(as.LBRACE))return;this.parseCondition()}}parseCondition(){const e=this.operators.length;this.operators.push(null,null);this.parseBlock();this.expect(as.RBRACE);if(this.accept(as.IF)){this.operators[e]=this.operators.length;this.operators[e+1]="jz"}else{if(!this.accept(as.LBRACE))throw new FormatError("PS Function: error parsing conditional.");{const t=this.operators.length;this.operators.push(null,null);const i=this.operators.length;this.parseBlock();this.expect(as.RBRACE);this.expect(as.IFELSE);this.operators[t]=this.operators.length;this.operators[t+1]="j";this.operators[e]=i;this.operators[e+1]="jz"}}}}const as={LBRACE:0,RBRACE:1,NUMBER:2,OPERATOR:3,IF:4,IFELSE:5};class PostScriptToken{static get opCache(){return shadow(this,"opCache",Object.create(null))}constructor(e,t){this.type=e;this.value=t}static getOperator(e){return PostScriptToken.opCache[e]||=new PostScriptToken(as.OPERATOR,e)}static get LBRACE(){return shadow(this,"LBRACE",new PostScriptToken(as.LBRACE,"{"))}static get RBRACE(){return shadow(this,"RBRACE",new PostScriptToken(as.RBRACE,"}"))}static get IF(){return shadow(this,"IF",new PostScriptToken(as.IF,"IF"))}static get IFELSE(){return shadow(this,"IFELSE",new PostScriptToken(as.IFELSE,"IFELSE"))}}class PostScriptLexer{constructor(e){this.stream=e;this.nextChar();this.strBuf=[]}nextChar(){return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(t<0)return Bt;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return new PostScriptToken(as.NUMBER,this.getNumber());case 123:this.nextChar();return PostScriptToken.LBRACE;case 125:this.nextChar();return PostScriptToken.RBRACE}const i=this.strBuf;i.length=0;i[0]=String.fromCharCode(t);for(;(t=this.nextChar())>=0&&(t>=65&&t<=90||t>=97&&t<=122);)i.push(String.fromCharCode(t));const a=i.join("");switch(a.toLowerCase()){case"if":return PostScriptToken.IF;case"ifelse":return PostScriptToken.IFELSE;default:return PostScriptToken.getOperator(a)}}getNumber(){let e=this.currentChar;const t=this.strBuf;t.length=0;t[0]=String.fromCharCode(e);for(;(e=this.nextChar())>=0&&(e>=48&&e<=57||45===e||46===e);)t.push(String.fromCharCode(e));const i=parseFloat(t.join(""));if(isNaN(i))throw new FormatError(`Invalid floating point number: ${i}`);return i}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,i){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,i){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,i){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,i){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,i)}else this._imageMap.has(e)||this._imageMap.set(e,i)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,i){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,i)}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#F=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#S(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#k(){return!(this._imageCache.size+e)):null}class PDFFunction{static getSampleArray(e,t,i,a){let s,r,n=1;for(s=0,r=e.length;s>o)*C;c&=(1<i?e=i:e0&&(l=r[h-1]);let Q=a[1];h>1,c=s.length>>1,C=new PostScriptEvaluator(g),h=Object.create(null);let l=8192;const Q=new Float32Array(c);return function constructPostScriptFn(e,t,i,a){let s,n,g="";const E=Q;for(s=0;se&&(n=e)}d[s]=n}if(l>0){l--;h[g]=d}i.set(d,a)}}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}class PostScriptStack{static MAX_STACK_SIZE=100;constructor(e){this.stack=e?Array.from(e):[]}push(e){if(this.stack.length>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");this.stack.push(e)}pop(){if(this.stack.length<=0)throw new Error("PostScript function stack underflow.");return this.stack.pop()}copy(e){if(this.stack.length+e>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");const t=this.stack;for(let i=t.length-e,a=e-1;a>=0;a--,i++)t.push(t[i])}index(e){this.push(this.stack[this.stack.length-e-1])}roll(e,t){const i=this.stack,a=i.length-e,s=i.length-1,r=a+(t-Math.floor(t/e)*e);for(let e=a,t=s;e0?t.push(n<>g);break;case"ceiling":n=t.pop();t.push(Math.ceil(n));break;case"copy":n=t.pop();t.copy(n);break;case"cos":n=t.pop();t.push(Math.cos(n%360/180*Math.PI));break;case"cvi":n=0|t.pop();t.push(n);break;case"cvr":break;case"div":g=t.pop();n=t.pop();t.push(n/g);break;case"dup":t.copy(1);break;case"eq":g=t.pop();n=t.pop();t.push(n===g);break;case"exch":t.roll(2,1);break;case"exp":g=t.pop();n=t.pop();t.push(n**g);break;case"false":t.push(!1);break;case"floor":n=t.pop();t.push(Math.floor(n));break;case"ge":g=t.pop();n=t.pop();t.push(n>=g);break;case"gt":g=t.pop();n=t.pop();t.push(n>g);break;case"idiv":g=t.pop();n=t.pop();t.push(n/g|0);break;case"index":n=t.pop();t.index(n);break;case"le":g=t.pop();n=t.pop();t.push(n<=g);break;case"ln":n=t.pop();t.push(Math.log(n));break;case"log":n=t.pop();t.push(Math.log10(n));break;case"lt":g=t.pop();n=t.pop();t.push(n=t?new AstLiteral(t):e.max<=t?e:new AstMin(e,t)}class PostScriptCompiler{compile(e,t,i){const a=[],s=[],r=t.length>>1,n=i.length>>1;let g,o,c,C,h,l,Q,E,u=0;for(let e=0;et.min){g.unshift("Math.max(",r,", ");g.push(")")}if(n4){a=!0;t=0}else{a=!1;t=1}const o=[];for(r=0;r=0&&"ET"===gs[e];--e)gs[e]="EN";for(let e=r+1;e0&&(t=gs[r-1]);let i=h;e+1E&&isOdd(E)&&(d=E)}for(E=u;E>=d;--E){let e=-1;for(r=0,n=o.length;r=0){reverseValues(ns,e,r);e=-1}}else e<0&&(e=r);e>=0&&reverseValues(ns,e,o.length)}for(r=0,n=ns.length;r"!==e||(ns[r]="")}return createBidiText(ns.join(""),a)}const os={style:"normal",weight:"normal"},Is={style:"normal",weight:"bold"},cs={style:"italic",weight:"normal"},Cs={style:"italic",weight:"bold"},hs=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:os,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:Is,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:cs,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:Cs,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:os,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:Is,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:cs,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:Cs,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:os,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:Is,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:cs,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:Cs,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:os,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:Is,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:cs,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:Cs,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:os,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:Is,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:cs,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:Cs,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:os}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}]]),ls=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter((e=>!t.has(e.toLowerCase()))).join(" ")}function generateFont({alias:e,local:t,path:i,fallback:a,style:s,ultimate:r},n,g,o=!0,c=!0,C=""){const h={style:null,ultimate:null};if(t){const e=C?` ${C}`:"";for(const i of t)n.push(`local(${i}${e})`)}if(e){const t=hs.get(e),r=C||function getStyleToAppend(e){switch(e){case Is:return"Bold";case cs:return"Italic";case Cs:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(s);Object.assign(h,generateFont(t,n,g,o&&!a,c&&!i,r))}s&&(h.style=s);r&&(h.ultimate=r);if(o&&a){const e=hs.get(a),{ultimate:t}=generateFont(e,n,g,o,c&&!i,C);h.ultimate||=t}c&&i&&g&&n.push(`url(${g}${i})`);return h}function getFontSubstitution(e,t,i,a,s,r){if(a.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==r&&"Type1"!==r||!/^[A-Z]{6}\+/.test(a)||(a=a.slice(7));const n=a=normalizeFontName(a);let g=e.get(n);if(g)return g;let o=hs.get(a);if(!o)for(const[e,t]of ls)if(a.startsWith(e)){a=`${t}${a.substring(e.length)}`;o=hs.get(a);break}let c=!1;if(!o){o=hs.get(s);c=!0}const C=`${t.getDocId()}_s${t.createFontId()}`;if(!o){if(!validateFontName(a)){warn(`Cannot substitute the font because of its name: ${a}`);e.set(n,null);return null}const t=/bold/gi.test(a),i=/oblique|italic/gi.test(a),s=t&&i&&Cs||t&&Is||i&&cs||os;g={css:`"${getFamilyName(a)}",${C}`,guessFallback:!0,loadedName:C,baseFontName:a,src:`local(${a})`,style:s};e.set(n,g);return g}const h=[];c&&validateFontName(a)&&h.push(`local(${a})`);const{style:l,ultimate:Q}=generateFont(o,h,i),E=null===Q,u=E?"":`,${Q}`;g={css:`"${getFamilyName(a)}",${C}${u}`,guessFallback:E,loadedName:C,baseFontName:a,src:h.join(","),style:l};e.set(n,g);return g}class ImageResizer{static#R=2048;static#y=FeatureTest.isImageDecoderSupported;constructor(e,t){this._imgData=e;this._isMask=t}static get canUseImageDecoder(){return shadow(this,"canUseImageDecoder",this.#y?ImageDecoder.isTypeSupported("image/bmp"):Promise.resolve(!1))}static needsToBeResized(e,t){if(e<=this.#R&&t<=this.#R)return!1;const{MAX_DIM:i}=this;if(e>i||t>i)return!0;const a=e*t;if(this._hasMaxArea)return a>this.MAX_AREA;if(a(this.MAX_AREA=this.#R**2)}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,65537,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#R,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#y=t}static _areGoodDims(e,t){try{const i=new OffscreenCanvas(e,t),a=i.getContext("2d");a.fillRect(0,0,1,1);const s=a.getImageData(0,0,1,1).data[3];i.width=i.height=1;return 0!==s}catch{return!1}}static _guessMax(e,t,i,a){for(;e+i+1pt){const e=this.#N();if(e)return e}const a=this._encodeBMP();let s,r;if(await ImageResizer.canUseImageDecoder){s=new ImageDecoder({data:a,type:"image/bmp",preferAnimation:!1,transfer:[a.buffer]});r=s.decode().catch((e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))})).finally((()=>{s.close()}))}else r=createImageBitmap(new Blob([a.buffer],{type:"image/bmp"}));const{MAX_AREA:n,MAX_DIM:g}=ImageResizer,o=Math.max(t/g,i/g,Math.sqrt(t*i/n)),c=Math.max(o,2),C=Math.round(10*(o+1.25))/10/c,h=Math.floor(Math.log2(C)),l=new Array(h+2).fill(2);l[0]=c;l.splice(-1,1,C/(1<>n,o=a>>n;let c,C=a;try{c=new Uint8Array(r)}catch{let e=Math.floor(Math.log2(r+1));for(;;)try{c=new Uint8Array(2**e-1);break}catch{e-=1}C=Math.floor((2**e-1)/(4*i));const t=i*C*4;t>n;e>3,n=i+3&-4;if(i!==n){const e=new Uint8Array(n*t);let a=0;for(let r=0,g=t*i;r>>8;t[i++]=255&s}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();i=t.byteLength}const a=i>>2,s=i-4*a,r=new Uint32Array(t.buffer,0,a);let n=0,g=0,o=this.h1,c=this.h2;const C=3432918353,h=461845907,l=11601,Q=13715;for(let e=0;e>>17;n=n*h&Qs|n*Q&Es;o^=n;o=o<<13|o>>>19;o=5*o+3864292196}else{g=r[e];g=g*C&Qs|g*l&Es;g=g<<15|g>>>17;g=g*h&Qs|g*Q&Es;c^=g;c=c<<13|c>>>19;c=5*c+3864292196}n=0;switch(s){case 3:n^=t[4*a+2]<<16;case 2:n^=t[4*a+1]<<8;case 1:n^=t[4*a];n=n*C&Qs|n*l&Es;n=n<<15|n>>>17;n=n*h&Qs|n*Q&Es;1&a?o^=n:c^=n}this.h1=o;this.h2=c}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&Qs|36045*e&Es;t=4283543511*t&Qs|(2950163797*(t<<16|e>>>16)&Qs)>>>16;e^=t>>>1;e=444984403*e&Qs|60499*e&Es;t=3301882366*t&Qs|(3120437893*(t<<16|e>>>16)&Qs)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}function addState(e,t,i,a,s){let r=e;for(let e=0,i=t.length-1;e1e3){c=Math.max(c,l);Q+=h+2;l=0;h=0}C.push({transform:t,x:l,y:Q,w:i.width,h:i.height});l+=i.width+2;h=Math.max(h,i.height)}const E=Math.max(c,l)+1,u=Q+h+1,d=new Uint8Array(E*u*4),f=E<<2;for(let e=0;e=0;){t[r-4]=t[r];t[r-3]=t[r+1];t[r-2]=t[r+2];t[r-1]=t[r+3];t[r+i]=t[r+i-4];t[r+i+1]=t[r+i-3];t[r+i+2]=t[r+i-2];t[r+i+3]=t[r+i-1];r-=f}}const p={width:E,height:u};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(E,u);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(d.buffer),E,u),0,0);p.bitmap=e.transferToImageBitmap();p.data=null}else{p.kind=S;p.data=d}i.splice(r,4*o,$e);a.splice(r,4*o,[p,C]);return r+1}));addState(us,[MA,xA,Ze,UA],null,(function iterateImageMaskGroup(e,t){const i=e.fnArray,a=(t-(e.iCurr-3))%4;switch(a){case 0:return i[t]===MA;case 1:return i[t]===xA;case 2:return i[t]===Ze;case 3:return i[t]===UA}throw new Error(`iterateImageMaskGroup - invalid pos: ${a}`)}),(function foundImageMaskGroup(e,t){const i=e.fnArray,a=e.argsArray,s=e.iCurr,r=s-3,n=s-2,g=s-1;let o=Math.floor((t-r)/4);if(o<10)return t-(t-r)%4;let c,C,h=!1;const l=a[g][0],Q=a[n][0],E=a[n][1],u=a[n][2],d=a[n][3];if(E===u){h=!0;c=n+4;let e=g+4;for(let t=1;t=4&&i[r-4]===i[n]&&i[r-3]===i[g]&&i[r-2]===i[o]&&i[r-1]===i[c]&&a[r-4][0]===C&&a[r-4][1]===h){l++;Q-=5}let E=Q+4;for(let e=1;e=i)break}a=(a||us)[e[t]];if(a&&!Array.isArray(a)){r.iCurr=t;t++;if(!a.checkFn||(0,a.checkFn)(r)){s=a;a=null}else a=null}else t++}this.state=a;this.match=s;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&E?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}set isOffscreenCanvasSupported(e){this.optimizer.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===UA||e===ee))&&this.flush()}addImageOps(e,t,i,a=!1){if(a){this.addOp(MA);this.addOp(GA,[[["SMask",!1]]])}void 0!==i&&this.addOp(Ye,["OC",i]);this.addOp(e,t);void 0!==i&&this.addOp(ve,[]);a&&this.addOp(UA)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(wA,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,i=e.length;ta&&(e=a);return e}function resizeImageMask(e,t,i,a,s,r){const n=s*r;let g;g=t<=8?new Uint8Array(n):t<=16?new Uint16Array(n):new Uint32Array(n);const o=i/s,c=a/r;let C,h,l,Q,E=0;const u=new Uint16Array(s),d=i;for(C=0;C0&&Number.isInteger(i.height)&&i.height>0&&(i.width!==l||i.height!==Q)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");l=i.width;Q=i.height}if(l<1||Q<1)throw new FormatError(`Invalid image width: ${l} or height: ${Q}`);this.width=l;this.height=Q;this.interpolate=c.get("I","Interpolate");this.imageMask=c.get("IM","ImageMask")||!1;this.matte=c.get("Matte")||!1;let E=i.bitsPerComponent;if(!E){E=c.get("BPC","BitsPerComponent");if(!E){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);E=1}}this.bpc=E;if(!this.imageMask){let s=c.getRaw("CS")||c.getRaw("ColorSpace");const r=!!s;if(r)this.jpxDecoderOptions?.smaskInData&&(s=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)s=Name.get("DeviceRGBA");else switch(i.numComps){case 1:s=Name.get("DeviceGray");break;case 3:s=Name.get("DeviceRGB");break;case 4:s=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${i.numComps} color components not supported.`)}this.colorSpace=ColorSpace.parse({cs:s,xref:e,resources:a?t:null,pdfFunctionFactory:g,localColorSpaceCache:o});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=r?this.numComp:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=c.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,E)||n&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<>3)*i,g=e.byteLength;let o,c;if(!a||s&&!(n===g))if(s){o=new Uint8Array(n);o.set(e);o.fill(255,g)}else o=new Uint8Array(e);else o=e;if(s)for(c=0;c>7&1;n[l+1]=h>>6&1;n[l+2]=h>>5&1;n[l+3]=h>>4&1;n[l+4]=h>>3&1;n[l+5]=h>>2&1;n[l+6]=h>>1&1;n[l+7]=1&h;l+=8}if(l>=1}}}}else{let i=0;h=0;for(l=0,C=r;l>a;s<0?s=0:s>c&&(s=c);n[l]=s;h&=(1<n[a+1]){t=255;break}}g[C]=t}}}if(g)for(C=0,l=3,h=t*a;C>3,C=t&&ImageResizer.needsToBeResized(i,a);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){s.kind=S;const e=s.data=await this.getImageBytes(g*n*4,{});return t?C?ImageResizer.createImage(s,!1):this.createBitmap(S,i,a,e):s}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===o?e=b:"DeviceRGB"!==this.colorSpace.name||8!==o||this.needsDecode||(e=F);if(e&&!this.smask&&!this.mask&&i===n&&a===g){const r=await this.#G(n,g);if(r)return r;const o=await this.getImageBytes(g*c,{});if(t)return C?ImageResizer.createImage({data:o,kind:e,width:i,height:a,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,n,g,o);s.kind=e;s.data=o;if(this.needsDecode){assert(e===b,"PDFImage.createImageData: The image must be grayscale.");const t=s.data;for(let e=0,i=t.length;e>3,n=await this.getImageBytes(a*r,{internal:!0}),g=this.getComponents(n);let o,c;if(1===s){c=i*a;if(this.needsDecode)for(o=0;o0&&t.args[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checkedh){const e="Image exceeded maximum allowed size and was removed.";if(this.options.ignoreErrors){warn(e);return}throw new Error(e)}let l;g.has("OC")&&(l=await this.parseMarkedContentProps(g.get("OC"),e));let Q,E;if(g.get("IM","ImageMask")||!1){const e=g.get("I","Interpolate"),i=c+7>>3,n=t.getBytes(i*C),h=g.getArray("D","Decode");if(this.parsingType3Font){Q=PDFImage.createRawMask({imgArray:n,width:c,height:C,imageIsFromDecodeStream:t instanceof DecodeStream,inverseDecode:h?.[0]>0,interpolate:e});Q.cached=!!s;E=[Q];a.addImageOps(Ze,E,l);if(s){const e={fn:Ze,args:E,optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}Q=await PDFImage.createMask({imgArray:n,width:c,height:C,imageIsFromDecodeStream:t instanceof DecodeStream,inverseDecode:h?.[0]>0,interpolate:e,isOffscreenCanvasSupported:this.options.isOffscreenCanvasSupported});if(Q.isSingleOpaquePixel){a.addImageOps(tt,[],l);if(s){const e={fn:tt,args:[],optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}const u=`mask_${this.idFactory.createObjId()}`;a.addDependency(u);Q.dataLen=Q.bitmap?Q.width*Q.height*4:Q.data.length;this._sendImgData(u,Q);E=[{data:u,width:Q.width,height:Q.height,interpolate:Q.interpolate,count:1}];a.addImageOps(Ze,E,l);if(s){const e={objId:u,fn:Ze,args:E,optionalContent:l};r.set(s,o,e);o&&this._regionalImageCache.set(null,o,e)}return}const u=g.has("SMask")||g.has("Mask");if(i&&c+C<200&&!u){try{const s=new PDFImage({xref:this.xref,res:e,image:t,isInline:i,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:n});Q=await s.createImageData(!0,!1);a.isOffscreenCanvasSupported=this.options.isOffscreenCanvasSupported;a.addImageOps(_e,[Q],l)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!this.options.ignoreErrors)throw new Error(t);warn(t)}return}let d=`img_${this.idFactory.createObjId()}`,f=!1;if(this.parsingType3Font)d=`${this.idFactory.getDocId()}_type3_${d}`;else if(s&&o){f=this.globalImageCache.shouldCache(o,this.pageIndex);if(f){assert(!i,"Cannot cache an inline image globally.");d=`${this.idFactory.getDocId()}_${d}`}}a.addDependency(d);E=[d,c,C];a.addImageOps(ze,E,l,u);if(f){if(this.globalImageCache.hasDecodeFailed(o)){this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0});this._sendImgData(d,null,f);return}if(c*C>25e4||u){const e=await this.handler.sendWithPromise("commonobj",[d,"CopyLocalImage",{imageRef:o}]);if(e){this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0});this.globalImageCache.addByteSize(o,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:i,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:n}).then((async e=>{Q=await e.createImageData(!1,this.options.isOffscreenCanvasSupported);Q.dataLen=Q.bitmap?Q.width*Q.height*4:Q.data.length;Q.ref=o;f&&this.globalImageCache.addByteSize(o,Q.dataLen);return this._sendImgData(d,Q,f)})).catch((e=>{warn(`Unable to decode image "${d}": "${e}".`);o&&this.globalImageCache.addDecodeFailed(o);return this._sendImgData(d,null,f)}));if(s){const e={objId:d,fn:ze,args:E,optionalContent:l,hasMask:u};r.set(s,o,e);if(o){this._regionalImageCache.set(null,o,e);f&&this.globalImageCache.setData(o,{objId:d,fn:ze,args:E,optionalContent:l,hasMask:u,byteSize:0})}}}handleSMask(e,t,i,a,s,r){const n=e.get("G"),g={subtype:e.get("S").name,backdrop:e.get("BC")},o=e.get("TR");if(isPDFFunction(o)){const e=this._pdfFunctionFactory.create(o),t=new Uint8Array(256),i=new Float32Array(1);for(let a=0;a<256;a++){i[0]=a/255;e(i,0,i,0);t[a]=255*i[0]|0}g.transferMap=t}return this.buildFormXObject(t,n,g,i,a,s.state.clone(),r)}handleTransferFunction(e){let t;if(Array.isArray(e))t=e;else{if(!isPDFFunction(e))return null;t=[e]}const i=[];let a=0,s=0;for(const e of t){const t=this.xref.fetchIfRef(e);a++;if(isName(t,"Identity")){i.push(null);continue}if(!isPDFFunction(t))return null;const r=this._pdfFunctionFactory.create(t),n=new Uint8Array(256),g=new Float32Array(1);for(let e=0;e<256;e++){g[0]=e/255;r(g,0,g,0);n[e]=255*g[0]|0}i.push(n);s++}return 1!==a&&4!==a||0===s?null:i}handleTilingType(e,t,i,a,s,r,n,g){const o=new OperatorList,c=Dict.merge({xref:this.xref,dictArray:[s.get("Resources"),i]});return this.getOperatorList({stream:a,task:n,resources:c,operatorList:o}).then((function(){const i=o.getIR(),a=getTilingPatternIR(i,s,t);r.addDependencies(o.dependencies);r.addOp(e,a);s.objId&&g.set(null,s.objId,{operatorListIR:i,dict:s})})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}}))}async handleSetFont(e,t,i,a,s,r,n=null,g=null){const o=t?.[0]instanceof Name?t[0].name:null;let c=await this.loadFont(o,i,e,n,g);if(c.font.isType3Font)try{await c.loadType3Data(this,e,s);a.addDependencies(c.type3Dependencies)}catch(e){c=new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Type3 font load error: ${e}`),dict:c.font,evaluatorOptions:this.options})}r.font=c.font;c.send(this.handler);return c.loadedName}handleText(e,t){const i=t.font,a=i.charsToGlyphs(e);if(i.data){(!!(t.textRenderingMode&D)||"Pattern"===t.fillColorSpace.name||i.disableFontFace||this.options.disableFontFace)&&PartialEvaluator.buildFontPaths(i,a,this.handler,this.options)}return a}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:i,cacheKey:a,task:s,stateManager:r,localGStateCache:n,localColorSpaceCache:g}){const o=t.objId;let c=!0;const C=[];let h=Promise.resolve();for(const a of t.getKeys()){const n=t.get(a);switch(a){case"Type":break;case"LW":case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":C.push([a,n]);break;case"Font":c=!1;h=h.then((()=>this.handleSetFont(e,null,n[0],i,s,r.state).then((function(e){i.addDependency(e);C.push([a,[e,n[1]]])}))));break;case"BM":C.push([a,normalizeBlendMode(n)]);break;case"SMask":if(isName(n,"None")){C.push([a,!1]);break}if(n instanceof Dict){c=!1;h=h.then((()=>this.handleSMask(n,e,i,s,r,g)));C.push([a,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(n);C.push([a,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+a);break;default:info("Unknown graphic state operator "+a)}}await h;C.length>0&&i.addOp(GA,[C]);c&&n.set(a,o,C)}loadFont(e,t,i,a=null,s=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t,evaluatorOptions:this.options});let r;if(t)t instanceof Ref&&(r=t);else{const t=i.get("Font");t&&(r=t.getRaw(e))}if(r){if(this.type3FontRefs?.has(r))return errorFont();if(this.fontCache.has(r))return this.fontCache.get(r);try{t=this.xref.fetchIfRef(r)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=a||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:n,resolve:g}=Promise.withResolvers();let o;try{o=this.preEvaluateFont(t);o.cssFontInfo=s}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:c,hash:C}=o,h=r instanceof Ref;let l;if(C&&c instanceof Dict){const e=c.fontAliases||=Object.create(null);if(e[C]){const t=e[C].aliasRef;if(h&&t&&this.fontCache.has(t)){this.fontCache.putAlias(r,t);return this.fontCache.get(r)}}else e[C]={fontID:this.idFactory.createFontId()};h&&(e[C].aliasRef=r);l=e[C].fontID}else l=this.idFactory.createFontId();assert(l?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(h)this.fontCache.put(r,n);else{t.cacheKey=`cacheKey_${l}`;this.fontCache.put(t.cacheKey,n)}t.loadedName=`${this.idFactory.getDocId()}_${l}`;this.translateFont(o).then((e=>{g(new TranslatedFont({loadedName:t.loadedName,font:e,dict:t,evaluatorOptions:this.options}))})).catch((e=>{warn(`loadFont - translateFont failed: "${e}".`);g(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e instanceof Error?e.message:e),dict:t,evaluatorOptions:this.options}))}));return n}buildPath(e,t,i,a=!1){const s=e.length-1;i||(i=[]);if(s<0||e.fnArray[s]!==it){if(a){warn(`Encountered path operator "${t}" inside of a text object.`);e.addOp(MA,null)}let s;switch(t){case TA:const e=i[0]+i[2],t=i[1]+i[3];s=[Math.min(i[0],e),Math.min(i[1],t),Math.max(i[0],e),Math.max(i[1],t)];break;case LA:case HA:s=[i[0],i[1],i[0],i[1]];break;default:s=[1/0,1/0,-1/0,-1/0]}e.addOp(it,[[t],i,s]);a&&e.addOp(UA,null)}else{const a=e.argsArray[s];a[0].push(t);a[1].push(...i);const r=a[2];switch(t){case TA:const e=i[0]+i[2],t=i[1]+i[3];r[0]=Math.min(r[0],i[0],e);r[1]=Math.min(r[1],i[1],t);r[2]=Math.max(r[2],i[0],e);r[3]=Math.max(r[3],i[1],t);break;case LA:case HA:r[0]=Math.min(r[0],i[0]);r[1]=Math.min(r[1],i[1]);r[2]=Math.max(r[2],i[0]);r[3]=Math.max(r[3],i[1])}}}parseColorSpace({cs:e,resources:t,localColorSpaceCache:i}){return ColorSpace.parseAsync({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:i}).catch((e=>{if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}))}parseShading({shading:e,resources:t,localColorSpaceCache:i,localShadingPatternCache:a}){let s,r=a.get(e);if(r)return r;try{s=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,i).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);a.set(e,null);return null}throw t}r=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(r=`${this.idFactory.getDocId()}_type3_${r}`);a.set(e,r);this.parsingType3Font?this.handler.send("commonobj",[r,"Pattern",s]):this.handler.send("obj",[r,this.pageIndex,"Pattern",s]);return r}handleColorN(e,t,i,a,s,r,n,g,o,c){const C=i.pop();if(C instanceof Name){const h=s.getRaw(C.name),l=h instanceof Ref&&o.getByRef(h);if(l)try{const s=a.base?a.base.getRgb(i,0):null,r=getTilingPatternIR(l.operatorListIR,l.dict,s);e.addOp(t,r);return}catch{}const Q=this.xref.fetchIfRef(h);if(Q){const s=Q instanceof BaseStream?Q.dict:Q,C=s.get("PatternType");if(C===fs){const g=a.base?a.base.getRgb(i,0):null;return this.handleTilingType(t,g,r,Q,s,e,n,o)}if(C===ps){const i=s.get("Shading"),a=this.parseShading({shading:i,resources:r,localColorSpaceCache:g,localShadingPatternCache:c});if(a){const i=lookupMatrix(s.getArray("Matrix"),null);e.addOp(t,["Shading",a,i])}return}throw new FormatError(`Unknown PatternType: ${C}`)}}throw new FormatError(`Unknown PatternName: ${C}`)}_parseVisibilityExpression(e,t,i){if(++t>10){warn("Visibility expression is too deeply nested");return}const a=e.length,s=this.xref.fetchIfRef(e[0]);if(!(a<2)&&s instanceof Name){switch(s.name){case"And":case"Or":case"Not":i.push(s.name);break;default:warn(`Invalid operator ${s.name} in visibility expression`);return}for(let s=1;s0)return{type:"OCMD",expression:t}}const t=i.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const i of t)e.push(i.toString());else e.push(t.objId);return{type:a,ids:e,policy:i.get("P")instanceof Name?i.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:a,id:t.toString()}}return null}getOperatorList({stream:e,task:t,resources:i,operatorList:a,initialState:s=null,fallbackFontDict:r=null}){i||=Dict.empty;s||=new EvalState;if(!a)throw new Error('getOperatorList: missing "operatorList" parameter');const n=this,g=this.xref;let o=!1;const c=new LocalImageCache,C=new LocalColorSpaceCache,h=new LocalGStateCache,l=new LocalTilingPatternCache,Q=new Map,E=i.get("XObject")||Dict.empty,u=i.get("Pattern")||Dict.empty,d=new StateManager(s),f=new EvaluatorPreprocessor(e,g,d),p=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=f.savedStatesDepth;e0&&a.addOp(GA,[t]);e=null;continue}}next(new Promise((function(e,s){if(!S)throw new FormatError("GState must be referred to by name.");const r=i.get("ExtGState");if(!(r instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const g=r.get(F);if(!(g instanceof Dict))throw new FormatError("GState should be a dictionary.");n.setGState({resources:i,gState:g,operatorList:a,cacheKey:F,task:t,stateManager:d,localGStateCache:h,localColorSpaceCache:C}).then(e,s)})).catch((function(e){if(!(e instanceof AbortException)){if(!n.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}})));return;case LA:case HA:case JA:case YA:case vA:case KA:case TA:n.buildPath(a,s,e,o);continue;case Le:case He:case Ke:case Te:continue;case Ye:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);a.addOp(Ye,["OC",null]);continue}if("OC"===e[0].name){next(n.parseMarkedContentProps(e[1],i).then((e=>{a.addOp(Ye,["OC",e])})).catch((e=>{if(!(e instanceof AbortException)){if(!n.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);a.addOp(Ye,["OC",null])}})));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(w=0,D=e.length;w{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${t.name}" task: "${e}".`);closePendingRestoreOPS()}}))}getTextContent({stream:e,task:t,resources:s,stateManager:r=null,includeMarkedContent:n=!1,sink:g,seenStyles:o=new Set,viewBox:c,lang:C=null,markedContentData:h=null,disableNormalization:l=!1,keepWhiteSpace:Q=!1}){s||=Dict.empty;r||=new StateManager(new TextState);n&&(h||={level:0});const E={items:[],styles:Object.create(null),lang:C},u={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},d=[" "," "];let f=0;function saveLastChar(e){const t=(f+1)%2,i=" "!==d[f]&&" "===d[t];d[f]=e;f=t;return!Q&&i}function shouldAddWhitepsace(){return!Q&&" "!==d[f]&&" "===d[(f+1)%2]}function resetLastChars(){d[0]=d[1]=" ";f=0}const p=this,m=this.xref,y=[];let w=null;const D=new LocalImageCache,b=new LocalGStateCache,F=new EvaluatorPreprocessor(e,m,r);let S;function pushWhitespace({width:e=0,height:t=0,transform:i=u.prevTransform,fontName:a=u.fontName}){E.items.push({str:" ",dir:"ltr",width:e,height:t,transform:i,fontName:a,hasEOL:!1})}function getCurrentTextTransform(){const e=S.font,t=[S.fontSize*S.textHScale,0,0,S.fontSize,0,S.textRise];if(e.isType3Font&&(S.fontSize<=1||e.isCharBBox)&&!isArrayEqual(S.fontMatrix,a)){const i=e.bbox[3]-e.bbox[1];i>0&&(t[3]*=i*S.fontMatrix[3])}return Util.transform(S.ctm,Util.transform(S.textMatrix,t))}function ensureTextContentItem(){if(u.initialized)return u;const{font:e,loadedName:t}=S;if(!o.has(t)){o.add(t);E.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(p.options.fontExtraProperties&&e.systemFontInfo){const i=E.styles[t];i.fontSubstitution=e.systemFontInfo.css;i.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}u.fontName=t;const i=u.transform=getCurrentTextTransform();if(e.vertical){u.width=u.totalWidth=Math.hypot(i[0],i[1]);u.height=u.totalHeight=0;u.vertical=!0}else{u.width=u.totalWidth=0;u.height=u.totalHeight=Math.hypot(i[2],i[3]);u.vertical=!1}const a=Math.hypot(S.textLineMatrix[0],S.textLineMatrix[1]),s=Math.hypot(S.ctm[0],S.ctm[1]);u.textAdvanceScale=s*a;const{fontSize:r}=S;u.trackingSpaceMin=.102*r;u.notASpace=.03*r;u.negativeSpaceMax=-.2*r;u.spaceInFlowMin=.102*r;u.spaceInFlowMax=.6*r;u.hasEOL=!1;u.initialized=!0;return u}function updateAdvanceScale(){if(!u.initialized)return;const e=Math.hypot(S.textLineMatrix[0],S.textLineMatrix[1]),t=Math.hypot(S.ctm[0],S.ctm[1])*e;if(t!==u.textAdvanceScale){if(u.vertical){u.totalHeight+=u.height*u.textAdvanceScale;u.height=0}else{u.totalWidth+=u.width*u.textAdvanceScale;u.width=0}u.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");l||(t=function normalizeUnicode(e){if(!Ct){Ct=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ht=new Map([["ſt","ſt"]])}return e.replaceAll(Ct,((e,t,i)=>t?t.normalize("NFKC"):ht.get(i)))}(t));const i=bidi(t,-1,e.vertical);return{str:i.str,dir:i.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,i){const r=await p.loadFont(e,i,s);if(r.font.isType3Font)try{await r.loadType3Data(p,s,t)}catch{}S.loadedName=r.loadedName;S.font=r.font;S.fontMatrix=r.font.fontMatrix||a}function applyInverseRotation(e,t,i){const a=Math.hypot(i[0],i[1]);return[(i[0]*e+i[1]*t)/a,(i[2]*e+i[3]*t)/a]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let i=t[4],a=t[5];if(S.font?.vertical){if(ic[2]||a+ec[3])return!1}else if(i+ec[2]||ac[3])return!1;if(!S.font||!u.prevTransform)return!0;let s=u.prevTransform[4],r=u.prevTransform[5];if(s===i&&r===a)return!0;let n=-1;t[0]&&0===t[1]&&0===t[2]?n=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(n=t[1]>0?90:270);switch(n){case 0:break;case 90:[i,a]=[a,i];[s,r]=[r,s];break;case 180:[i,a,s,r]=[-i,-a,-s,-r];break;case 270:[i,a]=[-a,-i];[s,r]=[-r,-s];break;default:[i,a]=applyInverseRotation(i,a,t);[s,r]=applyInverseRotation(s,r,u.prevTransform)}if(S.font.vertical){const e=(r-a)/u.textAdvanceScale,t=i-s,n=Math.sign(u.height);if(e.5*u.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>u.width){appendEOL();return!0}e<=n*u.notASpace&&resetLastChars();if(e<=n*u.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else u.height+=e;else if(!addFakeSpaces(e,u.prevTransform,n))if(0===u.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else u.height+=e;Math.abs(t)>.25*u.width&&flushTextContentItem();return!0}const g=(i-s)/u.textAdvanceScale,o=a-r,C=Math.sign(u.width);if(g.5*u.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(o)>u.height){appendEOL();return!0}g<=C*u.notASpace&&resetLastChars();if(g<=C*u.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(g)})}else u.width+=g;else if(!addFakeSpaces(g,u.prevTransform,C))if(0===u.str.length){resetLastChars();pushWhitespace({width:Math.abs(g)})}else u.width+=g;Math.abs(o)>.25*u.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){const i=S.font;if(!e){const e=S.charSpacing+t;e&&(i.vertical?S.translateTextMatrix(0,-e):S.translateTextMatrix(e*S.textHScale,0));Q&&compareWithLastPosition(0);return}const a=i.charsToGlyphs(e),s=S.fontMatrix[0]*S.fontSize;for(let e=0,r=a.length;e0){const e=y.join("");y.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case he:if(!r.state.font){p.ensureStateFont(r.state);continue}buildTextContentItem({chars:N[0],extraSpacing:0});break;case Be:if(!r.state.font){p.ensureStateFont(r.state);continue}S.carriageReturn();buildTextContentItem({chars:N[0],extraSpacing:0});break;case Qe:if(!r.state.font){p.ensureStateFont(r.state);continue}S.wordSpacing=N[0];S.charSpacing=N[1];S.carriageReturn();buildTextContentItem({chars:N[2],extraSpacing:0});break;case xe:flushTextContentItem();w??=s.get("XObject")||Dict.empty;R=N[0]instanceof Name;f=N[0].name;if(R&&D.getByName(f))break;next(new Promise((function(e,i){if(!R)throw new FormatError("XObject must be referred to by name.");let a=w.getRaw(f);if(a instanceof Ref){if(D.getByRef(a)){e();return}if(p.globalImageCache.getData(a,p.pageIndex)){e();return}a=m.fetch(a)}if(!(a instanceof BaseStream))throw new FormatError("XObject should be a stream");const E=a.dict.get("Subtype");if(!(E instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==E.name){D.set(f,a.dict.objId,!0);e();return}const u=r.state.clone(),d=new StateManager(u),y=lookupMatrix(a.dict.getArray("Matrix"),null);y&&d.transform(y);enqueueChunk();const b={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;g.enqueue(e,t)},get desiredSize(){return g.desiredSize},get ready(){return g.ready}};p.getTextContent({stream:a,task:t,resources:a.dict.get("Resources")||s,stateManager:d,includeMarkedContent:n,sink:b,seenStyles:o,viewBox:c,lang:C,markedContentData:h,disableNormalization:l,keepWhiteSpace:Q}).then((function(){b.enqueueInvoked||D.set(f,a.dict.objId,!0);e()}),i)})).catch((function(e){if(!(e instanceof AbortException)){if(!p.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}})));return;case GA:R=N[0]instanceof Name;f=N[0].name;if(R&&b.getByName(f))break;next(new Promise((function(e,t){if(!R)throw new FormatError("GState must be referred to by name.");const i=s.get("ExtGState");if(!(i instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const a=i.get(f);if(!(a instanceof Dict))throw new FormatError("GState should be a dictionary.");const r=a.get("Font");if(r){flushTextContentItem();S.fontName=null;S.fontSize=r[1];handleSetFont(null,r[0]).then(e,t)}else{b.set(f,a.objId,!0);e()}})).catch((function(e){if(!(e instanceof AbortException)){if(!p.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}})));return;case Je:flushTextContentItem();if(n){h.level++;E.items.push({type:"beginMarkedContent",tag:N[0]instanceof Name?N[0].name:null})}break;case Ye:flushTextContentItem();if(n){h.level++;let e=null;N[1]instanceof Dict&&(e=N[1].get("MCID"));E.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${p.idFactory.getPageObjId()}_mc${e}`:null,tag:N[0]instanceof Name?N[0].name:null})}break;case ve:flushTextContentItem();if(n){if(0===h.level)break;h.level--;E.items.push({type:"endMarkedContent"})}break;case UA:!e||e.font===S.font&&e.fontSize===S.fontSize&&e.fontName===S.fontName||flushTextContentItem()}if(E.items.length>=g.desiredSize){d=!0;break}}if(d)next(ms);else{flushTextContentItem();enqueueChunk();e()}})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${t.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}}))}async extractDataStructures(e,t){const i=this.xref;let a;const s=this.readToUnicode(t.toUnicode);if(t.composite){const i=e.get("CIDSystemInfo");i instanceof Dict&&(t.cidSystemInfo={registry:stringToPDFString(i.get("Registry")),ordering:stringToPDFString(i.get("Ordering")),supplement:i.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(a=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const r=[];let n,g=null;if(e.has("Encoding")){n=e.get("Encoding");if(n instanceof Dict){g=n.get("BaseEncoding");g=g instanceof Name?g.name:null;if(n.has("Differences")){const e=n.get("Differences");let t=0;for(const a of e){const e=i.fetchIfRef(a);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);r[t++]=e.name}}}}else if(n instanceof Name)g=n.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==g&&"MacExpertEncoding"!==g&&"WinAnsiEncoding"!==g&&(g=null)}const o=!t.file||t.isInternalFont,c=qi()[t.name];g&&o&&c&&(g=null);if(g)t.defaultEncoding=getEncoding(g);else{const e=!!(t.flags&Mi),i=!!(t.flags&xi);n=hi;"TrueType"!==t.type||i||(n=li);if(e||c){n=Ci;o&&(/Symbol/i.test(t.name)?n=Bi:/Dingbats/i.test(t.name)?n=Qi:/Wingdings/i.test(t.name)&&(n=li))}t.defaultEncoding=n}t.differences=r;t.baseEncodingName=g;t.hasEncoding=!!g||r.length>0;t.dict=e;t.toUnicode=await s;const C=await this.buildToUnicode(t);t.toUnicode=C;a&&(t.cidToGidMap=this.readCidToGidMap(a,C));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const i=[],a=e.defaultEncoding.slice(),s=e.baseEncodingName,r=e.differences;for(const e in r){const t=r[e];".notdef"!==t&&(a[e]=t)}const n=wi();for(const r in a){let g=a[r];if(""===g)continue;let o=n[g];if(void 0!==o){i[r]=String.fromCharCode(o);continue}let c=0;switch(g[0]){case"G":3===g.length&&(c=parseInt(g.substring(1),16));break;case"g":5===g.length&&(c=parseInt(g.substring(1),16));break;case"C":case"c":if(g.length>=3&&g.length<=4){const i=g.substring(1);if(t){c=parseInt(i,16);break}c=+i;if(Number.isNaN(c)&&Number.isInteger(parseInt(i,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":o=getUnicodeForGlyph(g,n);-1!==o&&(c=o);break;default:switch(g){case"f_h":case"f_t":case"T_h":i[r]=g.replaceAll("_","");continue}}if(c>0&&c<=1114111&&Number.isInteger(c)){if(s&&c===+r){const e=getEncoding(s);if(e&&(g=e[r])){i[r]=String.fromCharCode(n[g]);continue}}i[r]=String.fromCodePoint(c)}}return i}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:i}=e.cidSystemInfo,a=Name.get(`${t}-${i}-UCS2`),s=await CMapFactory.create({encoding:a,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),r=[],n=[];e.cMap.forEach((function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const i=s.lookup(t);if(i){n.length=0;for(let e=0,t=i.length;e>1;(0!==s||t.has(r))&&(i[r]=s)}return i}extractWidths(e,t,i){const a=this.xref;let s=[],r=0;const n=[];let g;if(i.composite){const t=e.get("DW");r="number"==typeof t?Math.ceil(t):1e3;const o=e.get("W");if(Array.isArray(o))for(let e=0,t=o.length;e{const t=o.get(e),s=new OperatorList;return a.getOperatorList({stream:t,task:i,resources:c,operatorList:s}).then((()=>{s.fnArray[0]===ue&&this._removeType3ColorOperators(s,E);C[e]=s.getIR();for(const e of s.dependencies)n.add(e)})).catch((function(t){warn(`Type3 font resource "${e}" is not available.`);const i=new OperatorList;C[e]=i.getIR()}))}));this.type3Loaded=g.then((()=>{r.charProcOperatorList=C;if(this._bbox){r.isCharBBox=!0;r.bbox=this._bbox}}));return this.type3Loaded}_removeType3ColorOperators(e,t=NaN){const i=Util.normalizeRect(e.argsArray[0].slice(2)),a=i[2]-i[0],s=i[3]-i[1],r=Math.hypot(a,s);if(0===a||0===s){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===t||Math.round(r/t)>=10){this._bbox||(this._bbox=[1/0,1/0,-1/0,-1/0]);this._bbox[0]=Math.min(this._bbox[0],i[0]);this._bbox[1]=Math.min(this._bbox[1],i[1]);this._bbox[2]=Math.max(this._bbox[2],i[2]);this._bbox[3]=Math.max(this._bbox[3],i[3])}let n=0,g=e.length;for(;n=LA&&r<=zA;if(s.variableArgs)g>n&&info(`Command ${a}: expected [0, ${n}] args, but received ${g} args.`);else{if(g!==n){const e=this.nonProcessedArgs;for(;g>n;){e.push(t.shift());g--}for(;gEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(r,t);e.fn=r;e.args=t;return!0}if(i===Bt)return!1;if(null!==i){null===t&&(t=[]);t.push(i);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case MA:this.stateManager.save();break;case UA:this.stateManager.restore();break;case xA:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:i,args:a}=e;switch(0|i){case re:const[e,i]=a;e instanceof Name&&(t.fontName=e.name);"number"==typeof i&&i>0&&(t.fontSize=i);break;case Se:ColorSpace.singletons.rgb.getRgbItem(a,0,t.fontColor,0);break;case be:ColorSpace.singletons.gray.getRgbItem(a,0,t.fontColor,0);break;case Re:ColorSpace.singletons.cmyk.getRgbItem(a,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,i){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=i;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpace.singletons.gray},i=!1;const a=[];try{for(;;){e.args.length=0;if(i||!this.read(e))break;const{fn:s,args:r}=e;switch(0|s){case MA:a.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case UA:t=a.pop()||t;break;case ce:t.scaleFactor*=Math.hypot(r[0],r[1]);break;case re:const[e,s]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof s&&s>0&&(t.fontSize=s*t.scaleFactor);break;case fe:t.fillColorSpace=ColorSpace.parse({cs:r[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:this._localColorSpaceCache});break;case ye:t.fillColorSpace.getRgbItem(r,0,t.fontColor,0);break;case Se:ColorSpace.singletons.rgb.getRgbItem(r,0,t.fontColor,0);break;case be:ColorSpace.singletons.gray.getRgbItem(r,0,t.fontColor,0);break;case Re:ColorSpace.singletons.cmyk.getRgbItem(r,0,t.fontColor,0);break;case he:case le:case Be:case Qe:i=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,(e=>numberToString(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const i=new OffscreenCanvas(1,1);this.ctxMeasure=i.getContext("2d",{willReadFrequently:!0});FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.set("Type",Name.get("FontDescriptor"));e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.set("FontStretch",Name.get("Normal"));e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("CIDFontType0"));e.set("CIDToGIDMap",Name.get("Identity"));e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],i=[...this.widths.entries()].sort();let a=null,s=null;for(const[e,r]of i)if(a)if(e===a+s.length)s.push(r);else{t.push(a,s);a=e;s=[r]}else{a=e;s=[r]}a&&t.push(a,s);e.set("W",t);const r=new Dict(this.xref);r.set("Ordering","Identity");r.set("Registry","Adobe");r.set("Supplement",0);e.set("CIDSystemInfo",r);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type0"));e.set("Encoding",Name.get("Identity-H"));e.set("DescendantFonts",[this.descendantFontRef]);e.set("ToUnicode",Name.get("Identity-H"));return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const i of e.split(/\r\n?|\n/))for(const e of i.split("")){const i=e.charCodeAt(0);if(this.widths.has(i))continue;const a=t.measureText(e),s=Math.ceil(a.width);this.widths.set(i,s);this.firstChar=Math.min(i,this.firstChar);this.lastChar=Math.max(i,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,i){const[a,n,g,o]=e;let c=g-a,C=o-n;t%180!=0&&([c,C]=[C,c]);const h=s*i;return{coords:[0,C+r*i-h],bbox:[0,0,c,C],matrix:0!==t?getRotationMatrix(t,C,h):void 0}}createAppearance(e,t,i,a,n,g){const o=this._createContext(),c=[];let C=-1/0;for(const t of e.split(/\r\n?|\n/)){c.push(t);const e=o.measureText(t).width;C=Math.max(C,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let i=this.widths.get(e);if(void 0===i){const a=o.measureText(t);i=Math.ceil(a.width);this.widths.set(e,i);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}C*=a/1e3;const[h,l,Q,E]=t;let u=Q-h,d=E-l;i%180!=0&&([u,d]=[d,u]);let f=1;C>u&&(f=u/C);let p=1;const m=s*a,y=r*a,w=m*c.length;w>d&&(p=d/w);const D=a*Math.min(f,p),b=["q",`0 0 ${numberToString(u)} ${numberToString(d)} re W n`,"BT",`1 0 0 1 0 ${numberToString(d+y)} Tm 0 Tc ${getPdfColor(n,!0)}`,`/${this.fontName.name} ${numberToString(D)} Tf`],{resources:F}=this;if(1!==(g="number"==typeof g&&g>=0&&g<=1?g:1)){b.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",g);t.set("CA",g);t.set("Type",Name.get("ExtGState"));e.set("R0",t);F.set("ExtGState",e)}const S=numberToString(m);for(const e of c)b.push(`0 -${S} Td <${stringToUTF16HexString(e)}> Tj`);b.push("ET","Q");const k=b.join("\n"),R=new Dict(this.xref);R.set("Subtype",Name.get("Form"));R.set("Type",Name.get("XObject"));R.set("BBox",[0,0,u,d]);R.set("Length",k.length);R.set("Resources",F);if(i){const e=getRotationMatrix(i,u,d);R.set("Matrix",e)}const N=new StringStream(k);N.dict=R;return N}}class NameOrNumberTree{constructor(e,t,i){this.root=e;this.xref=t;this._type=i}getAll(){const e=new Map;if(!this.root)return e;const t=this.xref,i=new RefSet;i.put(this.root);const a=[this.root];for(;a.length>0;){const s=t.fetchIfRef(a.shift());if(!(s instanceof Dict))continue;if(s.has("Kids")){const e=s.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(i.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);a.push(t);i.put(t)}continue}const r=s.get(this._type);if(Array.isArray(r))for(let i=0,a=r.length;i10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const s=i.get("Kids");if(!Array.isArray(s))return null;let r=0,n=s.length-1;for(;r<=n;){const a=r+n>>1,g=t.fetchIfRef(s[a]),o=g.get("Limits");if(et.fetchIfRef(o[1]))){i=g;break}r=a+1}}if(r>n)return null}const s=i.get(this._type);if(Array.isArray(s)){let i=0,a=s.length-2;for(;i<=a;){const r=i+a>>1,n=r+(1&r),g=t.fetchIfRef(s[n]);if(eg))return s[n+1];i=n+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Qa=Object.create(null)}();!function clearPrimitiveCaches(){Qt=Object.create(null);Et=Object.create(null);ut=Object.create(null)}();!function clearUnicodeCaches(){ki.clear()}();JpxImage.cleanup()}function pickPlatformItem(e){return e instanceof Dict?e.has("UF")?e.get("UF"):e.has("F")?e.get("F"):e.has("Unix")?e.get("Unix"):e.has("Mac")?e.get("Mac"):e.has("DOS")?e.get("DOS"):null:null}class FileSpec{#U=!1;constructor(e,t,i=!1){if(e instanceof Dict){this.xref=t;this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");i||(e.has("EF")?this.#U=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){let e="";const t=pickPlatformItem(this.root);t&&"string"==typeof t&&(e=stringToPDFString(t).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"));return shadow(this,"filename",e||"unnamed")}get content(){if(!this.#U)return null;this._contentRef||=pickPlatformItem(this.root?.get("EF"));let e=null;if(this._contentRef){const t=this.xref.fetchIfRef(this._contentRef);t instanceof BaseStream?e=t.getBytes():warn("Embedded file specification points to non-existing/invalid content")}else warn("Embedded file specification does not have any content");return e}get description(){let e="";const t=this.root?.get("Desc");t&&"string"==typeof t&&(e=stringToPDFString(t));return shadow(this,"description",e)}get serializable(){return{rawFilename:this.filename,filename:(e=this.filename,e.substring(e.lastIndexOf("/")+1)),content:this.content,description:this.description};var e}}const ys=0,ws=-2,Ds=-3,bs=-4,Fs=-5,Ss=-6,ks=-9;function isWhitespace(e,t){const i=e[t];return" "===i||"\n"===i||"\r"===i||"\t"===i}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,((e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)}))}_parseContent(e,t){const i=[];let a=t;function skipWs(){for(;a"!==e[a]&&"/"!==e[a];)++a;const s=e.substring(t,a);skipWs();for(;a"!==e[a]&&"/"!==e[a]&&"?"!==e[a];){skipWs();let t="",s="";for(;a"!==e[i]&&"?"!==e[i]&&"/"!==e[i];)++i;const a=e.substring(t,i);!function skipWs(){for(;i"!==e[i+1]);)++i;return{name:a,value:e.substring(s,i),parsed:i-t}}parseXml(e){let t=0;for(;t",i);if(t<0){this.onError(ks);return}this.onEndElement(e.substring(i,t));i=t+1;break;case"?":++i;const a=this._parseProcessingInstruction(e,i);if("?>"!==e.substring(i+a.parsed,i+a.parsed+2)){this.onError(Ds);return}this.onPi(a.name,a.value);i+=a.parsed+2;break;case"!":if("--"===e.substring(i+1,i+3)){t=e.indexOf("--\x3e",i+3);if(t<0){this.onError(Fs);return}this.onComment(e.substring(i+3,t));i=t+3}else if("[CDATA["===e.substring(i+1,i+8)){t=e.indexOf("]]>",i+8);if(t<0){this.onError(ws);return}this.onCdata(e.substring(i+8,t));i=t+3}else{if("DOCTYPE"!==e.substring(i+1,i+8)){this.onError(Ss);return}{const a=e.indexOf("[",i+8);let s=!1;t=e.indexOf(">",i+8);if(t<0){this.onError(bs);return}if(a>0&&t>a){t=e.indexOf("]>",i+8);if(t<0){this.onError(bs);return}s=!0}const r=e.substring(i+8,t+(s?1:0));this.onDoctype(r);i=t+(s?2:1)}}break;default:const s=this._parseContent(e,i);if(null===s){this.onError(Ss);return}let r=!1;if("/>"===e.substring(i+s.parsed,i+s.parsed+2))r=!0;else if(">"!==e.substring(i+s.parsed,i+s.parsed+1)){this.onError(ks);return}this.onBeginElement(s.name,s.attributes,r);i+=s.parsed+(r?2:1)}}else{for(;i0}searchNode(e,t){if(t>=e.length)return this;const i=e[t];if(i.name.startsWith("#")&&t0){a.push([s,0]);s=s.childNodes[0]}else{if(0===a.length)return null;for(;0!==a.length;){const[e,t]=a.pop(),i=t+1;if(i");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=ys;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=ys;this.parseXml(e);if(this._errorCode!==ys)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,i=e.length;t\\376\\377([^<]+)/g,(function(e,t){const i=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,(function(e,t,i,a){return String.fromCharCode(64*t+8*i+1*a)})).replaceAll(/&(amp|apos|gt|lt|quot);/g,(function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)})),a=[">"];for(let e=0,t=i.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?a.push(String.fromCharCode(t)):a.push("&#x"+(65536+t).toString(16).substring(1)+";")}return a.join("")}))}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter((e=>"rdf:li"===e.nodeName))}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,i=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,i.map((e=>e.textContent.trim())))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const Rs=1,Ns=2,Gs=3,Ms=4,Us=5;class StructTreeRoot{constructor(e,t){this.dict=e;this.ref=t instanceof Ref?t:null;this.roleMap=new Map;this.structParentIds=null}init(){this.readRoleMap()}#x(e,t,i){if(!(e instanceof Ref)||t<0)return;this.structParentIds||=new RefSetCache;let a=this.structParentIds.get(e);if(!a){a=[];this.structParentIds.put(e,a)}a.push([t,i])}addAnnotationIdToPage(e,t){this.#x(e,t,Ms)}readRoleMap(){const e=this.dict.get("RoleMap");if(e instanceof Dict)for(const[t,i]of e)i instanceof Name&&this.roleMap.set(t,i.name)}static async canCreateStructureTree({catalogRef:e,pdfManager:t,newAnnotationsByPage:i}){if(!(e instanceof Ref)){warn("Cannot save the struct tree: no catalog reference.");return!1}let a=0,s=!0;for(const[e,r]of i){const{ref:i}=await t.getPage(e);if(!(i instanceof Ref)){warn(`Cannot save the struct tree: page ${e} has no ref.`);s=!0;break}for(const e of r)if(e.accessibilityData?.type){e.parentTreeId=a++;s=!1}}if(s){for(const e of i.values())for(const t of e)delete t.parentTreeId;return!1}return!0}static async createStructureTree({newAnnotationsByPage:e,xref:t,catalogRef:i,pdfManager:a,changes:s}){const r=a.catalog.cloneDict(),n=new RefSetCache;n.put(i,r);const g=t.getNewTemporaryRef();r.set("StructTreeRoot",g);const o=new Dict(t);o.set("Type",Name.get("StructTreeRoot"));const c=t.getNewTemporaryRef();o.set("ParentTree",c);const C=[];o.set("K",C);n.put(g,o);const h=new Dict(t),l=[];h.set("Nums",l);const Q=await this.#L({newAnnotationsByPage:e,structTreeRootRef:g,structTreeRoot:null,kids:C,nums:l,xref:t,pdfManager:a,changes:s,cache:n});o.set("ParentTreeNextKey",Q);n.put(c,h);for(const[e,t]of n.items())s.put(e,{data:t})}async canUpdateStructTree({pdfManager:e,xref:t,newAnnotationsByPage:i}){if(!this.ref){warn("Cannot update the struct tree: no root reference.");return!1}let a=this.dict.get("ParentTreeNextKey");if(!Number.isInteger(a)||a<0){warn("Cannot update the struct tree: invalid next key.");return!1}const s=this.dict.get("ParentTree");if(!(s instanceof Dict)){warn("Cannot update the struct tree: ParentTree isn't a dict.");return!1}const r=s.get("Nums");if(!Array.isArray(r)){warn("Cannot update the struct tree: nums isn't an array.");return!1}const n=new NumberTree(s,t);for(const t of i.keys()){const{pageDict:i}=await e.getPage(t);if(!i.has("StructParents"))continue;const a=i.get("StructParents");if(!Number.isInteger(a)||!Array.isArray(n.get(a))){warn(`Cannot save the struct tree: page ${t} has a wrong id.`);return!1}}let g=!0;for(const[t,s]of i){const{pageDict:i}=await e.getPage(t);StructTreeRoot.#H({elements:s,xref:this.dict.xref,pageDict:i,numberTree:n});for(const e of s)if(e.accessibilityData?.type){e.accessibilityData.structParent>=0||(e.parentTreeId=a++);g=!1}}if(g){for(const e of i.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:i}){const a=this.dict.xref,s=this.dict.clone(),r=this.ref,n=new RefSetCache;n.put(r,s);let g,o=s.getRaw("ParentTree");if(o instanceof Ref)g=a.fetch(o);else{g=o;o=a.getNewTemporaryRef();s.set("ParentTree",o)}g=g.clone();n.put(o,g);let c=g.getRaw("Nums"),C=null;if(c instanceof Ref){C=c;c=a.fetch(C)}c=c.slice();C||g.set("Nums",c);const h=await StructTreeRoot.#L({newAnnotationsByPage:e,structTreeRootRef:r,structTreeRoot:this,kids:null,nums:c,xref:a,pdfManager:t,changes:i,cache:n});if(-1!==h){s.set("ParentTreeNextKey",h);C&&n.put(C,c);for(const[e,t]of n.items())i.put(e,{data:t})}}static async#L({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:i,kids:a,nums:s,xref:r,pdfManager:n,changes:g,cache:o}){const c=Name.get("OBJR");let C,h=-1;for(const[l,Q]of e){const e=await n.getPage(l),{ref:E}=e,u=E instanceof Ref;for(const{accessibilityData:n,ref:d,parentTreeId:f,structTreeParent:p}of Q){if(!n?.type)continue;const{structParent:Q}=n;if(i&&Number.isInteger(Q)&&Q>=0){let t=(C||=new Map).get(l);if(void 0===t){t=new StructTreePage(i,e.pageDict).collectObjects(E);C.set(l,t)}const a=t?.get(Q);if(a){const e=r.fetch(a).clone();StructTreeRoot.#J(e,n);g.put(a,{data:e});continue}}h=Math.max(h,f);const m=r.getNewTemporaryRef(),y=new Dict(r);StructTreeRoot.#J(y,n);await this.#Y({structTreeParent:p,tagDict:y,newTagRef:m,structTreeRootRef:t,fallbackKids:a,xref:r,cache:o});const w=new Dict(r);y.set("K",w);w.set("Type",c);u&&w.set("Pg",E);w.set("Obj",d);o.put(m,y);s.push(f,m)}}return h+1}static#J(e,{type:t,title:i,lang:a,alt:s,expanded:r,actualText:n}){e.set("S",Name.get(t));i&&e.set("T",stringToAsciiOrUTF16BE(i));a&&e.set("Lang",stringToAsciiOrUTF16BE(a));s&&e.set("Alt",stringToAsciiOrUTF16BE(s));r&&e.set("E",stringToAsciiOrUTF16BE(r));n&&e.set("ActualText",stringToAsciiOrUTF16BE(n))}static#H({elements:e,xref:t,pageDict:i,numberTree:a}){const s=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);let i=s.get(e);if(!i){i=[];s.set(e,i)}i.push(t)}const r=i.get("StructParents");if(!Number.isInteger(r))return;const n=a.get(r),updateElement=(e,i,a)=>{const r=s.get(e);if(r){const e=i.getRaw("P"),s=t.fetchIfRef(e);if(e instanceof Ref&&s instanceof Dict){const e={ref:a,dict:i};for(const t of r)t.structTreeParent=e}return!0}return!1};for(const e of n){if(!(e instanceof Ref))continue;const i=t.fetch(e),a=i.get("K");if(Number.isInteger(a))updateElement(a,i,e);else if(Array.isArray(a))for(let s of a){s=t.fetchIfRef(s);if(Number.isInteger(s)&&updateElement(s,i,e))break;if(!(s instanceof Dict))continue;if(!isName(s.get("Type"),"MCR"))break;const a=s.get("MCID");if(Number.isInteger(a)&&updateElement(a,i,e))break}}}static async#Y({structTreeParent:e,tagDict:t,newTagRef:i,structTreeRootRef:a,fallbackKids:s,xref:r,cache:n}){let g,o=null;if(e){({ref:o}=e);g=e.dict.getRaw("P")||a}else g=a;t.set("P",g);const c=r.fetchIfRef(g);if(!c){s.push(i);return}let C=n.get(g);if(!C){C=c.clone();n.put(g,C)}const h=C.getRaw("K");let l=h instanceof Ref?n.get(h):null;if(!l){l=r.fetchIfRef(h);l=Array.isArray(l)?l.slice():[h];const e=r.getNewTemporaryRef();C.set("K",e);n.put(e,l)}const Q=l.indexOf(o);l.splice(Q>=0?Q+1:l.length,0,i)}}class StructElementNode{constructor(e,t){this.tree=e;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:i}=this.tree;return i.roleMap.has(t)?i.roleMap.get(t):t}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const i=this.dict.get("K");if(Array.isArray(i))for(const t of i){const i=this.parseKid(e,t);i&&this.kids.push(i)}else{const t=this.parseKid(e,i);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:Rs,mcid:t,pageObjId:e});let i=null;t instanceof Ref?i=this.dict.xref.fetch(t):t instanceof Dict&&(i=t);if(!i)return null;const a=i.getRaw("Pg");a instanceof Ref&&(e=a.toString());const s=i.get("Type")instanceof Name?i.get("Type").name:null;if("MCR"===s){if(this.tree.pageDict.objId!==e)return null;const t=i.getRaw("Stm");return new StructElement({type:Ns,refObjId:t instanceof Ref?t.toString():null,pageObjId:e,mcid:i.get("MCID")})}if("OBJR"===s){if(this.tree.pageDict.objId!==e)return null;const t=i.getRaw("Obj");return new StructElement({type:Gs,refObjId:t instanceof Ref?t.toString():null,pageObjId:e})}return new StructElement({type:Us,dict:i})}}class StructElement{constructor({type:e,dict:t=null,mcid:i=null,pageObjId:a=null,refObjId:s=null}){this.type=e;this.dict=t;this.mcid=i;this.pageObjId=a;this.refObjId=s;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.rootDict=e?e.dict:null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const i=this.root.structParentIds?.get(e);if(!i)return null;const a=new Map,s=new NumberTree(t,this.rootDict.xref);for(const[e]of i){const t=s.getRaw(e);t instanceof Ref&&a.set(e,t)}return a}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const t=this.rootDict.get("ParentTree");if(!t)return;const i=this.pageDict.get("StructParents"),a=this.root.structParentIds?.get(e);if(!Number.isInteger(i)&&!a)return;const s=new Map,r=new NumberTree(t,this.rootDict.xref);if(Number.isInteger(i)){const e=r.get(i);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.rootDict.xref.fetch(t),s)}if(a)for(const[e,t]of a){const i=r.get(e);if(i){const e=this.addNode(this.rootDict.xref.fetchIfRef(i),s);1===e?.kids?.length&&e.kids[0].type===Gs&&(e.kids[0].type=t)}}}addNode(e,t,i=0){if(i>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const a=new StructElementNode(this,e);t.set(e,a);const s=e.get("P");if(!s||isName(s.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,a)||t.delete(e);return a}const r=this.addNode(s,t,i+1);if(!r)return a;let n=!1;for(const t of r.kids)if(t.type===Us&&t.dict===e){t.parentNode=a;n=!0}n||t.delete(e);return a}addTopLevelNode(e,t){const i=this.rootDict.get("K");if(!i)return!1;if(i instanceof Dict){if(i.objId!==e.objId)return!1;this.nodes[0]=t;return!0}if(!Array.isArray(i))return!0;let a=!1;for(let s=0;s40){warn("StructTree too deep to be fully serialized.");return}const a=Object.create(null);a.role=e.role;a.children=[];t.children.push(a);let s=e.dict.get("Alt");"string"!=typeof s&&(s=e.dict.get("ActualText"));"string"==typeof s&&(a.alt=stringToPDFString(s));const r=e.dict.get("A");if(r instanceof Dict){const e=lookupNormalRect(r.getArray("BBox"),null);if(e)a.bbox=e;else{const e=r.get("Width"),t=r.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(a.bbox=[0,0,e,t])}}const n=e.dict.get("Lang");"string"==typeof n&&(a.lang=stringToPDFString(n));for(const t of e.kids){const e=t.type===Us?t.parentNode:null;e?nodeToSerializable(e,a,i+1):t.type===Rs||t.type===Ns?a.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===Gs?a.children.push({type:"object",id:t.refObjId}):t.type===Ms&&a.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}function isValidExplicitDest(e){if(!Array.isArray(e)||e.length<2)return!1;const[t,i,...a]=e;if(!(t instanceof Ref||Number.isInteger(t)))return!1;if(!(i instanceof Name))return!1;const s=a.length;let r=!0;switch(i.name){case"XYZ":if(s<2||s>3)return!1;break;case"Fit":case"FitB":return 0===s;case"FitH":case"FitBH":case"FitV":case"FitBV":if(s>1)return!1;break;case"FitR":if(4!==s)return!1;r=!1;break;default:return!1}for(const e of a)if(!("number"==typeof e||r&&null===e))return!1;return!0}function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return isValidExplicitDest(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t);if(isValidExplicitDest(t))return JSON.stringify(t)}return null}class Catalog{constructor(e,t){this.pdfManager=e;this.xref=t;this._catDict=t.getCatalogObj();if(!(this._catDict instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict;this._actualNumPages=null;this.fontCache=new RefSetCache;this.builtInCMapCache=new Map;this.standardFontDataCache=new Map;this.globalImageCache=new GlobalImageCache;this.pageKidsCountCache=new RefSetCache;this.pageIndexCache=new RefSetCache;this.pageDictCache=new RefSetCache;this.nonBlendModesSet=new RefSet;this.systemFontCache=new Map}cloneDict(){return this._catDict.clone()}get version(){const e=this._catDict.get("Version");if(e instanceof Name){if(ft.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this._catDict.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this._catDict.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this._catDict.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this._catDict.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this._catDict.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this._catDict.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const i=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(i instanceof BaseStream&&i.dict instanceof Dict){const e=i.dict.get("Type"),a=i.dict.get("Subtype");if(isName(e,"Metadata")&&isName(a,"XML")){const e=stringToUTF8String(i.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this._readMarkInfo()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}_readMarkInfo(){const e=this._catDict.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const i in t){const a=e.get(i);"boolean"==typeof a&&(t[i]=a)}return t}get structTreeRoot(){let e=null;try{e=this._readStructTreeRoot()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}_readStructTreeRoot(){const e=this._catDict.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const i=new StructTreeRoot(t,e);i.init();return i}get toplevelPagesDict(){const e=this._catDict.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this._readDocumentOutline()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}_readDocumentOutline(){let e=this._catDict.get("Outlines");if(!(e instanceof Dict))return null;e=e.getRaw("First");if(!(e instanceof Ref))return null;const t={items:[]},i=[{obj:e,parent:t}],a=new RefSet;a.put(e);const s=this.xref,r=new Uint8ClampedArray(3);for(;i.length>0;){const t=i.shift(),n=s.fetchIfRef(t.obj);if(null===n)continue;n.has("Title")||warn("Invalid outline item encountered.");const g={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:n,resultObj:g,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const o=n.get("Title"),c=n.get("F")||0,C=n.getArray("C"),h=n.get("Count");let l=r;!isNumberArray(C,3)||0===C[0]&&0===C[1]&&0===C[2]||(l=ColorSpace.singletons.rgb.getRgb(C,0));const Q={action:g.action,attachment:g.attachment,dest:g.dest,url:g.url,unsafeUrl:g.unsafeUrl,newWindow:g.newWindow,setOCGState:g.setOCGState,title:"string"==typeof o?stringToPDFString(o):"",color:l,count:Number.isInteger(h)?h:void 0,bold:!!(2&c),italic:!!(1&c),items:[]};t.parent.items.push(Q);e=n.getRaw("First");if(e instanceof Ref&&!a.has(e)){i.push({obj:e,parent:Q});a.put(e)}e=n.getRaw("Next");if(e instanceof Ref&&!a.has(e)){i.push({obj:e,parent:t.parent});a.put(e)}}return t.items.length>0?t.items:null}get permissions(){let e=null;try{e=this._readPermissions()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}_readPermissions(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const i=[];for(const e in y){const a=y[e];t&a&&i.push(a)}return i}get optionalContentConfig(){let e=null;try{const t=this._catDict.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const i=t.get("D");if(!i)return shadow(this,"optionalContentConfig",null);const a=t.get("OCGs");if(!Array.isArray(a))return shadow(this,"optionalContentConfig",null);const s=new RefSetCache;for(const e of a)e instanceof Ref&&!s.has(e)&&s.put(e,this.#v(e));e=this.#K(i,s)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#v(e){const t=this.xref.fetch(e),i={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},a=t.get("Name");"string"==typeof a&&(i.name=stringToPDFString(a));let s=t.getArray("Intent");Array.isArray(s)||(s=[s]);s.every((e=>e instanceof Name))&&(i.intent=s.map((e=>e.name)));const r=t.get("Usage");if(!(r instanceof Dict))return i;const n=i.usage,g=r.get("Print");if(g instanceof Dict){const e=g.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":n.print={printState:e.name}}}const o=r.get("View");if(o instanceof Dict){const e=o.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":n.view={viewState:e.name}}}return i}#K(e,t){function parseOnOff(e){const i=[];if(Array.isArray(e))for(const a of e)a instanceof Ref&&t.has(a)&&i.push(a.toString());return i}function parseOrder(e,i=0){if(!Array.isArray(e))return null;const s=[];for(const r of e){if(r instanceof Ref&&t.has(r)){a.put(r);s.push(r.toString());continue}const e=parseNestedOrder(r,i);e&&s.push(e)}if(i>0)return s;const r=[];for(const[e]of t.items())a.has(e)||r.push(e.toString());r.length&&s.push({name:null,order:r});return s}function parseNestedOrder(e,t){if(++t>s){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const a=i.fetchIfRef(e);if(!Array.isArray(a))return null;const r=i.fetchIfRef(a[0]);if("string"!=typeof r)return null;const n=parseOrder(a.slice(1),t);return n?.length?{name:stringToPDFString(r),order:n}:null}const i=this.xref,a=new RefSet,s=10;!function parseRBGroups(e){if(Array.isArray(e))for(const a of e){const e=i.fetchIfRef(a);if(!Array.isArray(e)||!e.length)continue;const s=new Set;for(const i of e)if(i instanceof Ref&&t.has(i)&&!s.has(i.toString())){s.add(i.toString());t.get(i).rbGroups.push(s)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this._actualNumPages=e}get hasActualNumPages(){return null!==this._actualNumPages}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.hasActualNumPages?this._actualNumPages:this._pagesCount}get destinations(){const e=this._readDests(),t=Object.create(null);if(e instanceof NameTree)for(const[i,a]of e.getAll()){const e=fetchDest(a);e&&(t[stringToPDFString(i)]=e)}else if(e instanceof Dict)for(const[i,a]of e){const e=fetchDest(a);e&&(t[i]=e)}return shadow(this,"destinations",t)}getDestination(e){const t=this._readDests();if(t instanceof NameTree){const i=fetchDest(t.get(e));if(i)return i;const a=this.destinations[e];if(a){warn(`Found "${e}" at an incorrect position in the NameTree.`);return a}}else if(t instanceof Dict){const i=fetchDest(t.get(e));if(i)return i}return null}_readDests(){const e=this._catDict.get("Names");return e?.has("Dests")?new NameTree(e.getRaw("Dests"),this.xref):this._catDict.has("Dests")?this._catDict.get("Dests"):void 0}get pageLabels(){let e=null;try{e=this._readPageLabels()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}_readPageLabels(){const e=this._catDict.getRaw("PageLabels");if(!e)return null;const t=new Array(this.numPages);let i=null,a="";const s=new NumberTree(e,this.xref).getAll();let r="",n=1;for(let e=0,g=this.numPages;e=1))throw new FormatError("Invalid start in PageLabel dictionary.");n=e}else n=1}switch(i){case"D":r=n;break;case"R":case"r":r=toRomanNumerals(n,"r"===i);break;case"A":case"a":const e=26,t="a"===i?97:65,a=n-1;r=String.fromCharCode(t+a%e).repeat(Math.floor(a/e)+1);break;default:if(i)throw new FormatError(`Invalid style "${i}" in PageLabel dictionary.`);r=""}t[e]=a+r;n++}return t}get pageLayout(){const e=this._catDict.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this._catDict.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this._catDict.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const i of e.getKeys()){const a=e.get(i);let s;switch(i){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof a&&(s=a);break;case"NonFullScreenPageMode":if(a instanceof Name)switch(a.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":s=a.name;break;default:s="UseNone"}break;case"Direction":if(a instanceof Name)switch(a.name){case"L2R":case"R2L":s=a.name;break;default:s="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(a instanceof Name)switch(a.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":s=a.name;break;default:s="CropBox"}break;case"PrintScaling":if(a instanceof Name)switch(a.name){case"None":case"AppDefault":s=a.name;break;default:s="AppDefault"}break;case"Duplex":if(a instanceof Name)switch(a.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":s=a.name;break;default:s="None"}break;case"PrintPageRange":if(Array.isArray(a)&&a.length%2==0){a.every(((e,t,i)=>Number.isInteger(e)&&e>0&&(0===t||e>=i[t-1])&&e<=this.numPages))&&(s=a)}break;case"NumCopies":Number.isInteger(a)&&a>0&&(s=a);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${i}.`);continue}if(void 0!==s){t||(t=Object.create(null));t[i]=s}else warn(`Bad value, for key "${i}", in ViewerPreferences: ${a}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this._catDict.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const i=new Dict(this.xref);i.set("A",e);const a={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:i,resultObj:a});Array.isArray(a.dest)?t.dest=a.dest:a.action&&(t.action=a.action)}else Array.isArray(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const i=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,a]of i.getAll()){const i=new FileSpec(a,this.xref);t||(t=Object.create(null));t[stringToPDFString(e)]=i.serializable}}return shadow(this,"attachments",t)}get xfaImages(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const i=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,a]of i.getAll()){t||(t=new Dict(this.xref));t.set(stringToPDFString(e),a)}}return shadow(this,"xfaImages",t)}_collectJavaScript(){const e=this._catDict.get("Names");let t=null;function appendIfJavaScriptDict(e,i){if(!(i instanceof Dict))return;if(!isName(i.get("S"),"JavaScript"))return;let a=i.get("JS");if(a instanceof BaseStream)a=a.getString();else if("string"!=typeof a)return;a=stringToPDFString(a).replaceAll("\0","");a&&(t||=new Map).set(e,a)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,i]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e),i)}const i=this._catDict.get("OpenAction");i&&appendIfJavaScriptDict("OpenAction",i);return t}get jsActions(){const e=this._collectJavaScript();let t=collectActions(this.xref,this._catDict,fA);if(e){t||=Object.create(null);for(const[i,a]of e)i in t?t[i].push(a):t[i]=[a]}return shadow(this,"jsActions",t)}async fontFallback(e,t){const i=await Promise.all(this.fontCache);for(const a of i)if(a.loadedName===e){a.fallback(t);return}}async cleanup(e=!1){clearGlobalCaches();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();const t=await Promise.all(this.fontCache);for(const{dict:e}of t)delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],i=new RefSet,a=this._catDict.getRaw("Pages");a instanceof Ref&&i.put(a);const s=this.xref,r=this.pageKidsCountCache,n=this.pageIndexCache,g=this.pageDictCache;let o=0;for(;t.length;){const a=t.pop();if(a instanceof Ref){const c=r.get(a);if(c>=0&&o+c<=e){o+=c;continue}if(i.has(a))throw new FormatError("Pages tree contains circular reference.");i.put(a);const C=await(g.get(a)||s.fetchAsync(a));if(C instanceof Dict){let t=C.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!C.has("Kids")){r.has(a)||r.put(a,1);n.has(a)||n.put(a,o);if(o===e)return[C,a];o++;continue}}t.push(C);continue}if(!(a instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:c}=a;let C=a.getRaw("Count");C instanceof Ref&&(C=await s.fetchAsync(C));if(Number.isInteger(C)&&C>=0){c&&!r.has(c)&&r.put(c,C);if(o+C<=e){o+=C;continue}}let h=a.getRaw("Kids");h instanceof Ref&&(h=await s.fetchAsync(h));if(!Array.isArray(h)){let t=a.getRaw("Type");t instanceof Ref&&(t=await s.fetchAsync(t));if(isName(t,"Page")||!a.has("Kids")){if(o===e)return[a,null];o++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=h.length-1;e>=0;e--){const i=h[e];t.push(i);a===this.toplevelPagesDict&&i instanceof Ref&&!g.has(i)&&g.put(i,s.fetchAsync(i))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,i=[{currentNode:this.toplevelPagesDict,posInKids:0}],a=new RefSet,s=this._catDict.getRaw("Pages");s instanceof Ref&&a.put(s);const r=new Map,n=this.xref,g=this.pageIndexCache;let o=0;function addPageDict(e,t){t&&!g.has(t)&&g.put(t,o);r.set(o++,[e,t])}function addPageError(i){if(i instanceof XRefEntryException&&!e)throw i;if(e&&t&&0===o){warn(`getAllPageDicts - Skipping invalid first page: "${i}".`);i=Dict.empty}r.set(o++,[i,null])}for(;i.length>0;){const e=i.at(-1),{currentNode:t,posInKids:s}=e;let r=t.getRaw("Kids");if(r instanceof Ref)try{r=await n.fetchAsync(r)}catch(e){addPageError(e);break}if(!Array.isArray(r)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(s>=r.length){i.pop();continue}const g=r[s];let o;if(g instanceof Ref){if(a.has(g)){addPageError(new FormatError("Pages tree contains circular reference."));break}a.put(g);try{o=await n.fetchAsync(g)}catch(e){addPageError(e);break}}else o=g;if(!(o instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let c=o.getRaw("Type");if(c instanceof Ref)try{c=await n.fetchAsync(c)}catch(e){addPageError(e);break}isName(c,"Page")||!o.has("Kids")?addPageDict(o,g instanceof Ref?g:null):i.push({currentNode:o,posInKids:0});e.posInKids++}return r}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const i=this.xref;let a=0;const next=t=>function pagesBeforeRef(t){let a,s=0;return i.fetchAsync(t).then((function(i){if(isRefsEqual(t,e)&&!isDict(i,"Page")&&!(i instanceof Dict&&!i.has("Type")&&i.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!i)return null;if(!(i instanceof Dict))throw new FormatError("Node must be a dictionary.");a=i.getRaw("Parent");return i.getAsync("Parent")})).then((function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")})).then((function(e){if(!e)return null;const r=[];let n=!1;for(const a of e){if(!(a instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(a,t)){n=!0;break}r.push(i.fetchAsync(a).then((function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?s+=e.get("Count"):s++})))}if(!n)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(r).then((function(){return[s,a]}))}))}(t).then((t=>{if(!t){this.pageIndexCache.put(e,a);return a}const[i,s]=t;a+=i;return next(s)}));return next(e)}get baseUrl(){const e=this._catDict.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static parseDestDictionary({destDict:e,resultObj:t,docBaseUrl:i=null,docAttachments:a=null}){if(!(e instanceof Dict)){warn("parseDestDictionary: `destDict` must be a dictionary.");return}let s,r,n=e.get("A");if(!(n instanceof Dict))if(e.has("Dest"))n=e.get("Dest");else{n=e.get("AA");n instanceof Dict&&(n.has("D")?n=n.get("D"):n.has("U")&&(n=n.get("U")))}if(n instanceof Dict){const e=n.get("S");if(!(e instanceof Name)){warn("parseDestDictionary: Invalid type in Action dictionary.");return}const i=e.name;switch(i){case"ResetForm":const e=n.get("Flags"),g=!(1&("number"==typeof e?e:0)),o=[],c=[];for(const e of n.get("Fields")||[])e instanceof Ref?c.push(e.toString()):"string"==typeof e&&o.push(stringToPDFString(e));t.resetForm={fields:o,refs:c,include:g};break;case"URI":s=n.get("URI");s instanceof Name&&(s="/"+s.name);break;case"GoTo":r=n.get("D");break;case"Launch":case"GoToR":const C=n.get("F");if(C instanceof Dict){const e=new FileSpec(C,null,!0),{rawFilename:t}=e.serializable;s=t}else"string"==typeof C&&(s=C);const h=fetchRemoteDest(n);h&&"string"==typeof s&&(s=s.split("#",1)[0]+"#"+h);const l=n.get("NewWindow");"boolean"==typeof l&&(t.newWindow=l);break;case"GoToE":const Q=n.get("T");let E;if(a&&Q instanceof Dict){const e=Q.get("R"),t=Q.get("N");isName(e,"C")&&"string"==typeof t&&(E=a[stringToPDFString(t)])}if(E){t.attachment=E;const e=fetchRemoteDest(n);e&&(t.attachmentDest=e)}else warn('parseDestDictionary - unimplemented "GoToE" action.');break;case"Named":const u=n.get("N");u instanceof Name&&(t.action=u.name);break;case"SetOCGState":const d=n.get("State"),f=n.get("PreserveRB");if(!Array.isArray(d)||0===d.length)break;const p=[];for(const e of d)if(e instanceof Name)switch(e.name){case"ON":case"OFF":case"Toggle":p.push(e.name)}else e instanceof Ref&&p.push(e.toString());if(p.length!==d.length)break;t.setOCGState={state:p,preserveRB:"boolean"!=typeof f||f};break;case"JavaScript":const m=n.get("JS");let y;m instanceof BaseStream?y=m.getString():"string"==typeof m&&(y=m);const w=y&&recoverJsURL(stringToPDFString(y));if(w){s=w.url;t.newWindow=w.newWindow;break}default:if("JavaScript"===i||"SubmitForm"===i)break;warn(`parseDestDictionary - unsupported action: "${i}".`)}}else e.has("Dest")&&(r=e.get("Dest"));if("string"==typeof s){const e=createValidAbsoluteUrl(s,i,{addDefaultProtocol:!0,tryConvertEncoding:!0});e&&(t.url=e.href);t.unsafeUrl=s}if(r){r instanceof Name&&(r=r.name);"string"==typeof r?t.dest=stringToPDFString(r):isValidExplicitDest(r)&&(t.dest=r)}}}function addChildren(e,t){if(e instanceof Dict)e=e.getRawValues();else if(e instanceof BaseStream)e=e.dict.getRawValues();else if(!Array.isArray(e))return;for(const a of e)((i=a)instanceof Ref||i instanceof Dict||i instanceof BaseStream||Array.isArray(i))&&t.push(a);var i}class ObjectLoader{constructor(e,t,i){this.dict=e;this.keys=t;this.xref=i;this.refSet=null}async load(){if(this.xref.stream.isDataLoaded)return;const{keys:e,dict:t}=this;this.refSet=new RefSet;const i=[];for(const a of e){const e=t.getRaw(a);void 0!==e&&i.push(e)}return this._walk(i)}async _walk(e){const t=[],i=[];for(;e.length;){let a=e.pop();if(a instanceof Ref){if(this.refSet.has(a))continue;try{this.refSet.put(a);a=this.xref.fetch(a)}catch(e){if(!(e instanceof MissingDataException)){warn(`ObjectLoader._walk - requesting all data: "${e}".`);this.refSet=null;const{manager:t}=this.xref.stream;return t.requestAllChunks()}t.push(a);i.push({begin:e.begin,end:e.end})}}if(a instanceof BaseStream){const e=a.getBaseStreams();if(e){let s=!1;for(const t of e)if(!t.isDataLoaded){s=!0;i.push({begin:t.start,end:t.end})}s&&t.push(a)}}addChildren(a,e)}if(i.length){await this.xref.stream.manager.requestRanges(i);for(const e of t)e instanceof Ref&&this.refSet.remove(e);return this._walk(t)}this.refSet=null}}const xs=Symbol(),Ls=Symbol(),Hs=Symbol(),Js=Symbol(),Ys=Symbol(),vs=Symbol(),Ks=Symbol(),Ts=Symbol(),qs=Symbol(),Os=Symbol("content"),Ws=Symbol("data"),js=Symbol(),Xs=Symbol("extra"),Zs=Symbol(),Vs=Symbol(),zs=Symbol(),_s=Symbol(),$s=Symbol(),Ar=Symbol(),er=Symbol(),tr=Symbol(),ir=Symbol(),ar=Symbol(),sr=Symbol(),rr=Symbol(),nr=Symbol(),gr=Symbol(),or=Symbol(),Ir=Symbol(),cr=Symbol(),Cr=Symbol(),hr=Symbol(),lr=Symbol(),Qr=Symbol(),Er=Symbol(),ur=Symbol(),dr=Symbol(),fr=Symbol(),pr=Symbol(),mr=Symbol(),yr=Symbol(),wr=Symbol(),Dr=Symbol(),br=Symbol(),Fr=Symbol(),Sr=Symbol("namespaceId"),kr=Symbol("nodeName"),Rr=Symbol(),Nr=Symbol(),Gr=Symbol(),Mr=Symbol(),Ur=Symbol(),xr=Symbol(),Lr=Symbol(),Hr=Symbol(),Jr=Symbol("root"),Yr=Symbol(),vr=Symbol(),Kr=Symbol(),Tr=Symbol(),qr=Symbol(),Or=Symbol(),Pr=Symbol(),Wr=Symbol(),jr=Symbol(),Xr=Symbol(),Zr=Symbol(),Vr=Symbol("uid"),zr=Symbol(),_r={config:{id:0,check:e=>e.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},$r={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},An=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:i}){if(!e)return t;e=e.trim();const a=parseInt(e,10);return!isNaN(a)&&i(a)?a:t}function getFloat({data:e,defaultValue:t,validate:i}){if(!e)return t;e=e.trim();const a=parseFloat(e);return!isNaN(a)&&i(a)?a:t}function getKeyword({data:e,defaultValue:t,validate:i}){return e&&i(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const i=e.trim().match(An);if(!i)return getMeasurement(t);const[,a,s]=i,r=parseFloat(a);if(isNaN(r))return getMeasurement(t);if(0===r)return 0;const n=$r[s];return n?n(r):r}function getRatio(e){if(!e)return{num:1,den:1};const t=e.trim().split(/\s*:\s*/).map((e=>parseFloat(e))).filter((e=>!isNaN(e)));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[i,a]=t;return{num:i,den:a}}function getRelevant(e){return e?e.trim().split(/\s+/).map((e=>({excluded:"-"===e[0],viewname:e.substring(1)}))):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,i,a){this.success=e;this.html=t;this.bbox=i;this.breakNode=a}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const i=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,i)}addPdfFont(e){const t=e.cssFontInfo,i=t.fontFamily;let a=this.fonts.get(i);if(!a){a=Object.create(null);this.fonts.set(i,a);this.defaultFont||(this.defaultFont=a)}let s="";const r=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?s=r>=700?"bolditalic":"italic":r>=700&&(s="bold");if(!s){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(s="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(s+="italic")}s||(s="regular");a[s]=e}getDefault(){return this.defaultFont}find(e,t=!0){let i=this.fonts.get(e)||this.cache.get(e);if(i)return i;const a=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let s=e.replaceAll(a,"");i=this.fonts.get(s);if(i){this.cache.set(e,i);return i}s=s.toLowerCase();const r=[];for(const[e,t]of this.fonts.entries())e.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(t);if(0===r.length)for(const[,e]of this.fonts.entries())e.regular.name?.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(e);if(0===r.length){s=s.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts.entries())e.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(t)}if(0===r.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(a,"").toLowerCase().startsWith(s)&&r.push(e);if(r.length>=1){1!==r.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,r[0]);return r[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,i,a){this.lineHeight=i;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(a);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const s=a.find(e.typeface);if(s){this.pdfFont=selectFont(e,s);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(a))}else[this.pdfFont,this.xfaFont]=this.defaultFont(a)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,i,a){this.fontFinder=a;this.stack=[new FontInfo(e,t,i,a)]}pushData(e,t,i){const a=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||(e[t]=a.xfaFont[t]);for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=a.paraMargin[e]);const s=new FontInfo(e,t,i||a.lineHeight,this.fontFinder);s.pdfFont||(s.pdfFont=a.pdfFont);this.stack.push(s)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,i,a){this.glyphs=[];this.fontSelector=new FontSelector(e,t,i,a);this.extraHeight=0}pushData(e,t,i){this.fontSelector.pushData(e,t,i)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),i=t.xfaFont.size;if(t.pdfFont){const a=t.xfaFont.letterSpacing,s=t.pdfFont,r=s.lineHeight||1.2,n=t.lineHeight||Math.max(1.2,r)*i,g=r-(void 0===s.lineGap?.2:s.lineGap),o=Math.max(1,g)*i,c=i/1e3,C=s.defaultWidth||s.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=s.encodeString(t).join(""),i=s.charsToGlyphs(e);for(const e of i){const t=e.width||C;this.glyphs.push([t*c+a,n,o,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}else{for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([i,1.2*i,i,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}}compute(e){let t=-1,i=0,a=0,s=0,r=0,n=0,g=!1,o=!0;for(let c=0,C=this.glyphs.length;ce){a=Math.max(a,r);r=0;s+=n;n=d;t=-1;i=0;g=!0;o=!1}else{n=Math.max(d,n);i=r;r+=C;t=c}else if(r+C>e){s+=n;n=d;if(-1!==t){c=t;a=Math.max(a,i);r=0;t=-1;i=0}else{a=Math.max(a,r);r=C}g=!0;o=!1}else{r+=C;n=Math.max(d,n)}}a=Math.max(a,r);s+=n+this.extraHeight;return{width:1.02*a,height:s,isBroken:g}}}const en=/^[^.[]+/,tn=/^[^\]]+/,an=0,sn=1,rn=2,nn=3,gn=4,on=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[rr]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),In=new WeakMap;function parseExpression(e,t,i=!0){let a=e.match(en);if(!a)return null;let[s]=a;const r=[{name:s,cacheName:"."+s,index:0,js:null,formCalc:null,operator:an}];let n=s.length;for(;n0&&C.push(e)}if(0!==C.length||g||0!==o)e=isFinite(c)?C.filter((e=>ce[c])):C.flat();else{const i=t[Ir]();if(!(t=i))return null;o=-1;e=[t]}}return 0===e.length?null:e}function createDataNode(e,t,i){const a=parseExpression(i);if(!a)return null;if(a.some((e=>e.operator===sn)))return null;const s=on.get(a[0].name);let r=0;if(s){e=s(e,t);r=1}else e=t||e;for(let t=a.length;re[Pr]())).join("")}get[hn](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,hn,e._attributes)}[pr](e){let t=this;for(;t;){if(t===e)return!0;t=t[Ir]()}return!1}[Ir](){return this[wn]}[or](){return this[Ir]()}[rr](e=null){return e?this[e]:this[ln]}[js](){const e=Object.create(null);this[Os]&&(e.$content=this[Os]);for(const t of Object.getOwnPropertyNames(this)){const i=this[t];null!==i&&(i instanceof XFAObject?e[t]=i[js]():i instanceof XFAObjectArray?i.isEmpty()||(e[t]=i.dump()):e[t]=i)}return e}[Zr](){return null}[jr](){return HTMLResult.EMPTY}*[nr](){for(const e of this[rr]())yield e}*[un](e,t){for(const i of this[nr]())if(!e||t===e.has(i[kr])){const e=this[$s](),t=i[jr](e);t.success||(this[Xs].failingNode=i);yield t}}[Vs](){return null}[Ls](e,t){this[Xs].children.push(e)}[$s](){}[Js]({filter:e=null,include:t=!0}){if(this[Xs].generator){const e=this[$s](),t=this[Xs].failingNode[jr](e);if(!t.success)return t;t.html&&this[Ls](t.html,t.bbox);delete this[Xs].failingNode}else this[Xs].generator=this[un](e,t);for(;;){const e=this[Xs].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Ls](t.html,t.bbox)}this[Xs].generator=null;return HTMLResult.EMPTY}[Tr](e){this[bn]=new Set(Object.keys(e))}[fn](e){const t=this[hn],i=this[bn];return[...e].filter((e=>t.has(e)&&!i.has(e)))}[Yr](e,t=new Set){for(const i of this[ln])i[Dn](e,t)}[Dn](e,t){const i=this[dn](e,t);i?this[cn](i,e,t):this[Yr](e,t)}[dn](e,t){const{use:i,usehref:a}=this;if(!i&&!a)return null;let s=null,r=null,n=null,g=i;if(a){g=a;a.startsWith("#som(")&&a.endsWith(")")?r=a.slice(5,-1):a.startsWith(".#som(")&&a.endsWith(")")?r=a.slice(6,-1):a.startsWith("#")?n=a.slice(1):a.startsWith(".#")&&(n=a.slice(2))}else i.startsWith("#")?n=i.slice(1):r=i;this.use=this.usehref="";if(n)s=e.get(n);else{s=searchNode(e.get(Jr),this,r,!0,!1);s&&(s=s[0])}if(!s){warn(`XFA - Invalid prototype reference: ${g}.`);return null}if(s[kr]!==this[kr]){warn(`XFA - Incompatible prototype: ${s[kr]} !== ${this[kr]}.`);return null}if(t.has(s)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(s);const o=s[dn](e,t);o&&s[cn](o,e,t);s[Yr](e,t);t.delete(s);return s}[cn](e,t,i){if(i.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[Os]&&e[Os]&&(this[Os]=e[Os]);new Set(i).add(e);for(const t of this[fn](e[bn])){this[t]=e[t];this[bn]&&this[bn].add(t)}for(const a of Object.getOwnPropertyNames(this)){if(this[hn].has(a))continue;const s=this[a],r=e[a];if(s instanceof XFAObjectArray){for(const e of s[ln])e[Dn](t,i);for(let a=s[ln].length,n=r[ln].length;aXFAObject[Bn](e))):"object"==typeof e&&null!==e?Object.assign({},e):e}[Ts](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[Vr]=`${e[kr]}${Sn++}`;e[ln]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[hn].has(t)){e[t]=XFAObject[Bn](this[t]);continue}const i=this[t];e[t]=i instanceof XFAObjectArray?new XFAObjectArray(i[mn]):null}for(const t of this[ln]){const i=t[kr],a=t[Ts]();e[ln].push(a);a[wn]=e;null===e[i]?e[i]=a:e[i][ln].push(a)}return e}[rr](e=null){return e?this[ln].filter((t=>t[kr]===e)):this[ln]}[Ar](e){return this[e]}[er](e,t,i=!0){return Array.from(this[tr](e,t,i))}*[tr](e,t,i=!0){if("parent"!==e){for(const i of this[ln]){i[kr]===e&&(yield i);i.name===e&&(yield i);(t||i[Dr]())&&(yield*i[tr](e,t,!1))}i&&this[hn].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[wn]}}class XFAObjectArray{constructor(e=1/0){this[mn]=e;this[ln]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[ln].length<=this[mn]){this[ln].push(e);return!0}warn(`XFA - node "${e[kr]}" accepts no more than ${this[mn]} children`);return!1}isEmpty(){return 0===this[ln].length}dump(){return 1===this[ln].length?this[ln][0][js]():this[ln].map((e=>e[js]()))}[Ts](){const e=new XFAObjectArray(this[mn]);e[ln]=this[ln].map((e=>e[Ts]()));return e}get children(){return this[ln]}clear(){this[ln].length=0}}class XFAAttribute{constructor(e,t,i){this[wn]=e;this[kr]=t;this[Os]=i;this[qs]=!1;this[Vr]="attribute"+Sn++}[Ir](){return this[wn]}[fr](){return!0}[ir](){return this[Os].trim()}[qr](e){e=e.value||"";this[Os]=e.toString()}[Pr](){return this[Os]}[pr](e){return this[wn]===e||this[wn][pr](e)}}class XmlObject extends XFAObject{constructor(e,t,i={}){super(e,t);this[Os]="";this[Qn]=null;if("#text"!==t){const e=new Map;this[Cn]=e;for(const[t,a]of Object.entries(i))e.set(t,new XFAAttribute(this,t,a));if(i.hasOwnProperty(Rr)){const e=i[Rr].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Qn]=!1:"dataValue"===e&&(this[Qn]=!0))}}this[qs]=!1}[Xr](e){const t=this[kr];if("#text"===t){e.push(encodeToXmlString(this[Os]));return}const i=utf8StringToString(t),a=this[Sr]===kn?"xfa:":"";e.push(`<${a}${i}`);for(const[t,i]of this[Cn].entries()){const a=utf8StringToString(t);e.push(` ${a}="${encodeToXmlString(i[Os])}"`)}null!==this[Qn]&&(this[Qn]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[Os]||0!==this[ln].length){e.push(">");if(this[Os])"string"==typeof this[Os]?e.push(encodeToXmlString(this[Os])):this[Os][Xr](e);else for(const t of this[ln])t[Xr](e);e.push(``)}else e.push("/>")}[Nr](e){if(this[Os]){const e=new XmlObject(this[Sr],"#text");this[Hs](e);e[Os]=this[Os];this[Os]=""}this[Hs](e);return!0}[Mr](e){this[Os]+=e}[Zs](){if(this[Os]&&this[ln].length>0){const e=new XmlObject(this[Sr],"#text");this[Hs](e);e[Os]=this[Os];delete this[Os]}}[jr](){return"#text"===this[kr]?HTMLResult.success({name:"#text",value:this[Os]}):HTMLResult.EMPTY}[rr](e=null){return e?this[ln].filter((t=>t[kr]===e)):this[ln]}[_s](){return this[Cn]}[Ar](e){const t=this[Cn].get(e);return void 0!==t?t:this[rr](e)}*[tr](e,t){const i=this[Cn].get(e);i&&(yield i);for(const i of this[ln]){i[kr]===e&&(yield i);t&&(yield*i[tr](e,t))}}*[zs](e,t){const i=this[Cn].get(e);!i||t&&i[qs]||(yield i);for(const i of this[ln])yield*i[zs](e,t)}*[sr](e,t,i){for(const a of this[ln]){a[kr]!==e||i&&a[qs]||(yield a);t&&(yield*a[sr](e,t,i))}}[fr](){return null===this[Qn]?0===this[ln].length||this[ln][0][Sr]===_r.xhtml.id:this[Qn]}[ir](){return null===this[Qn]?0===this[ln].length?this[Os].trim():this[ln][0][Sr]===_r.xhtml.id?this[ln][0][Pr]().trim():null:this[Os].trim()}[qr](e){e=e.value||"";this[Os]=e.toString()}[js](e=!1){const t=Object.create(null);e&&(t.$ns=this[Sr]);this[Os]&&(t.$content=this[Os]);t.$name=this[kr];t.children=[];for(const i of this[ln])t.children.push(i[js](e));t.attributes=Object.create(null);for(const[e,i]of this[Cn])t.attributes[e]=i[Os];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[Os]=""}[Mr](e){this[Os]+=e}[Zs](){}}class OptionObject extends ContentObject{constructor(e,t,i){super(e,t);this[yn]=i}[Zs](){this[Os]=getKeyword({data:this[Os],defaultValue:this[yn][0],validate:e=>this[yn].includes(e)})}[Ys](e){super[Ys](e);delete this[yn]}}class StringObject extends ContentObject{[Zs](){this[Os]=this[Os].trim()}}class IntegerObject extends ContentObject{constructor(e,t,i,a){super(e,t);this[En]=i;this[Fn]=a}[Zs](){this[Os]=getInteger({data:this[Os],defaultValue:this[En],validate:this[Fn]})}[Ys](e){super[Ys](e);delete this[En];delete this[Fn]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,(e=>1===e))}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,(e=>0===e))}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const Rn={anchorType(e,t){const i=e[or]();if(i&&(!i.layout||"position"===i.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const i=e[or]();let a=e.w;const s=e.h;if(i.layout?.includes("row")){const t=i[Xs],s=e.colSpan;let r;if(-1===s){r=t.columnWidths.slice(t.currentColumn).reduce(((e,t)=>e+t),0);t.currentColumn=0}else{r=t.columnWidths.slice(t.currentColumn,t.currentColumn+s).reduce(((e,t)=>e+t),0);t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(r)||(a=e.w=r)}t.width=""!==a?measureToString(a):"auto";t.height=""!==s?measureToString(s):"auto"},position(e,t){const i=e[or]();if(!i?.layout||"position"===i.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[kr])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[Zr]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[or]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,i,a,s,r){const n=new TextMeasure(t,i,a,s);"string"==typeof e?n.addString(e):e[Ur](n);return n.compute(r)}function layoutNode(e,t){let i=null,a=null,s=!1;if((!e.w||!e.h)&&e.value){let r=0,n=0;if(e.margin){r=e.margin.leftInset+e.margin.rightInset;n=e.margin.topInset+e.margin.bottomInset}let g=null,o=null;if(e.para){o=Object.create(null);g=""===e.para.lineHeight?null:e.para.lineHeight;o.top=""===e.para.spaceAbove?0:e.para.spaceAbove;o.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;o.left=""===e.para.marginLeft?0:e.para.marginLeft;o.right=""===e.para.marginRight?0:e.para.marginRight}let c=e.font;if(!c){const t=e[cr]();let i=e[Ir]();for(;i&&i!==t;){if(i.font){c=i.font;break}i=i[Ir]()}}const C=(e.w||t.width)-r,h=e[Cr].fontFinder;if(e.value.exData&&e.value.exData[Os]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[Os],c,o,g,h,C);a=t.width;i=t.height;s=t.isBroken}else{const t=e.value[Pr]();if(t){const e=layoutText(t,c,o,g,h,C);a=e.width;i=e.height;s=e.isBroken}}null===a||e.w||(a+=r);null===i||e.h||(i+=n)}return{w:a,h:i,isBroken:s}}function computeBbox(e,t,i){let a;if(""!==e.w&&""!==e.h)a=[e.x,e.y,e.w,e.h];else{if(!i)return null;let s=e.w;if(""===s){if(0===e.maxW){const t=e[or]();s="position"===t.layout&&""!==t.w?0:e.minW}else s=Math.min(e.maxW,i.width);t.attributes.style.width=measureToString(s)}let r=e.h;if(""===r){if(0===e.maxH){const t=e[or]();r="position"===t.layout&&""!==t.h?0:e.minH}else r=Math.min(e.maxH,i.height);t.attributes.style.height=measureToString(r)}a=[e.x,e.y,s,r]}return a}function fixDimensions(e){const t=e[or]();if(t.layout?.includes("row")){const i=t[Xs],a=e.colSpan;let s;s=-1===a?i.columnWidths.slice(i.currentColumn).reduce(((e,t)=>e+t),0):i.columnWidths.slice(i.currentColumn,i.currentColumn+a).reduce(((e,t)=>e+t),0);isNaN(s)||(e.w=s)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=e.columnWidths.reduce(((e,t)=>e+t),0))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const i=Object.create(null);for(const a of t){const t=e[a];if(null!==t)if(Rn.hasOwnProperty(a))Rn[a](e,i);else if(t instanceof XFAObject){const e=t[Zr]();e?Object.assign(i,e):warn(`(DEBUG) - XFA - style for ${a} not implemented yet`)}}return i}function createWrapper(e,t){const{attributes:i}=t,{style:a}=i,s={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};i.class.push("xfaWrapped");if(e.border){const{widths:i,insets:r}=e.border[Xs];let n,g,o=r[0],c=r[3];const C=r[0]+r[2],h=r[1]+r[3];switch(e.border.hand){case"even":o-=i[0]/2;c-=i[3]/2;n=`calc(100% + ${(i[1]+i[3])/2-h}px)`;g=`calc(100% + ${(i[0]+i[2])/2-C}px)`;break;case"left":o-=i[0];c-=i[3];n=`calc(100% + ${i[1]+i[3]-h}px)`;g=`calc(100% + ${i[0]+i[2]-C}px)`;break;case"right":n=h?`calc(100% - ${h}px)`:"100%";g=C?`calc(100% - ${C}px)`:"100%"}const l=["xfaBorder"];isPrintOnly(e.border)&&l.push("xfaPrintOnly");const Q={name:"div",attributes:{class:l,style:{top:`${o}px`,left:`${c}px`,width:n,height:g}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==a[e]){Q.attributes.style[e]=a[e];delete a[e]}s.children.push(Q,t)}else s.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==a[e]){s.attributes.style[e]=a[e];delete a[e]}s.attributes.style.position="absolute"===a.position?"absolute":"relative";delete a.position;if(a.alignSelf){s.attributes.style.alignSelf=a.alignSelf;delete a.alignSelf}return s}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const i="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),a=getMeasurement(e[i],"0px");e[i]=a-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[cr]()[Xs].paraStack;return t.length?t.at(-1):null}function setPara(e,t,i){if(i.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const a=getCurrentPara(e);if(a){const e=i.attributes.style;e.display="flex";e.flexDirection="column";switch(a.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=a[Zr]();for(const[i,a]of Object.entries(t))i in e||(e[i]=a)}}}function setFontFamily(e,t,i,a){if(!i){delete a.fontFamily;return}const s=stripQuotes(e.typeface);a.fontFamily=`"${s}"`;const r=i.find(s);if(r){const{fontFamily:i}=r.regular.cssFontInfo;i!==s&&(a.fontFamily=`"${i}"`);const n=getCurrentPara(t);if(n&&""!==n.lineHeight)return;if(a.lineHeight)return;const g=selectFont(e,r);g&&(a.lineHeight=Math.max(1.2,g.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[Xs])return null;const t={name:"div",attributes:e[Xs].attributes,children:e[Xs].children};if(e[Xs].failingNode){const i=e[Xs].failingNode[Vs]();i&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[i])):t.children.push(i))}return 0===t.children.length?null:t}function addHTML(e,t,i){const a=e[Xs],s=a.availableSpace,[r,n,g,o]=i;switch(e.layout){case"position":a.width=Math.max(a.width,r+g);a.height=Math.max(a.height,n+o);a.children.push(t);break;case"lr-tb":case"rl-tb":if(!a.line||1===a.attempt){a.line=createLine(e,[]);a.children.push(a.line);a.numberInLine=0}a.numberInLine+=1;a.line.children.push(t);if(0===a.attempt){a.currentWidth+=g;a.height=Math.max(a.height,a.prevHeight+o)}else{a.currentWidth=g;a.prevHeight=a.height;a.height+=o;a.attempt=0}a.width=Math.max(a.width,a.currentWidth);break;case"rl-row":case"row":{a.children.push(t);a.width+=g;a.height=Math.max(a.height,o);const e=measureToString(a.height);for(const t of a.children)t.attributes.style.height=e;break}case"table":case"tb":a.width=Math.min(s.width,Math.max(a.width,g));a.height+=o;a.children.push(t)}}function getAvailableSpace(e){const t=e[Xs].availableSpace,i=e.margin?e.margin.topInset+e.margin.bottomInset:0,a=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[Xs].attempt?{width:t.width-a-e[Xs].currentWidth,height:t.height-i-e[Xs].prevHeight}:{width:t.width-a,height:t.height-i-e[Xs].height};case"rl-row":case"row":return{width:e[Xs].columnWidths.slice(e[Xs].currentColumn).reduce(((e,t)=>e+t)),height:t.height-a};case"table":case"tb":return{width:t.width-a,height:t.height-i-e[Xs].height};default:return t}}function checkDimensions(e,t){if(null===e[cr]()[Xs].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const i=e[or](),a=i[Xs]?.attempt||0,[,s,r,n]=function getTransformedBBox(e){let t,i,a=""===e.w?NaN:e.w,s=""===e.h?NaN:e.h,[r,n]=[0,0];switch(e.anchorType||""){case"bottomCenter":[r,n]=[a/2,s];break;case"bottomLeft":[r,n]=[0,s];break;case"bottomRight":[r,n]=[a,s];break;case"middleCenter":[r,n]=[a/2,s/2];break;case"middleLeft":[r,n]=[0,s/2];break;case"middleRight":[r,n]=[a,s/2];break;case"topCenter":[r,n]=[a/2,0];break;case"topRight":[r,n]=[a,0]}switch(e.rotate||0){case 0:[t,i]=[-r,-n];break;case 90:[t,i]=[-n,r];[a,s]=[s,-a];break;case 180:[t,i]=[r,n];[a,s]=[-a,-s];break;case 270:[t,i]=[n,-r];[a,s]=[-s,a]}return[e.x+t+Math.min(0,a),e.y+i+Math.min(0,s),Math.abs(a),Math.abs(s)]}(e);switch(i.layout){case"lr-tb":case"rl-tb":return 0===a?e[cr]()[Xs].noLayoutFailure?""!==e.w?Math.round(r-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(n-t.height)>2)&&(""!==e.w?Math.round(r-t.width)<=2||0===i[Xs].numberInLine&&t.height>2:t.width>2):!!e[cr]()[Xs].noLayoutFailure||!(""!==e.h&&Math.round(n-t.height)>2)&&((""===e.w||Math.round(r-t.width)<=2||!i[wr]())&&t.height>2);case"table":case"tb":return!!e[cr]()[Xs].noLayoutFailure||(""===e.h||e[yr]()?(""===e.w||Math.round(r-t.width)<=2||!i[wr]())&&t.height>2:Math.round(n-t.height)<=2);case"position":if(e[cr]()[Xs].noLayoutFailure)return!0;if(""===e.h||Math.round(n+s-t.height)<=2)return!0;return n+s>e[cr]()[Xs].currentContentArea.h;case"rl-row":case"row":return!!e[cr]()[Xs].noLayoutFailure||(""===e.h||Math.round(n-t.height)<=2);default:return!0}}const Nn=_r.template.id,Gn="http://www.w3.org/2000/svg",Mn=/^H(\d+)$/,Un=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),xn=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[ar]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Hs](t);e.value=t}e.value[qr](t)}function*getContainedChildren(e){for(const t of e[rr]())t instanceof SubformSet?yield*t[nr]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[Or]=e[Ir]()[Or];return}if(e[Or])return;let t=null;for(const i of e.traversal[rr]())if("next"===i.operation){t=i;break}if(!t||!t.ref){e[Or]=e[Ir]()[Or];return}const i=e[cr]();e[Or]=++i[Or];const a=i[vr](t.ref,e);if(!a)return;e=a[0]}}function applyAssist(e,t){const i=e.assist;if(i){const e=i[jr]();e&&(t.title=e);const a=i.role.match(Mn);if(a){const e="heading",i=a[1];t.role=e;t["aria-level"]=i}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const i=e[Ir]();"row"===i.layout&&(t.role="TH"===i.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[Os]?t.speak[Os]:t.toolTip?t.toolTip[Os]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[cr]();if(null===t[Xs].firstUnsplittable){t[Xs].firstUnsplittable=e;t[Xs].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[cr]();t[Xs].firstUnsplittable===e&&(t[Xs].noLayoutFailure=!1)}function handleBreak(e){if(e[Xs])return!1;e[Xs]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[cr]();let i=null;if(e.target){i=t[vr](e.target,e[Ir]());if(!i)return!1;i=i[0]}const{currentPageArea:a,currentContentArea:s}=t[Xs];if("pageArea"===e.targetType){i instanceof PageArea||(i=null);if(e.startNew){e[Xs].target=i||a;return!0}if(i&&i!==a){e[Xs].target=i;return!0}return!1}i instanceof ContentArea||(i=null);const r=i&&i[Ir]();let n,g=r;if(e.startNew)if(i){const e=r.contentArea.children,t=e.indexOf(s),a=e.indexOf(i);-1!==t&&te;a[Xs].noLayoutFailure=!0;const n=t[jr](i);e[Ls](n.html,n.bbox);a[Xs].noLayoutFailure=s;t[or]=r}class AppearanceFilter extends StringObject{constructor(e){super(Nn,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(Nn,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[jr](){const e=this.edge||new Edge({}),t=e[Zr](),i=Object.create(null);"visible"===this.fill?.presence?Object.assign(i,this.fill[Zr]()):i.fill="transparent";i.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);i.stroke=t.color;let a;const s={xmlns:Gn,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)a={name:"ellipse",attributes:{xmlns:Gn,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:i}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,r=this.sweepAngle>180?1:0,[n,g,o,c]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];a={name:"path",attributes:{xmlns:Gn,d:`M ${n} ${g} A 50 50 0 ${r} 0 ${o} ${c}`,vectorEffect:"non-scaling-stroke",style:i}};Object.assign(s,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const r={name:"svg",children:[a],attributes:s};if(hasMargin(this[Ir]()[Ir]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[r]});r.attributes.style.position="absolute";return HTMLResult.success(r)}}class Area extends XFAObject{constructor(e){super(Nn,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[nr](){yield*getContainedChildren(this)}[Dr](){return!0}[dr](){return!0}[Ls](e,t){const[i,a,s,r]=t;this[Xs].width=Math.max(this[Xs].width,i+s);this[Xs].height=Math.max(this[Xs].height,a+r);this[Xs].children.push(e)}[$s](){return this[Xs].availableSpace}[jr](e){const t=toStyle(this,"position"),i={style:t,id:this[Vr],class:["xfaArea"]};isPrintOnly(this)&&i.class.push("xfaPrintOnly");this.name&&(i.xfaName=this.name);const a=[];this[Xs]={children:a,width:0,height:0,availableSpace:e};const s=this[Js]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!s.success){if(s.isBreak())return s;delete this[Xs];return HTMLResult.FAILURE}t.width=measureToString(this[Xs].width);t.height=measureToString(this[Xs].height);const r={name:"div",attributes:i,children:a},n=[this.x,this.y,this[Xs].width,this[Xs].height];delete this[Xs];return HTMLResult.success(r,n)}}class Assist extends XFAObject{constructor(e){super(Nn,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[jr](){return this.toolTip?.[Os]||null}}class Barcode extends XFAObject{constructor(e){super(Nn,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(Nn,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(Nn,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(Nn,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(Nn,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[jr](e){return valueToHtml(1===this[Os]?"1":"0")}}class Border extends XFAObject{constructor(e){super(Nn,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[ar](){if(!this[Xs]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let i=e.length;i<4;i++)e.push(t)}const t=e.map((e=>e.thickness)),i=[0,0,0,0];if(this.margin){i[0]=this.margin.topInset;i[1]=this.margin.rightInset;i[2]=this.margin.bottomInset;i[3]=this.margin.leftInset}this[Xs]={widths:t,insets:i,edges:e}}return this[Xs]}[Zr](){const{edges:e}=this[ar](),t=e.map((e=>{const t=e[Zr]();t.color||="#000000";return t})),i=Object.create(null);this.margin&&Object.assign(i,this.margin[Zr]());"visible"===this.fill?.presence&&Object.assign(i,this.fill[Zr]());if(this.corner.children.some((e=>0!==e.radius))){const e=this.corner.children.map((e=>e[Zr]()));if(2===e.length||3===e.length){const t=e.at(-1);for(let i=e.length;i<4;i++)e.push(t)}i.borderRadius=e.map((e=>e.radius)).join(" ")}switch(this.presence){case"invisible":case"hidden":i.borderStyle="";break;case"inactive":i.borderStyle="none";break;default:i.borderStyle=t.map((e=>e.style)).join(" ")}i.borderWidth=t.map((e=>e.width)).join(" ");i.borderColor=t.map((e=>e.color)).join(" ");return i}}class Break extends XFAObject{constructor(e){super(Nn,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(Nn,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(Nn,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[jr](e){this[Xs]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(Nn,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[jr](e){const t=this[Ir]()[Ir](),i={name:"button",attributes:{id:this[Vr],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[Os]);if(!t)continue;const a=fixURL(t.url);a&&i.children.push({name:"a",attributes:{id:"link"+this[Vr],href:a,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(i)}}class Calculate extends XFAObject{constructor(e){super(Nn,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(Nn,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[qr](e){_setValue(this,e)}[ar](e){if(!this[Xs]){let{width:t,height:i}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":i=this.reserve<=0?i:this.reserve}this[Xs]=layoutNode(this,{width:t,height:i})}return this[Xs]}[jr](e){if(!this.value)return HTMLResult.EMPTY;this[Lr]();const t=this.value[jr](e).html;if(!t){this[xr]();return HTMLResult.EMPTY}const i=this.reserve;if(this.reserve<=0){const{w:t,h:i}=this[ar](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=i}}const a=[];"string"==typeof t?a.push({name:"#text",value:t}):a.push(t);const s=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(s.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(s.height=measureToString(this.reserve))}setPara(this,null,t);this[xr]();this.reserve=i;return HTMLResult.success({name:"div",attributes:{style:s,class:["xfaCaption"]},children:a})}}class Certificate extends StringObject{constructor(e){super(Nn,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(Nn,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(Nn,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle("margin"),i=measureToString(this.size);t.width=t.height=i;let a,s,r;const n=this[Ir]()[Ir](),g=n.items.children.length&&n.items.children[0][jr]().html||[],o={on:(void 0!==g[0]?g[0]:"on").toString(),off:(void 0!==g[1]?g[1]:"off").toString()},c=(n.value?.[Pr]()||"off")===o.on||void 0,C=n[or](),h=n[Vr];let l;if(C instanceof ExclGroup){r=C[Vr];a="radio";s="xfaRadio";l=C[Ws]?.[Vr]||C[Vr]}else{a="checkbox";s="xfaCheckbox";l=n[Ws]?.[Vr]||n[Vr]}const Q={name:"input",attributes:{class:[s],style:t,fieldId:h,dataId:l,type:a,checked:c,xfaOn:o.on,xfaOff:o.off,"aria-label":ariaLabel(n),"aria-required":!1}};r&&(Q.attributes.name=r);if(isRequired(n)){Q.attributes["aria-required"]=!0;Q.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[Q]})}}class ChoiceList extends XFAObject{constructor(e){super(Nn,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","margin"),i=this[Ir]()[Ir](),a={fontSize:`calc(${i.font?.size||10}px * var(--scale-factor))`},s=[];if(i.items.children.length>0){const e=i.items;let t=0,r=0;if(2===e.children.length){t=e.children[0].save;r=1-t}const n=e.children[t][jr]().html,g=e.children[r][jr]().html;let o=!1;const c=i.value?.[Pr]()||"";for(let e=0,t=n.length;eMath.min(Math.max(0,parseInt(e.trim(),10)),255))).map((e=>isNaN(e)?0:e));if(r.length<3)return{r:i,g:a,b:s};[i,a,s]=r;return{r:i,g:a,b:s}}(e.value):"";this.extras=null}[hr](){return!1}[Zr](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(Nn,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(Nn,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(Nn,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[jr](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},i=["xfaContentarea"];isPrintOnly(this)&&i.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:i,id:this[Vr]}})}}class Corner extends XFAObject{constructor(e){super(Nn,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(Nn,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class DateTime extends ContentObject{constructor(e){super(Nn,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(Nn,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin"),i=this[Ir]()[Ir](),a={name:"input",attributes:{type:"text",fieldId:i[Vr],dataId:i[Ws]?.[Vr]||i[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(i),"aria-required":!1}};if(isRequired(i)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Decimal extends ContentObject{constructor(e){super(Nn,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseFloat(this[Os].trim());this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(Nn,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(Nn,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(Nn,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(Nn,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(Nn,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[qr](e){_setValue(this,e)}[jr](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Lr]();const t=this.w,i=this.h,{w:a,h:s,isBroken:r}=layoutNode(this,e);if(a&&""===this.w){if(r&&this[or]()[wr]()){this[xr]();return HTMLResult.FAILURE}this.w=a}s&&""===this.h&&(this.h=s);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=i;this[xr]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const n=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,n);if(n.margin){n.padding=n.margin;delete n.margin}const g=["xfaDraw"];this.font&&g.push("xfaFont");isPrintOnly(this)&&g.push("xfaPrintOnly");const o={style:n,id:this[Vr],class:g};this.name&&(o.xfaName=this.name);const c={name:"div",attributes:o,children:[]};applyAssist(this,o);const C=computeBbox(this,c,e),h=this.value?this.value[jr](e).html:null;if(null===h){this.w=t;this.h=i;this[xr]();return HTMLResult.success(createWrapper(this,c),C)}c.children.push(h);setPara(this,n,h);this.w=t;this.h=i;this[xr]();return HTMLResult.success(createWrapper(this,c),C)}}class Edge extends XFAObject{constructor(e){super(Nn,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[Zr]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(Nn,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(Nn,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(Nn,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(Nn,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(Nn,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(Nn,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(Nn,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(Nn,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(Nn,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[ur](){return"text/html"===this.contentType}[Nr](e){if("text/html"===this.contentType&&e[Sr]===_r.xhtml.id){this[Os]=e;return!0}if("text/xml"===this.contentType){this[Os]=e;return!0}return!1}[jr](e){return"text/html"===this.contentType&&this[Os]?this[Os][jr](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(Nn,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(Nn,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[dr](){return!0}[hr](){return!0}[qr](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Hs](e);t.value=e}t.value[qr](e)}}[wr](){return this.layout.endsWith("-tb")&&0===this[Xs].attempt&&this[Xs].numberInLine>0||this[Ir]()[wr]()}[yr](){const e=this[or]();if(!e[yr]())return!1;if(void 0!==this[Xs]._isSplittable)return this[Xs]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[Xs]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[Xs].numberInLine)return!1;this[Xs]._isSplittable=!0;return!0}[Vs](){return flushHTML(this)}[Ls](e,t){addHTML(this,e,t)}[$s](){return getAvailableSpace(this)}[jr](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],i={id:this[Vr],class:[]};setAccess(this,i.class);this[Xs]||(this[Xs]=Object.create(null));Object.assign(this[Xs],{children:t,attributes:i,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const a=this[yr]();a||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["field"]);if(this.layout.includes("row")){const e=this[or]().columnWidths;if(Array.isArray(e)&&e.length>0){this[Xs].columnWidths=e;this[Xs].currentColumn=0}}const r=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),n=["xfaExclgroup"],g=layoutClass(this);g&&n.push(g);isPrintOnly(this)&&n.push("xfaPrintOnly");i.style=r;i.class=n;this.name&&(i.xfaName=this.name);this[Lr]();const o="lr-tb"===this.layout||"rl-tb"===this.layout,c=o?2:1;for(;this[Xs].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[dr](){return!0}[qr](e){_setValue(this,e)}[jr](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[Cr]=this[Cr];this[Hs](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Hs](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[Xs];this[Lr]();const t=this.caption?this.caption[jr](e).html:null,i=this.w,a=this.h;let s=0,r=0;if(this.margin){s=this.margin.leftInset+this.margin.rightInset;r=this.margin.topInset+this.margin.bottomInset}let n=null;if(""===this.w||""===this.h){let t=null,i=null,a=0,g=0;if(this.ui.checkButton)a=g=this.ui.checkButton.size;else{const{w:t,h:i}=layoutNode(this,e);if(null!==t){a=t;g=i}else g=function fonts_getMetrics(e,t=!1){let i=null;if(e){const t=stripQuotes(e.typeface),a=e[Cr].fontFinder.find(t);i=selectFont(e,a)}if(!i)return{lineHeight:12,lineGap:2,lineNoGap:10};const a=e.size||10,s=i.lineHeight?Math.max(t?0:1.2,i.lineHeight):1.2,r=void 0===i.lineGap?.2:i.lineGap;return{lineHeight:s*a,lineGap:r*a,lineNoGap:Math.max(1,s-r)*a}}(this.font,!0).lineNoGap}n=getBorderDims(this.ui[ar]());a+=n.w;g+=n.h;if(this.caption){const{w:s,h:r,isBroken:n}=this.caption[ar](e);if(n&&this[or]()[wr]()){this[xr]();return HTMLResult.FAILURE}t=s;i=r;switch(this.caption.placement){case"left":case"right":case"inline":t+=a;break;case"top":case"bottom":i+=g}}else{t=a;i=g}if(t&&""===this.w){t+=s;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(Nn,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseFloat(this[Os].trim());this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class template_Font extends XFAObject{constructor(e){super(Nn,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[Ys](e){super[Ys](e);this[Cr].usedTypefaces.add(this.typeface)}[Zr](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[Cr].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(Nn,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(Nn,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(Nn,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(Nn,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[jr](){if(this.contentType&&!Un.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[Cr].images&&this[Cr].images.get(this.href);if(!e&&(this.href||!this[Os]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=function fromBase64Util(e){return Uint8Array.fromBase64?Uint8Array.fromBase64(e):stringToBytes(atob(e))}(this[Os]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,i]of xn)if(e.length>t.length&&t.every(((t,i)=>t===e[i]))){this.contentType=i;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let i;switch(this.aspect){case"fit":case"actual":break;case"height":i={height:"100%",objectFit:"fill"};break;case"none":i={width:"100%",height:"100%",objectFit:"fill"};break;case"width":i={width:"100%",objectFit:"fill"}}const a=this[Ir]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:i,src:URL.createObjectURL(t),alt:a?ariaLabel(a[Ir]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(Nn,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[jr](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(Nn,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=parseInt(this[Os].trim(),10);this[Os]=isNaN(e)?null:e}[jr](e){return valueToHtml(null!==this[Os]?this[Os].toString():"")}}class Issuers extends XFAObject{constructor(e){super(Nn,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(Nn,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[jr](){const e=[];for(const t of this[rr]())e.push(t[Pr]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(Nn,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(Nn,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(Nn,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[jr](){const e=this[Ir]()[Ir](),t=this.edge||new Edge({}),i=t[Zr](),a=Object.create(null),s="visible"===t.presence?t.thickness:0;a.strokeWidth=measureToString(s);a.stroke=i.color;let r,n,g,o,c="100%",C="100%";if(e.w<=s){[r,n,g,o]=["50%",0,"50%","100%"];c=a.strokeWidth}else if(e.h<=s){[r,n,g,o]=[0,"50%","100%","50%"];C=a.strokeWidth}else"\\"===this.slope?[r,n,g,o]=[0,0,"100%","100%"]:[r,n,g,o]=[0,"100%","100%",0];const h={name:"svg",children:[{name:"line",attributes:{xmlns:Gn,x1:r,y1:n,x2:g,y2:o,style:a}}],attributes:{xmlns:Gn,width:c,height:C,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[h]});h.attributes.style.position="absolute";return HTMLResult.success(h)}}class Linear extends XFAObject{constructor(e){super(Nn,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[Zr]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(Nn,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){this[Os]=getStringOption(this[Os],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(Nn,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(Nn,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Zr](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(Nn,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(Nn,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const i=e.trim().split(/\s*,\s*/).map((e=>getMeasurement(e,"-1")));if(i.length<4||i[2]<0||i[3]<0)return{x:t,y:t,width:t,height:t};const[a,s,r,n]=i;return{x:a,y:s,width:r,height:n}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(Nn,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(Nn,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin"),i=this[Ir]()[Ir](),a={name:"input",attributes:{type:"text",fieldId:i[Vr],dataId:i[Ws]?.[Vr]||i[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(i),"aria-required":!1}};if(isRequired(i)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Occur extends XFAObject{constructor(e){super(Nn,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Ys](){const e=this[Ir](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[br](){if(!this[Xs]){this[Xs]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[Xs].numberOfUsee.oddOrEven===t&&e.pagePosition===i));if(a)return a;a=this.pageArea.children.find((e=>"any"===e.oddOrEven&&e.pagePosition===i));if(a)return a;a=this.pageArea.children.find((e=>"any"===e.oddOrEven&&"any"===e.pagePosition));return a||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(Nn,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map(((e,t)=>t%2==1?getMeasurement(e):e));this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[Zr](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[Zr]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(Nn,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(Nn,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";const t=this.color?this.color[Zr]():"#000000",i="repeating-linear-gradient",a=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${i}(to top,${a}) ${i}(to right,${a})`;case"crossDiagonal":return`${i}(45deg,${a}) ${i}(-45deg,${a})`;case"diagonalLeft":return`${i}(45deg,${a})`;case"diagonalRight":return`${i}(-45deg,${a})`;case"horizontal":return`${i}(to top,${a})`;case"vertical":return`${i}(to right,${a})`}return""}}class Picture extends StringObject{constructor(e){super(Nn,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(Nn,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(Nn,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){e=e?e[Zr]():"#FFFFFF";const t=this.color?this.color[Zr]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(Nn,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(Nn,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(Nn,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[jr](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[Zr](),i=Object.create(null);"visible"===this.fill?.presence?Object.assign(i,this.fill[Zr]()):i.fill="transparent";i.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);i.stroke=t.color;const a=(this.corner.children.length?this.corner.children[0]:new Corner({}))[Zr](),s={name:"svg",children:[{name:"rect",attributes:{xmlns:Gn,width:"100%",height:"100%",x:0,y:0,rx:a.radius,ry:a.radius,style:i}}],attributes:{xmlns:Gn,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[Ir]()[Ir]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[s]});s.attributes.style.position="absolute";return HTMLResult.success(s)}}class RefElement extends StringObject{constructor(e){super(Nn,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(Nn,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(Nn,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(Nn,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(Nn,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(Nn,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(Nn,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Zr](e){return e?e[Zr]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(Nn,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(Nn,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[Zr](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(Nn,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map((e=>"-1"===e?-1:getMeasurement(e)));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[or](){const e=this[Ir]();return e instanceof SubformSet?e[or]():e}[dr](){return!0}[wr](){return this.layout.endsWith("-tb")&&0===this[Xs].attempt&&this[Xs].numberInLine>0||this[Ir]()[wr]()}*[nr](){yield*getContainedChildren(this)}[Vs](){return flushHTML(this)}[Ls](e,t){addHTML(this,e,t)}[$s](){return getAvailableSpace(this)}[yr](){const e=this[or]();if(!e[yr]())return!1;if(void 0!==this[Xs]._isSplittable)return this[Xs]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[Xs]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[Xs]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[Xs].numberInLine)return!1;this[Xs]._isSplittable=!0;return!0}[jr](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[Cr]=this[Cr];this[Hs](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[Cr]=this[Cr];this[Hs](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[Cr]=this[Cr];this[Hs](e);this.overflow.push(e)}this[Hr](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[Xs]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],i={id:this[Vr],class:[]};setAccess(this,i.class);this[Xs]||(this[Xs]=Object.create(null));Object.assign(this[Xs],{children:t,line:null,attributes:i,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const a=this[cr](),s=a[Xs].noLayoutFailure,r=this[yr]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const n=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[or]().columnWidths;if(Array.isArray(e)&&e.length>0){this[Xs].columnWidths=e;this[Xs].currentColumn=0}}const g=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),o=["xfaSubform"],c=layoutClass(this);c&&o.push(c);i.style=g;i.class=o;this.name&&(i.xfaName=this.name);if(this.overflow){const t=this.overflow[ar]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Lr]();const C="lr-tb"===this.layout||"rl-tb"===this.layout,h=C?2:1;for(;this[Xs].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[Xs].afterBreakAfter=p;return HTMLResult.breakNode(e)}}delete this[Xs];return p}}class SubformSet extends XFAObject{constructor(e){super(Nn,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[nr](){yield*getContainedChildren(this)}[or](){let e=this[Ir]();for(;!(e instanceof Subform);)e=e[Ir]();return e}[dr](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(Nn,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){this[Os]=new Map(this[Os].split(this.delimiter).map((e=>{(e=e.split("=",2))[0]=e[0].trim();return e})))}}class SubjectDNs extends XFAObject{constructor(e){super(Nn,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(Nn,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(Nn,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[Zs](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[Or]=5e3}[yr](){return!0}[vr](e,t){return e.startsWith("#")?[this[lr].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[Wr](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[Xs]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[vs]();const t=e.pageSet.pageArea.children,i={name:"div",children:[]};let a=null,s=null,r=null;if(e.breakBefore.children.length>=1){s=e.breakBefore.children[0];r=s.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){s=e.subform.children[0].breakBefore.children[0];r=s.target}else if(e.break?.beforeTarget){s=e.break;r=s.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){s=e.subform.children[0].break;r=s.beforeTarget}if(s){const e=this[vr](r,s[Ir]());if(e instanceof PageArea){a=e;s[Xs]={}}}a||(a=t[0]);a[Xs]={numberOfUse:1};const n=a[Ir]();n[Xs]={numberOfUse:1,pageIndex:n.pageArea.children.indexOf(a),pageSetIndex:0};let g,o=null,c=null,C=!0,h=0,l=0;for(;;){if(C)h=0;else{i.children.pop();if(3==++h){warn("XFA - Something goes wrong: please file a bug.");return i}}g=null;this[Xs].currentPageArea=a;const t=a[jr]().html;i.children.push(t);if(o){this[Xs].noLayoutFailure=!0;t.children.push(o[jr](a[Xs].space).html);o=null}if(c){this[Xs].noLayoutFailure=!0;t.children.push(c[jr](a[Xs].space).html);c=null}const s=a.contentArea.children,r=t.children.filter((e=>e.attributes.class.includes("xfaContentarea")));C=!1;this[Xs].firstUnsplittable=null;this[Xs].noLayoutFailure=!1;const flush=t=>{const i=e[Vs]();if(i){C||=i.children?.length>0;r[t].children.push(i)}};for(let t=l,a=s.length;t0;r[t].children.push(h.html)}else!C&&i.children.length>1&&i.children.pop();return i}if(h.isBreak()){const e=h.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){o=this[vr](e.leader,e[Ir]());o=o?o[0]:null}if(e.trailer){c=this[vr](e.trailer,e[Ir]());c=c?c[0]:null}if("pageArea"===e.targetType){g=e[Xs].target;t=1/0}else if(e[Xs].target){g=e[Xs].target;l=e[Xs].index+1;t=1/0}else t=e[Xs].index}else if(this[Xs].overflowNode){const e=this[Xs].overflowNode;this[Xs].overflowNode=null;const i=e[ar](),a=i.target;i.addLeader=null!==i.leader;i.addTrailer=null!==i.trailer;flush(t);const r=t;t=1/0;if(a instanceof PageArea)g=a;else if(a instanceof ContentArea){const e=s.indexOf(a);if(-1!==e)e>r?t=e-1:l=e;else{g=a[Ir]();l=g.contentArea.children.indexOf(a)}}}else flush(t)}this[Xs].pageNumber+=1;g&&(g[br]()?g[Xs].numberOfUse+=1:g=null);a=g||a[gr]();yield null}}}class Text extends ContentObject{constructor(e){super(Nn,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[xs](){return!0}[Nr](e){if(e[Sr]===_r.xhtml.id){this[Os]=e;return!0}warn(`XFA - Invalid content in Text: ${e[kr]}.`);return!1}[Mr](e){this[Os]instanceof XFAObject||super[Mr](e)}[Zs](){"string"==typeof this[Os]&&(this[Os]=this[Os].replaceAll("\r\n","\n"))}[ar](){return"string"==typeof this[Os]?this[Os].split(/[\u2029\u2028\n]/).reduce(((e,t)=>{t&&e.push(t);return e}),[]).join("\n"):this[Os][Pr]()}[jr](e){if("string"==typeof this[Os]){const e=valueToHtml(this[Os]).html;if(this[Os].includes("\u2029")){e.name="div";e.children=[];this[Os].split("\u2029").map((e=>e.split(/[\u2028\n]/).reduce(((e,t)=>{e.push({name:"span",value:t},{name:"br"});return e}),[]))).forEach((t=>{e.children.push({name:"p",children:t})}))}else if(/[\u2028\n]/.test(this[Os])){e.name="div";e.children=[];this[Os].split(/[\u2028\n]/).forEach((t=>{e.children.push({name:"span",value:t},{name:"br"})}))}return HTMLResult.success(e)}return this[Os][jr](e)}}class TextEdit extends XFAObject{constructor(e){super(Nn,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[jr](e){const t=toStyle(this,"border","font","margin");let i;const a=this[Ir]()[Ir]();""===this.multiLine&&(this.multiLine=a instanceof Draw?1:0);i=1===this.multiLine?{name:"textarea",attributes:{dataId:a[Ws]?.[Vr]||a[Vr],fieldId:a[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:a[Ws]?.[Vr]||a[Vr],fieldId:a[Vr],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){i.attributes["aria-required"]=!0;i.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[i]})}}class Time extends StringObject{constructor(e){super(Nn,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[Zs](){const e=this[Os].trim();this[Os]=e?new Date(e):null}[jr](e){return valueToHtml(this[Os]?this[Os].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(Nn,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(Nn,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(Nn,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(Nn,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[Dr](){return!1}}class Ui extends XFAObject{constructor(e){super(Nn,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[ar](){if(void 0===this[Xs]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[Xs]=t;return t}}this[Xs]=null}return this[Xs]}[jr](e){const t=this[ar]();return t?t[jr](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(Nn,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(Nn,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[qr](e){const t=this[Ir]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Hs](this.image)}this.image[Os]=e[Os];return}const i=e[kr];if(null===this[i]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Hr](t)}}this[e[kr]]=e;this[Hs](e)}else this[i][Os]=e[Os]}[Pr](){if(this.exData)return"string"==typeof this.exData[Os]?this.exData[Os].trim():this.exData[Os][Pr]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[Os]||"").toString().trim()}return null}[jr](e){for(const t of Object.getOwnPropertyNames(this)){const i=this[t];if(i instanceof XFAObject)return i[jr](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(Nn,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[Dr](){return!0}}class TemplateNamespace{static[zr](e,t){if(TemplateNamespace.hasOwnProperty(e)){const i=TemplateNamespace[e](t);i[Tr](t);return i}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Ln=_r.datasets.id;function createText(e){const t=new Text({});t[Os]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(_r.datasets.id,"data");this.emptyMerge=0===this.data[rr]().length;this.root.form=this.form=e.template[Ts]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,i){e[Ws]=t;if(e[hr]())if(t[fr]()){const i=t[ir]();e[qr](createText(i))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const i=t[rr]().map((e=>e[Os].trim())).join("\n");e[qr](createText(i))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[fr]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,i,a){if(!e)return null;let s,r;for(let a=0;a<3;a++){s=i[sr](e,!1,!0);for(;;){r=s.next().value;if(!r)break;if(t===r[fr]())return r}if(i[Sr]===_r.datasets.id&&"data"===i[kr])break;i=i[Ir]()}if(!a)return null;s=this.data[sr](e,!0,!1);r=s.next().value;if(r)return r;s=this.data[zs](e,!0);r=s.next().value;return r?.[fr]()?r:null}_setProperties(e,t){if(e.hasOwnProperty("setProperty"))for(const{ref:i,target:a,connection:s}of e.setProperty.children){if(s)continue;if(!i)continue;const r=searchNode(this.root,t,i,!1,!1);if(!r){warn(`XFA - Invalid reference: ${i}.`);continue}const[n]=r;if(!n[pr](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const g=searchNode(this.root,e,a,!1,!1);if(!g){warn(`XFA - Invalid target: ${a}.`);continue}const[o]=g;if(!o[pr](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const c=o[Ir]();if(o instanceof SetProperty||c instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(o instanceof BindItems||c instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const C=n[Pr](),h=o[kr];if(o instanceof XFAAttribute){const e=Object.create(null);e[h]=C;const t=Reflect.construct(Object.getPrototypeOf(c).constructor,[e]);c[h]=t[h]}else if(o.hasOwnProperty(Os)){o[Ws]=n;o[Os]=C;o[Zs]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!e.hasOwnProperty("items")||!e.hasOwnProperty("bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Hr](t);e.items.clear();const i=new Items({}),a=new Items({});e[Hs](i);e.items.push(i);e[Hs](a);e.items.push(a);for(const{ref:s,labelRef:r,valueRef:n,connection:g}of e.bindItems.children){if(g)continue;if(!s)continue;const e=searchNode(this.root,t,s,!1,!1);if(e)for(const t of e){if(!t[pr](this.datasets)){warn(`XFA - Invalid ref (${s}): must be a datasets child.`);continue}const e=searchNode(this.root,t,r,!0,!1);if(!e){warn(`XFA - Invalid label: ${r}.`);continue}const[g]=e;if(!g[pr](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const o=searchNode(this.root,t,n,!0,!1);if(!o){warn(`XFA - Invalid value: ${n}.`);continue}const[c]=o;if(!c[pr](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const C=createText(g[Pr]()),h=createText(c[Pr]());i[Hs](C);i.text.push(C);a[Hs](h);a.text.push(h)}else warn(`XFA - Invalid reference: ${s}.`)}}_bindOccurrences(e,t,i){let a;if(t.length>1){a=e[Ts]();a[Hr](a.occur);a.occur=null}this._bindValue(e,t[0],i);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const s=e[Ir](),r=e[kr],n=s[Qr](e);for(let e=1,g=t.length;et.name===e.name)).length:i[a].children.length;const r=i[Qr](e)+1,n=t.initial-s;if(n){const t=e[Ts]();t[Hr](t.occur);t.occur=null;i[a].push(t);i[Er](r,t);for(let e=1;e0)this._bindOccurrences(a,[e[0]],null);else if(this.emptyMerge){const e=t[Sr]===Ln?-1:t[Sr],i=a[Ws]=new XmlObject(e,a.name||"root");t[Hs](i);this._bindElement(a,i)}continue}if(!a[dr]())continue;let e=!1,s=null,r=null,n=null;if(a.bind){switch(a.bind.match){case"none":this._setAndBind(a,t);continue;case"global":e=!0;break;case"dataRef":if(!a.bind.ref){warn(`XFA - ref is empty in node ${a[kr]}.`);this._setAndBind(a,t);continue}r=a.bind.ref}a.bind.picture&&(s=a.bind.picture[Os])}const[g,o]=this._getOccurInfo(a);if(r){n=searchNode(this.root,t,r,!0,!1);if(null===n){n=createDataNode(this.data,t,r);if(!n)continue;this._isConsumeData()&&(n[qs]=!0);this._setAndBind(a,n);continue}this._isConsumeData()&&(n=n.filter((e=>!e[qs])));n.length>o?n=n.slice(0,o):0===n.length&&(n=null);n&&this._isConsumeData()&&n.forEach((e=>{e[qs]=!0}))}else{if(!a.name){this._setAndBind(a,t);continue}if(this._isConsumeData()){const i=[];for(;i.length0?i:null}else{n=t[sr](a.name,!1,this.emptyMerge).next().value;if(!n){if(0===g){i.push(a);continue}const e=t[Sr]===Ln?-1:t[Sr];n=a[Ws]=new XmlObject(e,a.name);this.emptyMerge&&(n[qs]=!0);t[Hs](n);this._setAndBind(a,n);continue}this.emptyMerge&&(n[qs]=!0);n=[n]}}n?this._bindOccurrences(a,n,s):g>0?this._setAndBind(a,t):i.push(a)}i.forEach((e=>e[Ir]()[Hr](e)))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[rr]()]];for(;t.length>0;){const i=t.at(-1),[a,s]=i;if(a+1===s.length){t.pop();continue}const r=s[++i[0]],n=e.get(r[Vr]);if(n)r[qr](n);else{const t=r[_s]();for(const i of t.values()){const t=e.get(i[Vr]);if(t){i[qr](t);break}}}const g=r[rr]();g.length>0&&t.push([-1,g])}const i=[''];if(this.dataset)for(const e of this.dataset[rr]())"data"!==e[kr]&&e[Xr](i);this.data[Xr](i);i.push("");return i.join("")}}const Hn=_r.config.id;class Acrobat extends XFAObject{constructor(e){super(Hn,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Hn,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Hn,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Hn,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Hn,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Hn,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Hn,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Hn,"adobeExtensionLevel",0,(e=>e>=1&&e<=8))}}class Agent extends XFAObject{constructor(e){super(Hn,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Hn,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Hn,"amd")}}class config_Area extends XFAObject{constructor(e){super(Hn,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Hn,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Hn,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Hn,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Hn,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Hn,"behaviorOverride")}[Zs](){this[Os]=new Map(this[Os].trim().split(/\s+/).filter((e=>e.includes(":"))).map((e=>e.split(":",2))))}}class Cache extends XFAObject{constructor(e){super(Hn,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Hn,"change")}}class Common extends XFAObject{constructor(e){super(Hn,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Hn,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Hn,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Hn,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Hn,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Hn,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Hn,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Hn,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Hn,"copies",1,(e=>e>=1))}}class Creator extends StringObject{constructor(e){super(Hn,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Hn,"currentPage",0,(e=>e>=0))}}class Data extends XFAObject{constructor(e){super(Hn,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Hn,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Hn,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Hn,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Hn,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Hn,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Hn,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Hn,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Hn,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Hn,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Hn,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Hn,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Hn,"enforce")}}class Equate extends XFAObject{constructor(e){super(Hn,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Hn,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,i=this._unicodeRange;for(let a of i.split(",").map((e=>e.trim())).filter((e=>!!e))){a=a.split("-",2).map((e=>{const i=e.match(t);return i?parseInt(i[1],16):0}));1===a.length&&a.push(a[0]);e.push(a)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Hn,"exclude")}[Zs](){this[Os]=this[Os].trim().split(/\s+/).filter((e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e)))}}class ExcludeNS extends StringObject{constructor(e){super(Hn,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Hn,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Hn,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Hn,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Hn,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Hn,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Hn,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Hn,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Hn,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Hn,"interactive")}}class Jog extends OptionObject{constructor(e){super(Hn,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Hn,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Hn,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Hn,"level",0,(e=>e>0))}}class Linearized extends Option01{constructor(e){super(Hn,"linearized")}}class Locale extends StringObject{constructor(e){super(Hn,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Hn,"localeSet")}}class Log extends XFAObject{constructor(e){super(Hn,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Hn,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Hn,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Hn,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Hn,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Hn,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Hn,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Hn,"msgId",1,(e=>e>=1))}}class NameAttr extends StringObject{constructor(e){super(Hn,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Hn,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Hn,"numberOfCopies",null,(e=>e>=2&&e<=5))}}class OpenAction extends XFAObject{constructor(e){super(Hn,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Hn,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Hn,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Hn,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Hn,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Hn,"packets")}[Zs](){"*"!==this[Os]&&(this[Os]=this[Os].trim().split(/\s+/).filter((e=>["config","datasets","template","xfdf","xslt"].includes(e))))}}class PageOffset extends XFAObject{constructor(e){super(Hn,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Hn,"pageRange")}[Zs](){const e=this[Os].trim().split(/\s+/).map((e=>parseInt(e,10))),t=[];for(let i=0,a=e.length;i!1))}}class Pcl extends XFAObject{constructor(e){super(Hn,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Hn,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Hn,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Hn,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Hn,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Hn,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Hn,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Hn,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Hn,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Hn,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Hn,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Hn,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Hn,"printerName")}}class Producer extends StringObject{constructor(e){super(Hn,"producer")}}class Ps extends XFAObject{constructor(e){super(Hn,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Hn,"range")}[Zs](){this[Os]=this[Os].trim().split(/\s*,\s*/,2).map((e=>e.split("-").map((e=>parseInt(e.trim(),10))))).filter((e=>e.every((e=>!isNaN(e))))).map((e=>{1===e.length&&e.push(e[0]);return e}))}}class Record extends ContentObject{constructor(e){super(Hn,"record")}[Zs](){this[Os]=this[Os].trim();const e=parseInt(this[Os],10);!isNaN(e)&&e>=0&&(this[Os]=e)}}class Relevant extends ContentObject{constructor(e){super(Hn,"relevant")}[Zs](){this[Os]=this[Os].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Hn,"rename")}[Zs](){this[Os]=this[Os].trim();(this[Os].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[Os]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Hn,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Hn,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Hn,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Hn,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Hn,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Hn,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Hn,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Hn,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Hn,"startPage",0,(e=>!0))}}class SubmitFormat extends OptionObject{constructor(e){super(Hn,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Hn,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Hn,"subsetBelow",100,(e=>e>=0&&e<=100))}}class SuppressBanner extends Option01{constructor(e){super(Hn,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Hn,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Hn,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Hn,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Hn,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Hn,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Hn,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Hn,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Hn,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Hn,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Hn,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Hn,"validateApprovalSignatures")}[Zs](){this[Os]=this[Os].trim().split(/\s+/).filter((e=>["docReady","postSign"].includes(e)))}}class ValidationMessaging extends OptionObject{constructor(e){super(Hn,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Hn,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Hn,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Hn,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Hn,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Hn,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Hn,"window")}[Zs](){const e=this[Os].trim().split(/\s*,\s*/,2).map((e=>parseInt(e,10)));if(e.some((e=>isNaN(e))))this[Os]=[0,0];else{1===e.length&&e.push(e[0]);this[Os]=e}}}class Xdc extends XFAObject{constructor(e){super(Hn,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Hn,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Hn,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Hn,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[zr](e,t){if(ConfigNamespace.hasOwnProperty(e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const Jn=_r.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(Jn,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(Jn,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(Jn,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(Jn,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(Jn,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(Jn,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(Jn,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(Jn,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(Jn,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(Jn,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(Jn,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(Jn,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[zr](e,t){if(ConnectionSetNamespace.hasOwnProperty(e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const Yn=_r.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(Yn,"data",e)}[mr](){return!0}}class Datasets extends XFAObject{constructor(e){super(Yn,"datasets",!0);this.data=null;this.Signature=null}[Nr](e){const t=e[kr];("data"===t&&e[Sr]===Yn||"Signature"===t&&e[Sr]===_r.signature.id)&&(this[t]=e);this[Hs](e)}}class DatasetsNamespace{static[zr](e,t){if(DatasetsNamespace.hasOwnProperty(e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const vn=_r.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(vn,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(vn,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(vn,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(vn,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(vn,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(vn,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(vn,"day")}}class DayNames extends XFAObject{constructor(e){super(vn,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(vn,"era")}}class EraNames extends XFAObject{constructor(e){super(vn,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(vn,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(vn,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(vn,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(vn,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(vn,"month")}}class MonthNames extends XFAObject{constructor(e){super(vn,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(vn,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(vn,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(vn,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(vn,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(vn,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(vn,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(vn,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(vn,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[zr](e,t){if(LocaleSetNamespace.hasOwnProperty(e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const Kn=_r.signature.id;class signature_Signature extends XFAObject{constructor(e){super(Kn,"signature",!0)}}class SignatureNamespace{static[zr](e,t){if(SignatureNamespace.hasOwnProperty(e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const Tn=_r.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(Tn,"stylesheet",!0)}}class StylesheetNamespace{static[zr](e,t){if(StylesheetNamespace.hasOwnProperty(e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const qn=_r.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(qn,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[Gr](e){const t=_r[e[kr]];return t&&e[Sr]===t.id}}class XdpNamespace{static[zr](e,t){if(XdpNamespace.hasOwnProperty(e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const On=_r.xhtml.id,Pn=Symbol(),Wn=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),jn=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,Math.min(parseInt(e)/100)).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,Math.min(parseInt(e)/100)).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),Xn=/\s+/g,Zn=/[\r\n]+/g,Vn=/\r\n?/g;function mapStyle(e,t,i){const a=Object.create(null);if(!e)return a;const s=Object.create(null);for(const[t,i]of e.split(";").map((e=>e.split(":",2)))){const e=jn.get(t);if(""===e)continue;let r=i;e&&(r="string"==typeof e?e:e(i,s));t.endsWith("scale")?a.transform=a.transform?`${a[t]} ${r}`:r:a[t.replaceAll(/-([a-zA-Z])/g,((e,t)=>t.toUpperCase()))]=r}a.fontFamily&&setFontFamily({typeface:a.fontFamily,weight:a.fontWeight||"normal",posture:a.fontStyle||"normal",size:s.fontSize||0},t,t[Cr].fontFinder,a);if(i&&a.verticalAlign&&"0px"!==a.verticalAlign&&a.fontSize){const e=.583,t=.333,i=getMeasurement(a.fontSize);a.fontSize=measureToString(i*e);a.verticalAlign=measureToString(Math.sign(getMeasurement(a.verticalAlign))*i*t)}i&&a.fontSize&&(a.fontSize=`calc(${a.fontSize} * var(--scale-factor))`);fixTextIndent(a);return a}const zn=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(On,t);this[Pn]=!1;this.style=e.style||""}[Ys](e){super[Ys](e);this.style=function checkStyle(e){return e.style?e.style.trim().split(/\s*;\s*/).filter((e=>!!e)).map((e=>e.split(/\s*:\s*/,2))).filter((([t,i])=>{"font-family"===t&&e[Cr].usedTypefaces.add(i);return Wn.has(t)})).map((e=>e.join(":"))).join(";"):""}(this)}[xs](){return!zn.has(this[kr])}[Mr](e,t=!1){if(t)this[Pn]=!0;else{e=e.replaceAll(Zn,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(Xn," "))}e&&(this[Os]+=e)}[Ur](e,t=!0){const i=Object.create(null),a={top:NaN,bottom:NaN,left:NaN,right:NaN};let s=null;for(const[e,t]of this.style.split(";").map((e=>e.split(":",2))))switch(e){case"font-family":i.typeface=stripQuotes(t);break;case"font-size":i.size=getMeasurement(t);break;case"font-weight":i.weight=t;break;case"font-style":i.posture=t;break;case"letter-spacing":i.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map((e=>getMeasurement(e)));switch(e.length){case 1:a.top=a.bottom=a.left=a.right=e[0];break;case 2:a.top=a.bottom=e[0];a.left=a.right=e[1];break;case 3:a.top=e[0];a.bottom=e[2];a.left=a.right=e[1];break;case 4:a.top=e[0];a.left=e[1];a.bottom=e[2];a.right=e[3]}break;case"margin-top":a.top=getMeasurement(t);break;case"margin-bottom":a.bottom=getMeasurement(t);break;case"margin-left":a.left=getMeasurement(t);break;case"margin-right":a.right=getMeasurement(t);break;case"line-height":s=getMeasurement(t)}e.pushData(i,a,s);if(this[Os])e.addString(this[Os]);else for(const t of this[rr]())"#text"!==t[kr]?t[Ur](e):e.addString(t[Os]);t&&e.popFont()}[jr](e){const t=[];this[Xs]={children:t};this[Js]({});if(0===t.length&&!this[Os])return HTMLResult.EMPTY;let i;i=this[Pn]?this[Os]?this[Os].replaceAll(Vn,"\n"):void 0:this[Os]||void 0;return HTMLResult.success({name:this[kr],attributes:{href:this.href,style:mapStyle(this.style,this,this[Pn])},children:t,value:i})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Ur](e){e.pushFont({weight:"bold"});super[Ur](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[jr](e){const t=super[jr](e),{html:i}=t;if(!i)return HTMLResult.EMPTY;i.name="div";i.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[Pr](){return"\n"}[Ur](e){e.addString("\n")}[jr](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[jr](e){const t=[];this[Xs]={children:t};this[Js]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[Os]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Ur](e){e.pushFont({posture:"italic"});super[Ur](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Ur](e){super[Ur](e,!1);e.addString("\n");e.addPara();e.popFont()}[Pr](){return this[Ir]()[rr]().at(-1)===this?super[Pr]():super[Pr]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[zr](e,t){if(XhtmlNamespace.hasOwnProperty(e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const _n={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[zr](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[lr]=e}[Nr](e){this.element=e;return!0}[Zs](){super[Zs]();if(this.element.template instanceof Template){this[lr].set(Jr,this.element);this.element.template[Yr](this[lr]);this.element.template[lr]=this[lr]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[Nr](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(_r).map((({id:e})=>e)));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:i,namespace:a,prefixes:s}){const r=null!==a;if(r){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(a)}s&&this._addNamespacePrefix(s);if(i.hasOwnProperty(Rr)){const e=_n.datasets,t=i[Rr];let a=null;for(const[i,s]of Object.entries(t)){if(this._getNamespaceToUse(i)===e){a={xfa:s};break}}a?i[Rr]=a:delete i[Rr]}const n=this._getNamespaceToUse(e),g=n?.[zr](t,i)||new Empty;g[mr]()&&this._nsAgnosticLevel++;(r||s||g[mr]())&&(g[Ks]={hasNamespace:r,prefixes:s,nsAgnostic:g[mr]()});return g}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[i,{check:a}]of Object.entries(_r))if(a(e)){t=_n[i];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:i}of e){const e=this._searchNamespace(i);let a=this._namespacePrefixes.get(t);if(!a){a=[];this._namespacePrefixes.set(t,a)}a.push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:i,nsAgnostic:a}=e;t&&(this._currentNamespace=this._namespaceStack.pop());i&&i.forEach((({prefix:e})=>{this._namespacePrefixes.get(e).pop()}));a&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=ys;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===ys){this._current[Zs]();return this._current.element}}onText(e){e=e.replace(this._nbsps,(e=>e.slice(1)+" "));this._richText||this._current[xs]()?this._current[Mr](e,this._richText):this._whiteRegex.test(e)||this._current[Mr](e.trim())}onCdata(e){this._current[Mr](e)}_mkAttributes(e,t){let i=null,a=null;const s=Object.create({});for(const{name:r,value:n}of e)if("xmlns"===r)i?warn(`XFA - multiple namespace definition in <${t}>`):i=n;else if(r.startsWith("xmlns:")){const e=r.substring(6);a||(a=[]);a.push({prefix:e,value:n})}else{const e=r.indexOf(":");if(-1===e)s[r]=n;else{let t=s[Rr];t||(t=s[Rr]=Object.create(null));const[i,a]=[r.slice(0,e),r.slice(e+1)];(t[i]||=Object.create(null))[a]=n}}return[i,a,s]}_getNameAndPrefix(e,t){const i=e.indexOf(":");return-1===i?[e,null]:[e.substring(i+1),t?"":e.substring(0,i)]}onBeginElement(e,t,i){const[a,s,r]=this._mkAttributes(t,e),[n,g]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),o=this._builder.build({nsPrefix:g,name:n,attributes:r,namespace:a,prefixes:s});o[Cr]=this._globalData;if(i){o[Zs]();this._current[Nr](o)&&o[Kr](this._ids);o[Ys](this._builder)}else{this._stack.push(this._current);this._current=o}}onEndElement(e){const t=this._current;if(t[ur]()&&"string"==typeof t[Os]){const e=new XFAParser;e._globalData=this._globalData;const i=e.parse(t[Os]);t[Os]=null;t[Nr](i)}t[Zs]();this._current=this._stack.pop();this._current[Nr](t)&&t[Kr](this._ids);t[Ys](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[Cr].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return this.root&&this.form}_createPagesHelper(){const e=this.form[Wr]();return new Promise(((t,i)=>{const nextIteration=()=>{try{const i=e.next();i.done?t(i.value):setTimeout(nextIteration,0)}catch(e){i(e)}};setTimeout(nextIteration,0)}))}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map((e=>{const{width:t,height:i}=e.attributes.style;return[0,0,parseInt(t),parseInt(i)]}))}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[Cr].images=e}setFonts(e){this.form[Cr].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[Cr].usedTypefaces){e=stripQuotes(e);this.form[Cr].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[Cr].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[kr])){const e=XhtmlNamespace.body({});e[Hs](t);t=e}const i=t[jr]();if(!i.success)return null;const{html:a}=i,{attributes:s}=a;if(s){s.class&&(s.class=s.class.filter((e=>!e.startsWith("xfa"))));s.dir="auto"}return{html:a,str:t[Pr]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments")]).then((([t,i,a,s,r])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:i,structTreeRoot:a,baseUrl:s,attachments:r})),(e=>{warn(`createGlobals: "${e}".`);return null}))}static async create(e,t,i,a,s,r,n){const g=s?await this._getPageIndex(e,t,i.pdfManager):null;return i.pdfManager.ensure(this,"_create",[e,t,i,a,s,r,g,n])}static _create(e,t,i,a,s=!1,r=null,n=null,g=null){const o=e.fetchIfRef(t);if(!(o instanceof Dict))return;const{acroForm:c,pdfManager:C}=i,h=t instanceof Ref?t.toString():`annot_${a.createObjId()}`;let l=o.get("Subtype");l=l instanceof Name?l.name:null;const Q={xref:e,ref:t,dict:o,subtype:l,id:h,annotationGlobals:i,collectFields:s,orphanFields:r,needAppearances:!s&&!0===c.get("NeedAppearances"),pageIndex:n,evaluatorOptions:C.evaluatorOptions,pageRef:g};switch(l){case"Link":return new LinkAnnotation(Q);case"Text":return new TextAnnotation(Q);case"Widget":let e=getInheritableProperty({dict:o,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(Q);case"Btn":return new ButtonWidgetAnnotation(Q);case"Ch":return new ChoiceWidgetAnnotation(Q);case"Sig":return new SignatureWidgetAnnotation(Q)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(Q);case"Popup":return new PopupAnnotation(Q);case"FreeText":return new FreeTextAnnotation(Q);case"Line":return new LineAnnotation(Q);case"Square":return new SquareAnnotation(Q);case"Circle":return new CircleAnnotation(Q);case"PolyLine":return new PolylineAnnotation(Q);case"Polygon":return new PolygonAnnotation(Q);case"Caret":return new CaretAnnotation(Q);case"Ink":return new InkAnnotation(Q);case"Highlight":return new HighlightAnnotation(Q);case"Underline":return new UnderlineAnnotation(Q);case"Squiggly":return new SquigglyAnnotation(Q);case"StrikeOut":return new StrikeOutAnnotation(Q);case"Stamp":return new StampAnnotation(Q);case"FileAttachment":return new FileAttachmentAnnotation(Q);default:s||warn(l?`Unimplemented annotation type "${l}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(Q)}}static async _getPageIndex(e,t,i){try{const a=await e.fetchIfRefAsync(t);if(!(a instanceof Dict))return-1;const s=a.getRaw("P");if(s instanceof Ref)try{return await i.ensureCatalog("getPageIndex",[s])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(a.has("Kids"))return-1;const r=await i.ensureDoc("numPages");for(let e=0;ee/255))}function getQuadPoints(e,t){const i=e.getArray("QuadPoints");if(!isNumberArray(i,null)||0===i.length||i.length%8>0)return null;const a=new Float32Array(i.length);for(let e=0,s=i.length;et[2]||Et[3]))return null;a.set([l,u,Q,u,l,E,Q,E],e)}return a}function getTransformMatrix(e,t,i){const[a,s,r,n]=Util.getAxialAlignedBoundingBox(t,i);if(a===r||s===n)return[1,0,0,1,e[0],e[1]];const g=(e[2]-e[0])/(r-a),o=(e[3]-e[1])/(n-s);return[g,0,0,o,e[0]-a*g,e[1]-s*o]}class Annotation{constructor(e){const{dict:t,xref:i,annotationGlobals:a,ref:s,orphanFields:r}=e,n=r?.get(s);n&&t.set("Parent",n);this.setTitle(t.get("T"));this.setContents(t.get("Contents"));this.setModificationDate(t.get("M"));this.setFlags(t.get("F"));this.setRectangle(t.getArray("Rect"));this.setColor(t.getArray("C"));this.setBorderStyle(t);this.setAppearance(t);this.setOptionalContent(t);const g=t.get("MK");this.setBorderAndBackgroundColors(g);this.setRotation(g,t);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const o=!!(this.flags&eA),c=!!(this.flags&tA);this.data={annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:e.subtype,hasOwnCanvas:!1,noRotate:!!(this.flags&$),noHTML:o&&c,isEditable:!1,structParent:-1};if(a.structTreeRoot){let i=t.get("StructParent");this.data.structParent=i=Number.isInteger(i)&&i>=0?i:-1;a.structTreeRoot.addAnnotationIdToPage(e.pageRef,i)}if(e.collectFields){const a=t.get("Kids");if(Array.isArray(a)){const e=[];for(const t of a)t instanceof Ref&&e.push(t.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(i,t,dA);this.data.fieldName=this._constructFieldName(t);this.data.pageIndex=e.pageIndex}const C=t.get("IT");C instanceof Name&&(this.data.it=C.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:i}=this;if(void 0===e){if(void 0===t)return;return t?i&~_:i&~z|_}if(e){i|=_;return t?i&~AA|z:i&~z|AA}i&=~(z|AA);return t?i&~_:i|_}_isViewable(e){return!this._hasFlag(e,V)&&!this._hasFlag(e,AA)}_isPrintable(e){return this._hasFlag(e,_)&&!this._hasFlag(e,z)&&!this._hasFlag(e,V)}mustBeViewed(e,t){const i=e?.get(this.data.id)?.noView;return void 0!==i?!i:this.viewable&&!this._hasFlag(this.flags,z)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:i}=e,a=getInheritableProperty({dict:t,key:"DA"})||i.acroForm.get("DA");this._defaultAppearance="string"==typeof a?a:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&V&&"Annotation"!==this.constructor.name&&(this.flags^=V)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const i=e[t];if(i instanceof Name)switch(i.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=i.name;continue}warn(`Ignoring invalid lineEnding: ${i}`)}}setRotation(e,t){this.rotation=0;let i=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(i)&&0!==i){i%=360;i<0&&(i+=360);i%90==0&&(this.rotation=i)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t)&&t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const i=t.get("N");if(i instanceof BaseStream){this.appearance=i;return}if(!(i instanceof Dict))return;const a=e.get("AS");if(!(a instanceof Name&&i.has(a.name)))return;const s=i.get(a.name);s instanceof BaseStream&&(this.appearance=s)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}loadResources(e,t){return t.dict.getAsync("Resources").then((t=>{if(!t)return;return new ObjectLoader(t,e,t.xref).load().then((function(){return t}))}))}async getOperatorList(e,t,a,s){const{hasOwnCanvas:r,id:n,rect:g}=this.data;let c=this.appearance;const C=!!(r&&a&o);if(C&&(g[0]===g[2]||g[1]===g[3])){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!c){if(!C)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};c=new StringStream("");c.dict=new Dict}const h=c.dict,l=await this.loadResources(["ExtGState","ColorSpace","Pattern","Shading","XObject","Font"],c),Q=lookupRect(h.getArray("BBox"),[0,0,1,1]),E=lookupMatrix(h.getArray("Matrix"),i),u=getTransformMatrix(g,Q,E),d=new OperatorList;let f;this.oc&&(f=await e.parseMarkedContentProps(this.oc,null));void 0!==f&&d.addOp(Ye,["OC",f]);d.addOp(je,[n,g,u,E,C]);await e.getOperatorList({stream:c,task:t,resources:l,operatorList:d,fallbackFontDict:this._fallbackFontDict});d.addOp(Xe,[]);void 0!==f&&d.addOp(ve,[]);this.reset();return{opList:d,separateForm:!1,separateCanvas:C}}async save(e,t,i,a){return null}get hasTextContent(){return!1}async extractTextContent(e,t,i){if(!this.appearance)return;const a=await this.loadResources(["ExtGState","Font","Properties","XObject"],this.appearance),s=[],r=[];let n=null;const g={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){n||=t.transform.slice(-2);r.push(t.str);if(t.hasEOL){s.push(r.join("").trimEnd());r.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:a,includeMarkedContent:!0,keepWhiteSpace:!0,sink:g,viewBox:i});this.reset();r.length&&s.push(r.join("").trimEnd());if(s.length>1||s[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),i=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(n,t,i);this.data.textContent=s}}_transformPoint(e,t,i){const{rect:a}=this.data;t||=[0,0,1,1];i||=[1,0,0,1,0,0];const s=getTransformMatrix(a,t,i);s[4]-=a[0];s[5]-=a[1];e=Util.applyTransform(e,s);return Util.applyTransform(e,i)}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let i=e;const a=new RefSet;e.objId&&a.put(e.objId);for(;i.has("Parent");){i=i.get("Parent");if(!(i instanceof Dict)||i.objId&&a.has(i.objId))break;i.objId&&a.put(i.objId);i.has("T")&&t.unshift(stringToPDFString(i.get("T")))}return t.join(".")}}class AnnotationBorderStyle{constructor(){this.width=1;this.rawWidth=1;this.style=lA;this.dashArray=[3];this.horizontalCornerRadius=0;this.verticalCornerRadius=0}setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const i=(t[2]-t[0])/2,a=(t[3]-t[1])/2;if(i>0&&a>0&&(e>i||e>a)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=lA;break;case"D":this.style=BA;break;case"B":this.style=QA;break;case"I":this.style=EA;break;case"U":this.style=uA}}setDashArray(e,t=!1){if(Array.isArray(e)){let i=!0,a=!0;for(const t of e){if(!(+t>=0)){i=!1;break}t>0&&(a=!1)}if(0===e.length||i&&!a){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const i=t.get("RT");this.data.replyType=i instanceof Name?i.name:Z}let i=null;if(this.data.replyType===X){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;i=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;i=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=i instanceof Ref?i.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:t,strokeColor:i,fillColor:a,blendMode:s,strokeAlpha:r,fillAlpha:n,pointsCallback:g}){let o=Number.MAX_VALUE,c=Number.MAX_VALUE,C=Number.MIN_VALUE,h=Number.MIN_VALUE;const l=["q"];t&&l.push(t);i&&l.push(`${i[0]} ${i[1]} ${i[2]} RG`);a&&l.push(`${a[0]} ${a[1]} ${a[2]} rg`);const Q=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=Q.length;e"string"==typeof e)).map((e=>stringToPDFString(e))):e instanceof Name?stringToPDFString(e.name):"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,AA)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(0===t)return i;return getRotationMatrix(t,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1])}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const i=this.data.rect[2]-this.data.rect[0],a=this.data.rect[3]-this.data.rect[1],s=0===t||180===t?`0 0 ${i} ${a} re`:`0 0 ${a} ${i} re`;let r="";this.backgroundColor&&(r=`${getPdfColor(this.backgroundColor,!0)} ${s} f `);if(this.borderColor){r+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${s} S `}return r}async getOperatorList(e,t,i,a){if(i&h&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,i,a);const s=await this._getAppearance(e,t,i,a);if(this.appearance&&null===s)return super.getOperatorList(e,t,i,a);const r=new OperatorList;if(!this._defaultAppearance||null===s)return{opList:r,separateForm:!1,separateCanvas:!1};const n=!!(this.data.hasOwnCanvas&&i&o),g=[0,0,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1]],c=getTransformMatrix(this.data.rect,g,[1,0,0,1,0,0]);let C;this.oc&&(C=await e.parseMarkedContentProps(this.oc,null));void 0!==C&&r.addOp(Ye,["OC",C]);r.addOp(je,[this.data.id,this.data.rect,c,this.getRotationMatrix(a),n]);const l=new StringStream(s);await e.getOperatorList({stream:l,task:t,resources:this._fieldResources.mergedResources,operatorList:r});r.addOp(Xe,[]);void 0!==C&&r.addOp(ve,[]);return{opList:r,separateForm:!1,separateCanvas:n}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);this.borderColor&&t.set("BC",getPdfColorArray(this.borderColor));this.backgroundColor&&t.set("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,i,a){const{dict:s,ref:r}=function getParentToUpdate(e,t,i){const a=new RefSet,s=e,r={dict:null,ref:null};for(;e instanceof Dict&&!a.has(t);){a.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return r;e=i.fetch(t)}if(e instanceof Dict&&e!==s){r.dict=e;r.ref=t}return r}(e,this.ref,i);if(s){if(!a.has(r)){const e=s.clone();e.set("V",t);a.put(r,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,s){const r=a?.get(this.data.id),n=this._buildFlags(r?.noView,r?.noPrint);let g=r?.value,o=r?.rotation;if(g===this.data.fieldValue||void 0===g){if(!this._hasValueFromXFA&&void 0===o&&void 0===n)return;g||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(g)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(g,this.data.fieldValue)&&void 0===n)return;void 0===o&&(o=this.rotation);let c=null;if(!this._needAppearances){c=await this._getAppearance(e,t,C,a);if(null===c&&void 0===n)return}let h=!1;if(c?.needAppearances){h=!0;c=null}const{xref:l}=e,Q=l.fetchIfRef(this.ref);if(!(Q instanceof Dict))return;const E=new Dict(l);for(const e of Q.getKeys())"AP"!==e&&E.set(e,Q.getRaw(e));if(void 0!==n){E.set("F",n);if(null===c&&!h){const e=Q.getRaw("AP");e&&E.set("AP",e)}}const u={path:this.data.fieldName,value:g},d=this.setValue(E,Array.isArray(g)?g.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(g),l,s);this.amendSavedDict(a,d||E);const f=this._getMKDict(o);f&&E.set("MK",f);s.put(this.ref,{data:E,xfa:u,needAppearances:h});if(null!==c){const e=l.getNewTemporaryRef(),t=new Dict(l);E.set("AP",t);t.set("N",e);const r=this._getSaveFieldResources(l),n=new StringStream(c),g=n.dict=new Dict(l);g.set("Subtype",Name.get("Form"));g.set("Resources",r);g.set("BBox",[0,0,this.data.rect[2]-this.data.rect[0],this.data.rect[3]-this.data.rect[1]]);const o=this.getRotationMatrix(a);o!==i&&g.set("Matrix",o);s.put(e,{data:n,xfa:null,needAppearances:!1})}E.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,i,a){if(this.hasFieldFlag(rA))return null;const s=a?.get(this.data.id);let r,g;if(s){r=s.formattedValue||s.value;g=s.rotation}if(void 0===g&&void 0===r&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const o=this.getBorderAndBackgroundAppearances(a);if(void 0===r){r=this.data.fieldValue;if(!r)return`/Tx BMC q ${o}Q EMC`}Array.isArray(r)&&1===r.length&&(r=r[0]);assert("string"==typeof r,"Expected `value` to be a string.");r=r.trimEnd();if(this.data.combo){const e=this.data.options.find((({exportValue:e})=>r===e));r=e?.displayValue||r}if(""===r)return`/Tx BMC q ${o}Q EMC`;void 0===g&&(g=this.rotation);let c,h=-1;if(this.data.multiLine){c=r.split(/\r\n?|\n/).map((e=>e.normalize("NFC")));h=c.length}else c=[r.replace(/\r\n?|\n/,"").normalize("NFC")];let l=this.data.rect[3]-this.data.rect[1],Q=this.data.rect[2]-this.data.rect[0];90!==g&&270!==g||([Q,l]=[l,Q]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let E,u,d,f=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const p=[];let m=!1;for(const e of c){const t=f.encodeString(e);t.length>1&&(m=!0);p.push(t.join(""))}if(m&&i&C)return{needAppearances:!0};if(m&&this._isOffscreenCanvasSupported){const i=this.data.comb?"monospace":"sans-serif",a=new FakeUnicodeFont(e.xref,i),s=a.createFontResources(c.join("")),n=s.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const t of n.getKeys())e.set(t,n.getRaw(t))}else this._fieldResources.mergedResources.set("Font",n);const g=a.fontName.name;f=await WidgetAnnotation._getFontData(e,t,{fontName:g,fontSize:0},s);for(let e=0,t=p.length;e2)return`/Tx BMC q ${o}BT `+E+` 1 0 0 1 ${numberToString(2)} ${numberToString(b)} Tm (${escapeString(p[0])}) Tj ET Q EMC`;return`/Tx BMC q ${o}BT `+E+` 1 0 0 1 0 0 Tm ${this._renderText(p[0],f,u,Q,D,{shift:0},2,b)} ET Q EMC`}static async _getFontData(e,t,i,a){const s=new OperatorList,r={font:null,clone(){return this}},{fontName:n,fontSize:g}=i;await e.handleSetFont(a,[n&&Name.get(n),g],null,s,t,r,null);return r.font}_getTextWidth(e,t){return t.charsToGlyphs(e).reduce(((e,t)=>e+t.width),0)/1e3}_computeFontSize(e,t,i,a,r){let{fontSize:n}=this.data.defaultAppearanceData,g=(n||12)*s,o=Math.round(e/g);if(!n){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===r){const r=this._getTextWidth(i,a);n=roundWithTwoDigits(Math.min(e/s,t/r));o=1}else{const c=i.split(/\r\n?|\n/),C=[];for(const e of c){const t=a.encodeString(e).join(""),i=a.charsToGlyphs(t),s=a.getCharPositions(t);C.push({line:t,glyphs:i,positions:s})}const isTooBig=i=>{let s=0;for(const r of C){s+=this._splitLine(null,a,i,t,r).length*i;if(s>e)return!0}return!1};o=Math.max(o,r);for(;;){g=e/o;n=roundWithTwoDigits(g/s);if(!isTooBig(n))break;o++}}const{fontName:c,fontColor:C}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:i}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(i,!0)}`}({fontSize:n,fontName:c,fontColor:C})}return[this._defaultAppearance,n,e/o]}_renderText(e,t,i,a,s,r,n,g){let o;if(1===s){o=(a-this._getTextWidth(e,t)*i)/2}else if(2===s){o=a-this._getTextWidth(e,t)*i-n}else o=n;const c=numberToString(o-r.shift);r.shift=o;return`${c} ${g=numberToString(g)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:i,acroFormResources:a}=this._fieldResources,s=this.data.defaultAppearanceData?.fontName;if(!s)return t||Dict.empty;for(const e of[t,i])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(s))return e}if(a instanceof Dict){const i=a.get("Font");if(i instanceof Dict&&i.has(s)){const a=new Dict(e);a.set(s,i.getRaw(s));const r=new Dict(e);r.set("Font",a);return Dict.merge({xref:e,dictArray:[r,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=z;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let i=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(i)||i<0||i>2)&&(i=null);this.data.textAlignment=i;let a=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(a)||a<0)&&(a=0);this.data.maxLen=a;this.data.multiLine=this.hasFieldFlag(sA);this.data.comb=this.hasFieldFlag(hA)&&!this.hasFieldFlag(sA)&&!this.hasFieldFlag(rA)&&!this.hasFieldFlag(IA)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(CA)}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,i,a,s,r,n,g,o,c,C){const h=s/this.data.maxLen,l=this.getBorderAndBackgroundAppearances(C),Q=[],E=t.getCharPositions(i);for(const[e,t]of E)Q.push(`(${escapeString(i.substring(e,t))}) Tj`);const u=Q.join(` ${numberToString(h)} 0 Td `);return`/Tx BMC q ${l}BT `+e+` 1 0 0 1 ${numberToString(n)} ${numberToString(g+o)} Tm ${u} ET Q EMC`}_getMultilineAppearance(e,t,i,a,s,r,n,g,o,c,C,h){const l=[],Q=s-2*g,E={shift:0};for(let e=0,r=t.length;ea){o.push(e.substring(l,i));l=i;Q=u;c=-1;h=-1}else{Q+=u;c=i;C=s;h=t}else if(Q+u>a)if(-1!==c){o.push(e.substring(l,C));l=C;t=h+1;c=-1;Q=0}else{o.push(e.substring(l,i));l=i;Q=u}else Q+=u}lt?`\\${t}`:"\\s+"));new RegExp(`^\\s*${r}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.hasFieldFlag(rA),charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;this.data.checkBox=!this.hasFieldFlag(nA)&&!this.hasFieldFlag(gA);this.data.radioButton=this.hasFieldFlag(nA)&&!this.hasFieldFlag(gA);this.data.pushButton=this.hasFieldFlag(gA);this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,s){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,s);let r=null,n=null;if(s){const e=s.get(this.data.id);r=e?e.value:null;n=e?e.rotation:null}if(null===r&&this.appearance)return super.getOperatorList(e,t,a,s);null==r&&(r=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const g=r?this.checkedAppearance:this.uncheckedAppearance;if(g){const r=this.appearance,o=lookupMatrix(g.dict.getArray("Matrix"),i);n&&g.dict.set("Matrix",this.getRotationMatrix(s));this.appearance=g;const c=super.getOperatorList(e,t,a,s);this.appearance=r;g.dict.set("Matrix",o);return c}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,i,a){this.data.checkBox?this._saveCheckbox(e,t,i,a):this.data.radioButton&&this._saveRadioButton(e,t,i,a)}async _saveCheckbox(e,t,i,a){if(!i)return;const s=i.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let n=s?.rotation,g=s?.value;if(void 0===n&&void 0===r){if(void 0===g)return;if(this.data.fieldValue===this.data.exportValue===g)return}let o=e.xref.fetchIfRef(this.ref);if(!(o instanceof Dict))return;o=o.clone();void 0===n&&(n=this.rotation);void 0===g&&(g=this.data.fieldValue===this.data.exportValue);const c={path:this.data.fieldName,value:g?this.data.exportValue:""},C=Name.get(g?this.data.exportValue:"Off");this.setValue(o,C,e.xref,a);o.set("AS",C);o.set("M",`D:${getModificationDate()}`);void 0!==r&&o.set("F",r);const h=this._getMKDict(n);h&&o.set("MK",h);a.put(this.ref,{data:o,xfa:c,needAppearances:!1})}async _saveRadioButton(e,t,i,a){if(!i)return;const s=i.get(this.data.id),r=this._buildFlags(s?.noView,s?.noPrint);let n=s?.rotation,g=s?.value;if(void 0===n&&void 0===r){if(void 0===g)return;if(this.data.fieldValue===this.data.buttonValue===g)return}let o=e.xref.fetchIfRef(this.ref);if(!(o instanceof Dict))return;o=o.clone();void 0===g&&(g=this.data.fieldValue===this.data.buttonValue);void 0===n&&(n=this.rotation);const c={path:this.data.fieldName,value:g?this.data.buttonValue:""},C=Name.get(g?this.data.buttonValue:"Off");g&&this.setValue(o,C,e.xref,a);o.set("AS",C);o.set("M",`D:${getModificationDate()}`);void 0!==r&&o.set("F",r);const h=this._getMKDict(n);h&&o.set("MK",h);a.put(this.ref,{data:o,xfa:c,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const i=this.data.rect[2]-this.data.rect[0],a=this.data.rect[3]-this.data.rect[1],s=[0,0,i,a],r=.8*Math.min(i,a);let n,g;if("check"===t){n={width:.755*r,height:.705*r};g="3"}else if("disc"===t){n={width:.791*r,height:.705*r};g="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const o=`q BT /PdfJsZaDb ${r} Tf 0 g ${numberToString((i-n.width)/2)} ${numberToString((a-n.height)/2)} Td (${g}) Tj ET Q`,c=new Dict(e.xref);c.set("FormType",1);c.set("Subtype",Name.get("Form"));c.set("Type",Name.get("XObject"));c.set("BBox",s);c.set("Matrix",[1,0,0,1,0,0]);c.set("Length",o.length);const C=new Dict(e.xref),h=new Dict(e.xref);h.set("PdfJsZaDb",this.fallbackFontDict);C.set("Font",h);c.set("Resources",C);this.checkedAppearance=new StringStream(o);this.checkedAppearance.dict=c;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const i=t.get("N");if(!(i instanceof Dict))return;const a=this._decodeFormValue(e.dict.get("AS"));"string"==typeof a&&(this.data.fieldValue=a);const s=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",r=i.getKeys();if(0===r.length)r.push("Off",s);else if(1===r.length)"Off"===r[0]?r.push(s):r.unshift("Off");else if(r.includes(s)){r.length=0;r.push("Off",s)}else{const e=r.find((e=>"Off"!==e));r.length=0;r.push("Off",e)}r.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=r[1];const n=i.get(this.data.exportValue);this.checkedAppearance=n instanceof BaseStream?n:null;const g=i.get("Off");this.uncheckedAppearance=g instanceof BaseStream?g:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const i=t.get("V");i instanceof Name&&(this.data.fieldValue=this._decodeFormValue(i))}const i=e.dict.get("AP");if(!(i instanceof Dict))return;const a=i.get("N");if(!(a instanceof Dict))return;for(const e of a.getKeys())if("Off"!==e){this.data.buttonValue=this._decodeFormValue(e);break}const s=a.get(this.data.buttonValue);this.checkedAppearance=s instanceof BaseStream?s:null;const r=a.get("Off");this.uncheckedAppearance=r instanceof BaseStream?r:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:i}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:i.baseUrl,docAttachments:i.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.set("BaseFont",Name.get("ZapfDingbats"));e.set("Type",Name.get("FallbackType"));e.set("Subtype",Name.get("FallbackType"));e.set("Encoding",Name.get("ZapfDingbatsEncoding"));return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const a=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(a))for(let e=0,t=a.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map((e=>({exportValue:e,displayValue:e}))));this.data.combo=this.hasFieldFlag(oA);this.data.multiSelect=this.hasFieldFlag(cA);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let i=e?.get(this.data.id)?.value;Array.isArray(i)||(i=[i]);const a=[],{options:s}=this.data;for(let e=0,t=0,r=s.length;ei){i=a;t=e}}[Q,E]=this._computeFontSize(e,c-4,t,l,-1)}const u=E*s,d=(u-E)/2,f=Math.floor(o/u);let p=0;if(h.length>0){const e=Math.min(...h),t=Math.max(...h);p=Math.max(0,t-f+1);p>e&&(p=e)}const m=Math.min(p+f+1,C),y=["/Tx BMC q",`1 1 ${c} ${o} re W n`];if(h.length){y.push("0.600006 0.756866 0.854904 rg");for(const e of h)p<=e&&ee.trimEnd()));const{coords:e,bbox:t,matrix:i}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,a);this.data.textPosition=this._transformPoint(e,t,i)}if(this._isOffscreenCanvasSupported){const s=e.dict.get("CA"),r=new FakeUnicodeFont(i,"sans-serif");this.appearance=r.createAppearance(this._contents.str,this.rectangle,this.rotation,a,t,s);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:i,ap:a}){const{color:s,fontSize:r,oldAnnotation:n,rect:g,rotation:o,user:c,value:C}=e,h=n||new Dict(t);h.set("Type",Name.get("Annot"));h.set("Subtype",Name.get("FreeText"));if(n){h.set("M",`D:${getModificationDate()}`);h.delete("RC")}else h.set("CreationDate",`D:${getModificationDate()}`);h.set("Rect",g);const l=`/Helv ${r} Tf ${getPdfColor(s,!0)}`;h.set("DA",l);h.set("Contents",stringToAsciiOrUTF16BE(C));h.set("F",4);h.set("Border",[0,0,0]);h.set("Rotate",o);c&&h.set("T",stringToAsciiOrUTF16BE(c));if(i||a){const e=new Dict(t);h.set("AP",e);i?e.set("N",i):e.set("N",a)}return h}static async createNewAppearanceStream(e,t,i){const{baseFontRef:a,evaluator:r,task:n}=i,{color:g,fontSize:o,rect:c,rotation:C,value:h}=e,l=new Dict(t),Q=new Dict(t);if(a)Q.set("Helv",a);else{const e=new Dict(t);e.set("BaseFont",Name.get("Helvetica"));e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type1"));e.set("Encoding",Name.get("WinAnsiEncoding"));Q.set("Helv",e)}l.set("Font",Q);const E=await WidgetAnnotation._getFontData(r,n,{fontName:"Helv",fontSize:o},l),[u,d,f,p]=c;let m=f-u,y=p-d;C%180!=0&&([m,y]=[y,m]);const w=h.split("\n"),D=o/1e3;let b=-1/0;const F=[];for(let e of w){const t=E.encodeString(e);if(t.length>1)return null;e=t.join("");F.push(e);let i=0;const a=E.charsToGlyphs(e);for(const e of a)i+=e.width*D;b=Math.max(b,i)}let S=1;b>m&&(S=m/b);let k=1;const R=s*o,N=1*o,G=R*w.length;G>y&&(k=y/G);const M=o*Math.min(S,k);let U,x,L;switch(C){case 0:L=[1,0,0,1];x=[c[0],c[1],m,y];U=[c[0],c[3]-N];break;case 90:L=[0,1,-1,0];x=[c[1],-c[2],m,y];U=[c[1],-c[0]-N];break;case 180:L=[-1,0,0,-1];x=[-c[2],-c[3],m,y];U=[-c[2],-c[1]-N];break;case 270:L=[0,-1,1,0];x=[-c[3],c[0],m,y];U=[-c[3],c[2]-N]}const H=["q",`${L.join(" ")} 0 0 cm`,`${x.join(" ")} re W n`,"BT",`${getPdfColor(g,!0)}`,`0 Tc /Helv ${numberToString(M)} Tf`];H.push(`${U.join(" ")} Td (${escapeString(F[0])}) Tj`);const J=numberToString(R);for(let e=1,t=F.length;e{e.push(`${a[0]} ${a[1]} m`,`${a[2]} ${a[3]} l`,"S");return[t[0]-o,t[2]+o,t[7]-o,t[3]+o]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=M;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),s=getRgbColor(t.getArray("IC"),null),r=s?getPdfColorArray(s):null,n=r?a:null;if(0===this.borderStyle.width&&!r)return;this._setDefaultAppearance({xref:i,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:r,strokeAlpha:a,fillAlpha:n,pointsCallback:(e,t)=>{const i=t[4]+this.borderStyle.width/2,a=t[5]+this.borderStyle.width/2,s=t[6]-t[4]-this.borderStyle.width,n=t[3]-t[7]-this.borderStyle.width;e.push(`${i} ${a} ${s} ${n} re`);r?e.push("B"):e.push("S");return[t[0],t[2],t[7],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=U;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),s=getRgbColor(t.getArray("IC"),null),r=s?getPdfColorArray(s):null,n=r?a:null;if(0===this.borderStyle.width&&!r)return;const g=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:i,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:r,strokeAlpha:a,fillAlpha:n,pointsCallback:(e,t)=>{const i=t[0]+this.borderStyle.width/2,a=t[1]-this.borderStyle.width/2,s=t[6]-this.borderStyle.width/2,n=t[7]+this.borderStyle.width/2,o=i+(s-i)/2,c=a+(n-a)/2,C=(s-i)/2*g,h=(n-a)/2*g;e.push(`${o} ${n} m`,`${o+C} ${n} ${s} ${c+h} ${s} ${c} c`,`${s} ${c-h} ${o+C} ${a} ${o} ${a} c`,`${o-C} ${a} ${i} ${c-h} ${i} ${c} c`,`${i} ${c+h} ${o-C} ${n} ${o} ${n} c`,"h");r?e.push("B"):e.push("S");return[t[0],t[2],t[7],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=L;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(t.getArray("LE"));this.data.lineEndings=this.lineEndings}const a=t.getArray("Vertices");if(!isNumberArray(a,null))return;const s=this.data.vertices=Float32Array.from(a);if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA"),r=this.borderStyle.width||1,n=2*r,g=[1/0,1/0,-1/0,-1/0];for(let e=0,t=s.length;e{for(let t=0,i=s.length;t{for(const t of this.data.inkLists){for(let i=0,a=t.length;ie/255)));Q.set("CA",n);const u=new Dict(t);Q.set("AP",u);i?u.set("N",i):u.set("N",a);return Q}static async createNewAppearanceStream(e,t,i){if(e.outlines)return this.createNewAppearanceStreamForHighlight(e,t,i);const{color:a,rect:s,paths:r,thickness:n,opacity:g}=e,o=[`${n} w 1 J 1 j`,`${getPdfColor(a,!1)}`];1!==g&&o.push("/R0 gs");for(const e of r.lines){o.push(`${numberToString(e[4])} ${numberToString(e[5])} m`);for(let t=6,i=e.length;t{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}static createNewDict(e,t,{apRef:i,ap:a}){const{color:s,oldAnnotation:r,opacity:n,rect:g,rotation:o,user:c,quadPoints:C}=e,h=r||new Dict(t);h.set("Type",Name.get("Annot"));h.set("Subtype",Name.get("Highlight"));h.set(r?"M":"CreationDate",`D:${getModificationDate()}`);h.set("CreationDate",`D:${getModificationDate()}`);h.set("Rect",g);h.set("F",4);h.set("Border",[0,0,0]);h.set("Rotate",o);h.set("QuadPoints",C);h.set("C",Array.from(s,(e=>e/255)));h.set("CA",n);c&&h.set("T",stringToAsciiOrUTF16BE(c));if(i||a){const e=new Dict(t);h.set("AP",e);e.set("N",i||a)}return h}static async createNewAppearanceStream(e,t,i){const{color:a,rect:s,outlines:r,opacity:n}=e,g=[`${getPdfColor(a,!0)}`,"/R0 gs"],o=[];for(const e of r){o.length=0;o.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,i=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:i}=e;this.data.annotationType=Y;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],a=t.get("CA");this._setDefaultAppearance({xref:i,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:a,pointsCallback:(e,t)=>{const i=(t[1]-t[5])/6;let a=i,s=t[4];const r=t[5],n=t[6];e.push(`${s} ${r+a} m`);do{s+=2;a=0===a?i:0;e.push(`${s} ${r+a} l`)}while(s{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[2],t[7],t[3]]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{#T;constructor(e){super(e);this.data.annotationType=K;this.#T=this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!1;this.#T=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}this.data.hasOwnCanvas=this.#T;return!t?.has(this.data.id)}static async createImage(e,t){const{width:i,height:a}=e,s=new OffscreenCanvas(i,a),r=s.getContext("2d",{alpha:!0});r.drawImage(e,0,0);const n=r.getImageData(0,0,i,a).data,g=new Uint32Array(n.buffer),o=g.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(o){r.fillStyle="white";r.fillRect(0,0,i,a);r.drawImage(e,0,0)}const c=s.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),C=Name.get("XObject"),h=Name.get("Image"),l=new Dict(t);l.set("Type",C);l.set("Subtype",h);l.set("BitsPerComponent",8);l.set("ColorSpace",Name.get("DeviceRGB"));l.set("Filter",Name.get("DCTDecode"));l.set("BBox",[0,0,i,a]);l.set("Width",i);l.set("Height",a);let Q=null;if(o){const e=new Uint8Array(g.length);if(FeatureTest.isLittleEndian)for(let t=0,i=g.length;t>>24;else for(let t=0,i=g.length;t=0&&r<=1?r:null}}class DecryptStream extends DecodeStream{constructor(e,t,i){super(t);this.str=e;this.dict=e.dict;this.decrypt=i;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.str.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.str.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const i=this.bufferLength,a=i+e.length;this.ensureBuffer(a).set(e,i);this.bufferLength=a}}class ARCFourCipher{constructor(e){this.a=0;this.b=0;const t=new Uint8Array(256),i=e.length;for(let e=0;e<256;++e)t[e]=e;for(let a=0,s=0;a<256;++a){const r=t[a];s=s+r+e[a%i]&255;t[a]=t[s];t[s]=r}this.s=t}encryptBlock(e){let t=this.a,i=this.b;const a=this.s,s=e.length,r=new Uint8Array(s);for(let n=0;n>5&255;C[h++]=s>>13&255;C[h++]=s>>21&255;C[h++]=s>>>29&255;C[h++]=0;C[h++]=0;C[h++]=0;const E=new Int32Array(16);for(h=0;h>>32-g)|0;s=r}r=r+s|0;n=n+c|0;g=g+Q|0;o=o+u|0}return new Uint8Array([255&r,r>>8&255,r>>16&255,r>>>24&255,255&n,n>>8&255,n>>16&255,n>>>24&255,255&g,g>>8&255,g>>16&255,g>>>24&255,255&o,o>>8&255,o>>16&255,o>>>24&255])}}();class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}or(e){this.high|=e.high;this.low|=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}shiftLeft(e){if(e>=32){this.high=this.low<>>32-e;this.low<<=e}}rotateRight(e){let t,i;if(32&e){i=this.low;t=this.high}else{t=this.low;i=this.high}e&=31;this.low=t>>>e|i<<32-e;this.high=i>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let i=(this.high>>>0)+(e.high>>>0);t>4294967295&&(i+=1);this.low=0|t;this.high=0|i}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const Ag=function calculateSHA256Closure(){function rotr(e,t){return e>>>t|e<<32-t}function ch(e,t,i){return e&t^~e&i}function maj(e,t,i){return e&t^e&i^t&i}function sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}const e=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];return function hash(t,i,a){let s=1779033703,r=3144134277,n=1013904242,g=2773480762,o=1359893119,c=2600822924,C=528734635,h=1541459225;const l=64*Math.ceil((a+9)/64),Q=new Uint8Array(l);let E,u;for(E=0;E>>29&255;Q[E++]=a>>21&255;Q[E++]=a>>13&255;Q[E++]=a>>5&255;Q[E++]=a<<3&255;const f=new Uint32Array(64);for(E=0;E>>10)+f[u-7]+littleSigma(f[u-15])+f[u-16]|0;let t,i,a=s,l=r,d=n,m=g,y=o,w=c,D=C,b=h;for(u=0;u<64;++u){t=b+sigmaPrime(y)+ch(y,w,D)+e[u]+f[u];i=sigma(a)+maj(a,l,d);b=D;D=w;w=y;y=m+t|0;m=d;d=l;l=a;a=t+i|0}s=s+a|0;r=r+l|0;n=n+d|0;g=g+m|0;o=o+y|0;c=c+w|0;C=C+D|0;h=h+b|0}var p;return new Uint8Array([s>>24&255,s>>16&255,s>>8&255,255&s,r>>24&255,r>>16&255,r>>8&255,255&r,n>>24&255,n>>16&255,n>>8&255,255&n,g>>24&255,g>>16&255,g>>8&255,255&g,o>>24&255,o>>16&255,o>>8&255,255&o,c>>24&255,c>>16&255,c>>8&255,255&c,C>>24&255,C>>16&255,C>>8&255,255&C,h>>24&255,h>>16&255,h>>8&255,255&h])}}(),eg=function calculateSHA512Closure(){function ch(e,t,i,a,s){e.assign(t);e.and(i);s.assign(t);s.not();s.and(a);e.xor(s)}function maj(e,t,i,a,s){e.assign(t);e.and(i);s.assign(t);s.and(a);e.xor(s);s.assign(i);s.and(a);e.xor(s)}function sigma(e,t,i){e.assign(t);e.rotateRight(28);i.assign(t);i.rotateRight(34);e.xor(i);i.assign(t);i.rotateRight(39);e.xor(i)}function sigmaPrime(e,t,i){e.assign(t);e.rotateRight(14);i.assign(t);i.rotateRight(18);e.xor(i);i.assign(t);i.rotateRight(41);e.xor(i)}function littleSigma(e,t,i){e.assign(t);e.rotateRight(1);i.assign(t);i.rotateRight(8);e.xor(i);i.assign(t);i.shiftRight(7);e.xor(i)}function littleSigmaPrime(e,t,i){e.assign(t);e.rotateRight(19);i.assign(t);i.rotateRight(61);e.xor(i);i.assign(t);i.shiftRight(6);e.xor(i)}const e=[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)];return function hash(t,i,a,s=!1){let r,n,g,o,c,C,h,l;if(s){r=new Word64(3418070365,3238371032);n=new Word64(1654270250,914150663);g=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);c=new Word64(1731405415,4290775857);C=new Word64(2394180231,1750603025);h=new Word64(3675008525,1694076839);l=new Word64(1203062813,3204075428)}else{r=new Word64(1779033703,4089235720);n=new Word64(3144134277,2227873595);g=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);c=new Word64(1359893119,2917565137);C=new Word64(2600822924,725511199);h=new Word64(528734635,4215389547);l=new Word64(1541459225,327033209)}const Q=128*Math.ceil((a+17)/128),E=new Uint8Array(Q);let u,d;for(u=0;u>>29&255;E[u++]=a>>21&255;E[u++]=a>>13&255;E[u++]=a>>5&255;E[u++]=a<<3&255;const p=new Array(80);for(u=0;u<80;u++)p[u]=new Word64(0,0);let m=new Word64(0,0),y=new Word64(0,0),w=new Word64(0,0),D=new Word64(0,0),b=new Word64(0,0),F=new Word64(0,0),S=new Word64(0,0),k=new Word64(0,0);const R=new Word64(0,0),N=new Word64(0,0),G=new Word64(0,0),M=new Word64(0,0);let U,x;for(u=0;u=1;--e){i=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=i;i=r[14];a=r[10];r[14]=r[6];r[10]=r[2];r[6]=i;r[2]=a;i=r[15];a=r[11];s=r[7];r[15]=r[3];r[11]=i;r[7]=a;r[3]=s;for(let e=0;e<16;++e)r[e]=this._inv_s[r[e]];for(let i=0,a=16*e;i<16;++i,++a)r[i]^=t[a];for(let e=0;e<16;e+=4){const t=this._mix[r[e]],a=this._mix[r[e+1]],s=this._mix[r[e+2]],n=this._mix[r[e+3]];i=t^a>>>8^a<<24^s>>>16^s<<16^n>>>24^n<<8;r[e]=i>>>24&255;r[e+1]=i>>16&255;r[e+2]=i>>8&255;r[e+3]=255&i}}i=r[13];r[13]=r[9];r[9]=r[5];r[5]=r[1];r[1]=i;i=r[14];a=r[10];r[14]=r[6];r[10]=r[2];r[6]=i;r[2]=a;i=r[15];a=r[11];s=r[7];r[15]=r[3];r[11]=i;r[7]=a;r[3]=s;for(let e=0;e<16;++e){r[e]=this._inv_s[r[e]];r[e]^=t[e]}return r}_encrypt(e,t){const i=this._s;let a,s,r;const n=new Uint8Array(16);n.set(e);for(let e=0;e<16;++e)n[e]^=t[e];for(let e=1;e=a;--i)if(e[i]!==t){t=0;break}g-=t;r[r.length-1]=e.subarray(0,16-t)}}const o=new Uint8Array(g);for(let e=0,t=0,i=r.length;e=256&&(g=255&(27^g))}for(let t=0;t<4;++t){i[e]=a^=i[e-32];e++;i[e]=s^=i[e-32];e++;i[e]=r^=i[e-32];e++;i[e]=n^=i[e-32];e++}}return i}}class PDF17{checkOwnerPassword(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);return isArrayEqual(Ag(s,0,s.length),a)}checkUserPassword(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);return isArrayEqual(Ag(a,0,a.length),i)}getOwnerKey(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);const r=Ag(s,0,s.length);return new AES256Cipher(r).decryptBlock(a,!1,new Uint8Array(16))}getUserKey(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);const s=Ag(a,0,a.length);return new AES256Cipher(s).decryptBlock(i,!1,new Uint8Array(16))}}class PDF20{_hash(e,t,i){let a=Ag(t,0,t.length).subarray(0,32),s=[0],r=0;for(;r<64||s.at(-1)>r-32;){const t=e.length+a.length+i.length,c=new Uint8Array(t);let C=0;c.set(e,C);C+=e.length;c.set(a,C);C+=a.length;c.set(i,C);const h=new Uint8Array(64*t);for(let e=0,i=0;e<64;e++,i+=t)h.set(c,i);s=new AES128Cipher(a.subarray(0,16)).encrypt(h,a.subarray(16,32));const l=s.slice(0,16).reduce(((e,t)=>e+t),0)%3;0===l?a=Ag(s,0,s.length):1===l?a=(n=s,g=0,o=s.length,eg(n,g,o,!0)):2===l&&(a=eg(s,0,s.length));r++}var n,g,o;return a.subarray(0,32)}checkOwnerPassword(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);return isArrayEqual(this._hash(e,s,i),a)}checkUserPassword(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);return isArrayEqual(this._hash(e,a,[]),i)}getOwnerKey(e,t,i,a){const s=new Uint8Array(e.length+56);s.set(e,0);s.set(t,e.length);s.set(i,e.length+t.length);const r=this._hash(e,s,i);return new AES256Cipher(r).decryptBlock(a,!1,new Uint8Array(16))}getUserKey(e,t,i){const a=new Uint8Array(e.length+8);a.set(e,0);a.set(t,e.length);const s=this._hash(e,a,[]);return new AES256Cipher(s).decryptBlock(i,!1,new Uint8Array(16))}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const i=new this.StreamCipherConstructor;return new DecryptStream(e,t,(function cipherTransformDecryptStream(e,t){return i.decryptBlock(e,t)}))}decryptString(e){const t=new this.StringCipherConstructor;let i=stringToBytes(e);i=t.decryptBlock(i,!0);return bytesToString(i)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const i=16-e.length%16;e+=String.fromCharCode(i).repeat(i);const a=new Uint8Array(16);if("undefined"!=typeof crypto)crypto.getRandomValues(a);else for(let e=0;e<16;e++)a[e]=Math.floor(256*Math.random());let s=stringToBytes(e);s=t.encrypt(s,a);const r=new Uint8Array(16+s.length);r.set(a);r.set(s,16);return bytesToString(r)}let i=stringToBytes(e);i=t.encrypt(i);return bytesToString(i)}}class CipherTransformFactory{static#q=new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]);#O(e,t,i,a,s,r,n,g,o,c,C,h){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const l=6===e?new PDF20:new PDF17;return l.checkUserPassword(t,g,n)?l.getUserKey(t,o,C):t.length&&l.checkOwnerPassword(t,a,r,i)?l.getOwnerKey(t,s,r,c):null}#P(e,t,i,a,s,r,n,g){const o=40+i.length+e.length,c=new Uint8Array(o);let C,h,l=0;if(t){h=Math.min(32,t.length);for(;l>8&255;c[l++]=s>>16&255;c[l++]=s>>>24&255;for(C=0,h=e.length;C=4&&!g){c[l++]=255;c[l++]=255;c[l++]=255;c[l++]=255}let Q=$n(c,0,l);const E=n>>3;if(r>=3)for(C=0;C<50;++C)Q=$n(Q,0,E);const u=Q.subarray(0,E);let d,f;if(r>=3){for(l=0;l<32;++l)c[l]=CipherTransformFactory.#q[l];for(C=0,h=e.length;C>3;if(i>=3)for(g=0;g<50;++g)o=$n(o,0,o.length);let C,h;if(i>=3){h=t;const e=new Uint8Array(c);for(g=19;g>=0;g--){for(let t=0;t>8&255;s[n++]=e>>16&255;s[n++]=255&t;s[n++]=t>>8&255;if(a){s[n++]=115;s[n++]=65;s[n++]=108;s[n++]=84}return $n(s,0,n).subarray(0,Math.min(i.length+5,16))}#X(e,t,i,a,s){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const r=this,n=e.get(t.name),g=n?.get("CFM");if(!g||"None"===g.name)return function(){return new NullCipher};if("V2"===g.name)return function(){return new ARCFourCipher(r.#j(i,a,s,!1))};if("AESV2"===g.name)return function(){return new AES128Cipher(r.#j(i,a,s,!0))};if("AESV3"===g.name)return function(){return new AES256Cipher(s)};throw new FormatError("Unknown crypto method")}constructor(e,t,i){const a=e.get("Filter");if(!isName(a,"Standard"))throw new FormatError("unknown encryption method");this.filterName=a.name;this.dict=e;const s=e.get("V");if(!Number.isInteger(s)||1!==s&&2!==s&&4!==s&&5!==s)throw new FormatError("unsupported encryption algorithm");this.algorithm=s;let r=e.get("Length");if(!r)if(s<=3)r=40;else{const t=e.get("CF"),i=e.get("StmF");if(t instanceof Dict&&i instanceof Name){t.suppressEncryption=!0;const e=t.get(i.name);r=e?.get("Length")||128;r<40&&(r<<=3)}}if(!Number.isInteger(r)||r<40||r%8!=0)throw new FormatError("invalid key length");const n=stringToBytes(e.get("O")),g=stringToBytes(e.get("U")),o=n.subarray(0,32),c=g.subarray(0,32),C=e.get("P"),h=e.get("R"),l=(4===s||5===s)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=l;const Q=stringToBytes(t);let E,u;if(i){if(6===h)try{i=utf8StringToString(i)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}E=stringToBytes(i)}if(5!==s)u=this.#P(Q,E,o,c,C,h,r,l);else{const t=n.subarray(32,40),i=n.subarray(40,48),a=g.subarray(0,48),s=g.subarray(32,40),r=g.subarray(40,48),C=stringToBytes(e.get("OE")),l=stringToBytes(e.get("UE")),Q=stringToBytes(e.get("Perms"));u=this.#O(h,E,o,t,i,a,c,s,r,C,l,Q)}if(!u&&!i)throw new PasswordException("No password given",rt);if(!u&&i){const e=this.#W(E,o,h,r);u=this.#P(Q,e,o,c,C,h,r,l)}if(!u)throw new PasswordException("Incorrect Password",nt);this.encryptionKey=u;if(s>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#X(this.cf,this.strf,e,t,this.encryptionKey),this.#X(this.cf,this.stmf,e,t,this.encryptionKey));const i=this.#j(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(i)};return new CipherTransform(cipherConstructor,cipherConstructor)}}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{constructor(e){super(e);this.node=null}onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const i=t.firstChild;return"value"===i?.nodeName?t.children.map((e=>decodeString(e.textContent))):decodeString(t.textContent)}}class XRef{#Z=null;constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[n,g]=r;if(!Number.isInteger(n)||!Number.isInteger(g))throw new FormatError(`Invalid XRef range fields: ${n}, ${g}`);if(!Number.isInteger(i)||!Number.isInteger(a)||!Number.isInteger(s))throw new FormatError(`Invalid XRef entry fields length: ${n}, ${g}`);for(let r=t.entryNum;r=e.length);){i+=String.fromCharCode(a);a=e[t]}return i}function skipUntil(e,t,i){const a=i.length,s=e.length;let r=0;for(;t=a)break;t++;r++}return r}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,i=/^(\d+)\s+(\d+)\s+obj\b/,a=new Uint8Array([116,114,97,105,108,101,114]),s=new Uint8Array([115,116,97,114,116,120,114,101,102]),r=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const n=this.stream;n.pos=0;const g=n.getBytes(),o=bytesToString(g),c=g.length;let C=n.start;const h=[],l=[];for(;C=c)break;Q=g[C]}while(10!==Q&&13!==Q);continue}const E=readToken(g,C);let u;if(E.startsWith("xref")&&(4===E.length||/\s/.test(E[4]))){C+=skipUntil(g,C,a);h.push(C);C+=skipUntil(g,C,s)}else if(u=i.exec(E)){const t=0|u[1],i=0|u[2],a=C+E.length;let s,h=!1;if(this.entries[t]){if(this.entries[t].gen===i)try{new Parser({lexer:new Lexer(n.makeSubStream(a))}).getObj();h=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${E}): "${e}".`):h=!0}}else h=!0;h&&(this.entries[t]={offset:C-n.start,gen:i,uncompressed:!0});e.lastIndex=a;const Q=e.exec(o);if(Q){s=e.lastIndex+1-C;if("endobj"!==Q[1]){warn(`indexObjects: Found "${Q[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);s-=Q[1].length+1}}else s=c-C;const d=g.subarray(C,C+s),f=skipUntil(d,0,r);if(f0?Math.max(...this._xrefStms):null)}getEntry(e){const t=this.entries[e];return t&&!t.free&&t.offset?t:null}fetchIfRef(e,t=!1){return e instanceof Ref?this.fetch(e,t):e}fetch(e,t=!1){if(!(e instanceof Ref))throw new Error("ref object is not a reference");const i=e.num,a=this._cacheMap.get(i);if(void 0!==a){a instanceof Dict&&!a.objId&&(a.objId=e.toString());return a}let s=this.getEntry(i);if(null===s){this._cacheMap.set(i,s);return s}if(this._pendingRefs.has(e)){this._pendingRefs.remove(e);warn(`Ignoring circular reference: ${e}.`);return lt}this._pendingRefs.put(e);try{s=s.uncompressed?this.fetchUncompressed(e,s,t):this.fetchCompressed(e,s,t);this._pendingRefs.remove(e)}catch(t){this._pendingRefs.remove(e);throw t}s instanceof Dict?s.objId=e.toString():s instanceof BaseStream&&(s.dict.objId=e.toString());return s}fetchUncompressed(e,t,i=!1){const a=e.gen;let s=e.num;if(t.gen!==a){const r=`Inconsistent generation in XRef: ${e}`;if(this._generationFallback&&t.gen0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this._getBoundingBox("MediaBox")||tg)}get cropBox(){return shadow(this,"cropBox",this._getBoundingBox("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const i=Util.intersect(e,t);if(i&&i[2]-i[0]>0&&i[3]-i[1]>0)return shadow(this,"view",i);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this._getInheritableProperty("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}_onSubStreamError(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}getContentStream(){return this.pdfManager.ensure(this,"content").then((e=>e instanceof BaseStream?e:Array.isArray(e)?new StreamsSequenceStream(e,this._onSubStreamError.bind(this)):new NullStream))}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#V(e,t,i){const a=[];for(const s of e)if(s.id){const e=Ref.fromString(s.id);if(!e){warn(`A non-linked annotation cannot be modified: ${s.id}`);continue}if(s.deleted){t.put(e,e);if(s.popupRef){const e=Ref.fromString(s.popupRef);e&&t.put(e,e)}continue}i?.put(e);s.ref=e;a.push(this.xref.fetchAsync(e).then((e=>{e instanceof Dict&&(s.oldAnnotation=e.clone())}),(()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)})));delete s.id}await Promise.all(a)}async saveNewAnnotations(e,t,i,a,s){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const r=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),n=new RefSetCache,g=new RefSet;await this.#V(i,n,g);const o=this.pageDict,c=this.annotations.filter((e=>!(e instanceof Ref&&n.has(e)))),C=await AnnotationFactory.saveNewAnnotations(r,t,i,a,s);for(const{ref:e}of C.annotations)e instanceof Ref&&!g.has(e)&&c.push(e);const h=o.clone();h.set("Annots",c);s.put(this.ref,{data:h});for(const e of n)s.put(e,{data:null})}save(e,t,i,a){const s=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions});return this._parsedAnnotations.then((function(e){const r=[];for(const n of e)r.push(n.save(s,t,i,a).catch((function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null})));return Promise.all(r)}))}loadResources(e){this.resourcesPromise||=this.pdfManager.ensure(this,"resources");return this.resourcesPromise.then((()=>new ObjectLoader(this.resources,e,this.xref).load()))}getOperatorList({handler:e,sink:t,task:i,intent:a,cacheKey:s,annotationStorage:r=null,modifiedIds:n=null}){const C=this.getContentStream(),E=this.loadResources(["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"]),d=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),f=this.xfaFactory?null:getNewAnnotationsMap(r),p=f?.get(this.pageIndex);let m=Promise.resolve(null),y=null;if(p){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const a=new Set;for(const{bitmapId:e,bitmap:t}of p)!e||t||a.has(e)||a.add(e);const{isOffscreenCanvasSupported:s}=this.evaluatorOptions;if(a.size>0){const e=p.slice();for(const[t,i]of r)t.startsWith(u)&&i.bitmap&&a.has(i.bitmapId)&&e.push(i);t=AnnotationFactory.generateImages(e,this.xref,s)}else t=AnnotationFactory.generateImages(p,this.xref,s);y=new RefSet;m=Promise.all([e,this.#V(p,y,null)]).then((([e])=>e?AnnotationFactory.printNewAnnotations(e,d,i,p,t):null))}const w=Promise.all([C,E]).then((([r])=>{const n=new OperatorList(a,t);e.send("StartRenderPage",{transparency:d.hasBlendModes(this.resources,this.nonBlendModesSet),pageIndex:this.pageIndex,cacheKey:s});return d.getOperatorList({stream:r,task:i,resources:this.resources,operatorList:n}).then((function(){return n}))}));return Promise.all([w,this._parsedAnnotations,m]).then((function([e,t,s]){if(s){t=t.filter((e=>!(e.ref&&y.has(e.ref))));for(let e=0,i=s.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace)));if(r>=0){t.splice(r,1,a);s.splice(e--,1);i--}}}t=t.concat(s)}if(0===t.length||a&l){e.flush(!0);return{length:e.totalLength}}const C=!!(a&h),E=!!(a&Q),u=!!(a&g),f=!!(a&o),p=!!(a&c),m=[];for(const e of t)(u||f&&e.mustBeViewed(r,C)&&e.mustBeViewedWhenEditing(E,n)||p&&e.mustBePrinted(r))&&m.push(e.getOperatorList(d,i,a,r).catch((function(e){warn(`getOperatorList - ignoring annotation data during "${i.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}})));return Promise.all(m).then((function(t){let i=!1,a=!1;for(const{opList:s,separateForm:r,separateCanvas:n}of t){e.addOpList(s);i||=r;a||=n}e.flush(!0,{form:i,canvas:a});return{length:e.totalLength}}))}))}async extractTextContent({handler:e,task:t,includeMarkedContent:i,disableNormalization:a,sink:s}){const r=this.getContentStream(),n=this.loadResources(["ExtGState","Font","Properties","XObject"]),g=this.pdfManager.ensureCatalog("lang"),[o,,c]=await Promise.all([r,n,g]);return new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}).getTextContent({stream:o,task:t,resources:this.resources,includeMarkedContent:i,disableNormalization:a,sink:s,viewBox:this.view,lang:c})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return this.pdfManager.ensure(t,"serializable")}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,i){const a=await this._parsedAnnotations;if(0===a.length)return a;const s=[],r=[];let n;const C=!!(i&g),h=!!(i&o),l=!!(i&c);for(const i of a){const a=C||h&&i.viewable;(a||l&&i.printable)&&s.push(i.data);if(i.hasTextContent&&a){n||=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions});r.push(i.extractTextContent(n,t,[-1/0,-1/0,1/0,1/0]).catch((function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)})))}}await Promise.all(r);return s}get annotations(){const e=this._getInheritableProperty("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then((async e=>{if(0===e.length)return e;const[t,i]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const a=i?.orphanFields,s=[];for(const i of e)s.push(AnnotationFactory.create(this.xref,i,t,this._localIdFactory,!1,a,this.ref).catch((function(e){warn(`_parsedAnnotations: "${e}".`);return null})));const r=[];let n,g;for(const e of await Promise.all(s))e&&(e instanceof WidgetAnnotation?(g||=[]).push(e):e instanceof PopupAnnotation?(n||=[]).push(e):r.push(e));g&&r.push(...g);n&&r.push(...n);return r})))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,pA))}}const ig=new Uint8Array([37,80,68,70,45]),ag=new Uint8Array([115,116,97,114,116,120,114,101,102]),sg=new Uint8Array([101,110,100,111,98,106]);function find(e,t,i=1024,a=!1){const s=t.length,r=e.peekBytes(i),n=r.length-s;if(n<=0)return!1;if(a){const i=s-1;let a=r.length-1;for(;a>=i;){let n=0;for(;n=s){e.pos+=a-i;return!0}a--}}else{let i=0;for(;i<=n;){let a=0;for(;a=s){e.pos+=i;return!0}i++}}return!1}class PDFDocument{constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);this._pagePromises=new Map;this._version=null;const i={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++i.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,sg)){e.skip(6);let i=e.peekByte();for(;isWhiteSpace(i);){e.pos++;i=e.peekByte()}t=e.pos-e.start}}else{const i=1024,a=ag.length;let s=!1,r=e.end;for(;!s&&r>0;){r-=i-a;r<0&&(r=0);e.pos=r;s=find(e,ag,i,!0)}if(s){e.skip(9);let i;do{i=e.getByte()}while(isWhiteSpace(i));let a="";for(;i>=32&&i<=57;){a+=String.fromCharCode(i);i=e.getByte()}t=parseInt(a,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,ig))return;e.moveStart();e.skip(ig.length);let t,i="";for(;(t=e.getByte())>32&&i.length<7;)i+=String.fromCharCode(t);ft.test(i)?this._version=i:warn(`Invalid PDF header version: ${i}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}_hasOnlyDocumentSignatures(e,t=0){return!!Array.isArray(e)&&e.every((e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("_hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this._hasOnlyDocumentSignatures(e.get("Kids"),t)}const i=isName(e.get("FT"),"Sig"),a=e.get("Rect"),s=Array.isArray(a)&&a.every((e=>0===e));return i&&s}))}get _xfaStreams(){const e=this.catalog.acroForm;if(!e)return null;const t=e.get("XFA"),i={"xdp:xdp":"",template:"",datasets:"",config:"",connectionSet:"",localeSet:"",stylesheet:"","/xdp:xdp":""};if(t instanceof BaseStream&&!t.isEmpty){i["xdp:xdp"]=t;return i}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,a=t.length;e0;e.hasFields=a;const s=t.get("XFA");e.hasXfa=Array.isArray(s)&&s.length>0||s instanceof BaseStream&&!s.isEmpty;const r=!!(1&t.get("SigFlags")),n=r&&this._hasOnlyDocumentSignatures(i);e.hasAcroForm=a&&!n;e.hasSignatures=r}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const e={PDFFormatVersion:this.version,Language:this.catalog.lang,EncryptFilterName:this.xref.encrypt?this.xref.encrypt.filterName:null,IsLinearized:!!this.linearization,IsAcroFormPresent:this.formInfo.hasAcroForm,IsXFAPresent:this.formInfo.hasXfa,IsCollectionPresent:!!this.catalog.collection,IsSignaturesPresent:this.formInfo.hasSignatures};let t;try{t=this.xref.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(t instanceof Dict))return shadow(this,"documentInfo",e);for(const i of t.getKeys()){const a=t.get(i);switch(i){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof a){e[i]=stringToPDFString(a);continue}break;case"Trapped":if(a instanceof Name){e[i]=a;continue}break;default:let t;switch(typeof a){case"string":t=stringToPDFString(a);break;case"number":case"boolean":t=a;break;default:a instanceof Name&&(t=a)}if(void 0===t){warn(`Bad value, for custom key "${i}", in Info: ${a}.`);continue}e.Custom||(e.Custom=Object.create(null));e.Custom[i]=t;continue}warn(`Bad value, for key "${i}", in Info: ${a}.`)}return shadow(this,"documentInfo",e)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let i,a;if(Array.isArray(t)&&validate(t[0])){i=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(a=stringToBytes(t[1]))}else i=$n(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[toHexUtil(i),a?toHexUtil(a):null])}async _getLinearizationPage(e){const{catalog:t,linearization:i,xref:a}=this,s=Ref.get(i.objectNumberFirst,0);try{const e=await a.fetchAsync(s);if(e instanceof Dict){let i=e.getRaw("Type");i instanceof Ref&&(i=await a.fetchAsync(i));if(isName(i,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(s)||t.pageKidsCountCache.put(s,1);t.pageIndexCache.has(s)||t.pageIndexCache.put(s,0);return[e,s]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(i){warn(`_getLinearizationPage: "${i.message}".`);return t.getPageDict(e)}}getPage(e){const t=this._pagePromises.get(e);if(t)return t;const{catalog:i,linearization:a,xfaFactory:s}=this;let r;r=s?Promise.resolve([Dict.empty,null]):a?.pageFirst===e?this._getLinearizationPage(e):i.getPageDict(e);r=r.then((([t,a])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:a,globalIdFactory:this._globalIdFactory,fontCache:i.fontCache,builtInCMapCache:i.builtInCMapCache,standardFontDataCache:i.standardFontDataCache,globalImageCache:i.globalImageCache,systemFontCache:i.systemFontCache,nonBlendModesSet:i.nonBlendModesSet,xfaFactory:s})));this._pagePromises.set(e,r);return r}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this._pagePromises.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:i}=this;t.setActualNumPages();let a;try{await Promise.all([i.ensureDoc("xfaFactory"),i.ensureDoc("linearization"),i.ensureCatalog("numPages")]);if(this.xfaFactory)return;a=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(a))throw new FormatError("Page count is not an integer.");if(a<=1)return;await this.getPage(a-1)}catch(s){this._pagePromises.delete(a-1);await this.cleanup();if(s instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${a}.`);let r;try{r=await t.getAllPageDicts(e)}catch(i){if(i instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[a,s]]of r){let r;if(a instanceof Error){r=Promise.reject(a);r.catch((()=>{}))}else r=Promise.resolve(new Page({pdfManager:i,xref:this.xref,pageIndex:e,pageDict:a,ref:s,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this._pagePromises.set(e,r)}t.setActualNumPages(r.size)}}fontFallback(e,t){return this.catalog.fontFallback(e,t)}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#z(e,t,i,a,s,r,n){const{xref:g}=this;if(!(i instanceof Ref)||r.has(i))return;r.put(i);const o=await g.fetchAsync(i);if(!(o instanceof Dict))return;if(o.has("T")){const t=stringToPDFString(await o.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let i=o;for(;;){i=i.getRaw("Parent")||t;if(i instanceof Ref){if(r.has(i))break;i=await g.fetchAsync(i)}if(!(i instanceof Dict))break;if(i.has("T")){const t=stringToPDFString(await i.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!o.has("Parent")&&isName(o.get("Subtype"),"Widget")&&n.put(i,t);a.has(e)||a.set(e,[]);a.get(e).push(AnnotationFactory.create(g,i,s,null,!0,n,null).then((e=>e?.getFieldObject())).catch((function(e){warn(`#collectFieldObjects: "${e}".`);return null})));if(!o.has("Kids"))return;const c=await o.getAsync("Kids");if(Array.isArray(c))for(const t of c)await this.#z(e,i,t,a,s,r,n)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then((async e=>{if(!e.hasFields)return null;const[t,i]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureCatalog("acroForm")]);if(!t)return null;const a=new RefSet,s=Object.create(null),r=new Map,n=new RefSetCache;for(const e of await i.getAsync("Fields"))await this.#z("",null,e,r,t,a,n);const g=[];for(const[e,t]of r)g.push(Promise.all(t).then((t=>{(t=t.filter((e=>!!e))).length>0&&(s[e]=t)})));await Promise.all(g);return{allFields:s,orphanFields:n}})))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t&&Object.values(t.allFields).some((e=>e.some((e=>null!==e.actions))))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const i of e)i instanceof Ref&&t.push(i.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}}class BasePdfManager{constructor(e){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e.docBaseUrl);this._docId=e.docId;this._password=e.password;this.enableXfa=e.enableXfa;e.evaluatorOptions.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;e.evaluatorOptions.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(e.evaluatorOptions)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}get catalog(){return this.pdfDocument.catalog}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}loadXfaFonts(e,t){return this.pdfDocument.loadXfaFonts(e,t)}loadXfaImages(){return this.pdfDocument.loadXfaImages()}serializeXfaData(e){return this.pdfDocument.serializeXfaData(e)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,i){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,i){const a=e[t];return"function"==typeof a?a.apply(e,i):a}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,i){try{const a=e[t];return"function"==typeof a?a.apply(e,i):a}catch(a){if(!(a instanceof MissingDataException))throw a;await this.requestRange(a.begin,a.end);return this.ensure(e,t,i)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const rg=1,ng=2,gg=1,og=2,Ig=3,cg=4,Cg=5,hg=6,lg=7,Bg=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof MissingPDFException||e instanceof PasswordException||e instanceof UnexpectedResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"MissingPDFException":return new MissingPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"UnexpectedResponseException":return new UnexpectedResponseException(e.message,e.status);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#_=new AbortController;constructor(e,t,i){this.sourceName=e;this.targetName=t;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#$.bind(this),{signal:this.#_.signal})}#$({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#AA(e);return}if(e.callback){const t=e.callbackId,i=this.callbackCapabilities[t];if(!i)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===rg)i.resolve(e.data);else{if(e.callback!==ng)throw new Error("Unexpected callback case");i.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const i=this.sourceName,a=e.sourceName,s=this.comObj;Promise.try(t,e.data).then((function(t){s.postMessage({sourceName:i,targetName:a,callback:rg,callbackId:e.callbackId,data:t})}),(function(t){s.postMessage({sourceName:i,targetName:a,callback:ng,callbackId:e.callbackId,reason:wrapReason(t)})}))}else e.streamId?this.#eA(e):t(e.data)}on(e,t){const i=this.actionHandler;if(i[e])throw new Error(`There is already an actionName called "${e}"`);i[e]=t}send(e,t,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},i)}sendWithPromise(e,t,i){const a=this.callbackId++,s=Promise.withResolvers();this.callbackCapabilities[a]=s;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:a,data:t},i)}catch(e){s.reject(e)}return s.promise}sendWithStream(e,t,i,a){const s=this.streamId++,r=this.sourceName,n=this.targetName,g=this.comObj;return new ReadableStream({start:i=>{const o=Promise.withResolvers();this.streamControllers[s]={controller:i,startCall:o,pullCall:null,cancelCall:null,isClosed:!1};g.postMessage({sourceName:r,targetName:n,action:e,streamId:s,data:t,desiredSize:i.desiredSize},a);return o.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[s].pullCall=t;g.postMessage({sourceName:r,targetName:n,stream:hg,streamId:s,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[s].cancelCall=t;this.streamControllers[s].isClosed=!0;g.postMessage({sourceName:r,targetName:n,stream:gg,streamId:s,reason:wrapReason(e)});return t.promise}},i)}#eA(e){const t=e.streamId,i=this.sourceName,a=e.sourceName,s=this.comObj,r=this,n=this.actionHandler[e.action],g={enqueue(e,r=1,n){if(this.isCancelled)return;const g=this.desiredSize;this.desiredSize-=r;if(g>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}s.postMessage({sourceName:i,targetName:a,stream:cg,streamId:t,chunk:e},n)},close(){if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:i,targetName:a,stream:Ig,streamId:t});delete r.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;s.postMessage({sourceName:i,targetName:a,stream:Cg,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};g.sinkCapability.resolve();g.ready=g.sinkCapability.promise;this.streamSinks[t]=g;Promise.try(n,e.data,g).then((function(){s.postMessage({sourceName:i,targetName:a,stream:Bg,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:Bg,streamId:t,reason:wrapReason(e)})}))}#AA(e){const t=e.streamId,i=this.sourceName,a=e.sourceName,s=this.comObj,r=this.streamControllers[t],n=this.streamSinks[t];switch(e.stream){case Bg:e.success?r.startCall.resolve():r.startCall.reject(wrapReason(e.reason));break;case lg:e.success?r.pullCall.resolve():r.pullCall.reject(wrapReason(e.reason));break;case hg:if(!n){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,success:!0});break}n.desiredSize<=0&&e.desiredSize>0&&n.sinkCapability.resolve();n.desiredSize=e.desiredSize;Promise.try(n.onPull||onFn).then((function(){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:lg,streamId:t,reason:wrapReason(e)})}));break;case cg:assert(r,"enqueue should have stream controller");if(r.isClosed)break;r.controller.enqueue(e.chunk);break;case Ig:assert(r,"close should have stream controller");if(r.isClosed)break;r.isClosed=!0;r.controller.close();this.#tA(r,t);break;case Cg:assert(r,"error should have stream controller");r.controller.error(wrapReason(e.reason));this.#tA(r,t);break;case og:e.success?r.cancelCall.resolve():r.cancelCall.reject(wrapReason(e.reason));this.#tA(r,t);break;case gg:if(!n)break;const g=wrapReason(e.reason);Promise.try(n.onCancel||onFn,g).then((function(){s.postMessage({sourceName:i,targetName:a,stream:og,streamId:t,success:!0})}),(function(e){s.postMessage({sourceName:i,targetName:a,stream:og,streamId:t,reason:wrapReason(e)})}));n.sinkCapability.reject(g);n.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#tA(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#_?.abort();this.#_=null}}async function writeObject(e,t,i,{encrypt:a=null}){const s=a?.createCipherTransform(e.num,e.gen);i.push(`${e.num} ${e.gen} obj\n`);t instanceof Dict?await writeDict(t,i,s):t instanceof BaseStream?await writeStream(t,i,s):(Array.isArray(t)||ArrayBuffer.isView(t))&&await writeArray(t,i,s);i.push("\nendobj\n")}async function writeDict(e,t,i){t.push("<<");for(const a of e.getKeys()){t.push(` /${escapePDFName(a)} `);await writeValue(e.getRaw(a),t,i)}t.push(">>")}async function writeStream(e,t,i){let a=e.getBytes();const{dict:s}=e,[r,n]=await Promise.all([s.getAsync("Filter"),s.getAsync("DecodeParms")]),g=isName(Array.isArray(r)?await s.xref.fetchIfRefAsync(r[0]):r,"FlateDecode");if(a.length>=256||g)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(a).then((async()=>{await t.ready;await t.close()})).catch((()=>{}));const i=await new Response(e.readable).arrayBuffer();a=new Uint8Array(i);let o,c;if(r){if(!g){o=Array.isArray(r)?[Name.get("FlateDecode"),...r]:[Name.get("FlateDecode"),r];n&&(c=Array.isArray(n)?[null,...n]:[null,n])}}else o=Name.get("FlateDecode");o&&s.set("Filter",o);c&&s.set("DecodeParms",c)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let o=bytesToString(a);i&&(o=i.encryptString(o));s.set("Length",o.length);await writeDict(s,t,i);t.push(" stream\n",o,"\nendstream")}async function writeArray(e,t,i){t.push("[");let a=!0;for(const s of e){a?a=!1:t.push(" ");await writeValue(s,t,i)}t.push("]")}async function writeValue(e,t,i){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await writeArray(e,t,i);else if("string"==typeof e){i&&(e=i.encryptString(e));t.push(`(${escapeString(e)})`)}else"number"==typeof e?t.push(numberToString(e)):"boolean"==typeof e?t.push(e.toString()):e instanceof Dict?await writeDict(e,t,i):e instanceof BaseStream?await writeStream(e,t,i):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,i,a){for(let s=t+i-1;s>i-1;s--){a[s]=255&e;e>>=8}return i+t}function writeString(e,t,i){for(let a=0,s=e.length;a1&&(r=i.documentElement.searchNode([s.at(-1)],0));r?r.childNodes=Array.isArray(a)?a.map((e=>new SimpleDOMNode("value",e))):[new SimpleDOMNode("#text",a)]:warn(`Node not found for path: ${t}`)}const a=[];i.documentElement.dump(a);return a.join("")}(a.fetchIfRef(t).getString(),i)}const s=new StringStream(e);s.dict=new Dict(a);s.dict.set("Type",Name.get("EmbeddedFile"));i.put(t,{data:s})}function getIndexes(e){const t=[];for(const{ref:i}of e)i.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(i.num,1);return t}function computeIDs(e,t,i){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const a=function computeMD5(e,t){const i=Math.floor(Date.now()/1e3),a=t.filename||"",s=[i.toString(),a,e.toString()];let r=s.reduce(((e,t)=>e+t.length),0);for(const e of Object.values(t.info)){s.push(e);r+=e.length}const n=new Uint8Array(r);let g=0;for(const e of s){writeString(e,g,n);g+=e.length}return bytesToString($n(n))}(e,t);i.set("ID",[t.fileIds[0],a])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:i,xref:a=null,hasXfa:s=!1,xfaDatasetsRef:r=null,hasXfaDatasetsEntry:n=!1,needAppearances:g,acroFormRef:o=null,acroForm:c=null,xfaData:C=null,useXrefStream:h=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:i,hasXfa:a,hasXfaDatasetsEntry:s,xfaDatasetsRef:r,needAppearances:n,changes:g}){!a||s||r||warn("XFA - Cannot save it");if(!n&&(!a||!r||s))return;const o=t.clone();if(a&&!s){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,r);o.set("XFA",e)}n&&o.set("NeedAppearances",!0);g.put(i,{data:o})}({xref:a,acroForm:c,acroFormRef:o,hasXfa:s,hasXfaDatasetsEntry:n,xfaDatasetsRef:r,needAppearances:g,changes:i});s&&updateXFA({xfaData:C,xfaDatasetsRef:r,changes:i,xref:a});const l=function getTrailerDict(e,t,i){const a=new Dict(null);a.set("Prev",e.startXRef);const s=e.newRef;if(i){t.put(s,{data:""});a.set("Size",s.num+1);a.set("Type",Name.get("XRef"))}else a.set("Size",s.num);null!==e.rootRef&&a.set("Root",e.rootRef);null!==e.infoRef&&a.set("Info",e.infoRef);null!==e.encryptRef&&a.set("Encrypt",e.encryptRef);return a}(t,i,h),Q=[],E=await async function writeChanges(e,t,i=[]){const a=[];for(const[s,{data:r}]of e.items())if(null!==r&&"string"!=typeof r){await writeObject(s,r,i,t);a.push({ref:s,data:i.join("")});i.length=0}else a.push({ref:s,data:r});return a.sort(((e,t)=>e.ref.num-t.ref.num))}(i,a,Q);let u=e.length;const d=e.at(-1);if(10!==d&&13!==d){Q.push("\n");u+=1}for(const{data:e}of E)null!==e&&Q.push(e);await(h?async function getXRefStreamTable(e,t,i,a,s){const r=[];let n=0,g=0;for(const{ref:e,data:a}of i){let i;n=Math.max(n,t);if(null!==a){i=Math.min(e.gen,65535);r.push([1,t,i]);t+=a.length}else{i=Math.min(e.gen+1,65535);r.push([0,0,i])}g=Math.max(g,i)}a.set("Index",getIndexes(i));const o=[1,getSizeInBytes(n),getSizeInBytes(g)];a.set("W",o);computeIDs(t,e,a);const c=o.reduce(((e,t)=>e+t),0),C=new Uint8Array(c*r.length),h=new Stream(C);h.dict=a;let l=0;for(const[e,t,i]of r){l=writeInt(e,o[0],l,C);l=writeInt(t,o[1],l,C);l=writeInt(i,o[2],l,C)}await writeObject(e.newRef,h,s,{});s.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,u,E,l,Q):async function getXRefTable(e,t,i,a,s){s.push("xref\n");const r=getIndexes(i);let n=0;for(const{ref:e,data:a}of i){if(e.num===r[n]){s.push(`${r[n]} ${r[n+1]}\n`);n+=2}if(null!==a){s.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=a.length}else s.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,a);s.push("trailer\n");await writeDict(a,s);s.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,u,E,l,Q));const f=Q.reduce(((e,t)=>e+t.length),e.length),p=new Uint8Array(f);p.set(e);let m=e.length;for(const e of Q){writeString(e,m,p);m+=e.length}return p}class PDFWorkerStream{constructor(e){this._msgHandler=e;this._contentLength=null;this._fullRequestReader=null;this._rangeRequestReaders=[]}getFullReader(){assert(!this._fullRequestReader,"PDFWorkerStream.getFullReader can only be called once.");this._fullRequestReader=new PDFWorkerStreamReader(this._msgHandler);return this._fullRequestReader}getRangeReader(e,t){const i=new PDFWorkerStreamRangeReader(e,t,this._msgHandler);this._rangeRequestReaders.push(i);return i}cancelAllRequests(e){this._fullRequestReader?.cancel(e);for(const t of this._rangeRequestReaders.slice(0))t.cancel(e)}}class PDFWorkerStreamReader{constructor(e){this._msgHandler=e;this.onProgress=null;this._contentLength=null;this._isRangeSupported=!1;this._isStreamingSupported=!1;const t=this._msgHandler.sendWithStream("GetReader");this._reader=t.getReader();this._headersReady=this._msgHandler.sendWithPromise("ReaderHeadersReady").then((e=>{this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._contentLength=e.contentLength}))}get headersReady(){return this._headersReady}get contentLength(){return this._contentLength}get isStreamingSupported(){return this._isStreamingSupported}get isRangeSupported(){return this._isRangeSupported}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader{constructor(e,t,i){this._msgHandler=i;this.onProgress=null;const a=this._msgHandler.sendWithStream("GetRangeReader",{begin:e,end:t});this._reader=a.getReader()}get isStreamingSupported(){return!1}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!t&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let i=!1;e.on("test",(t=>{if(!i){i=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(gt=e)}(e.verbosity)}));e.on("GetDocRequest",(e=>this.createDocumentHandler(e,t)))}static createDocumentHandler(e,t){let i,a=!1,s=null;const r=new Set,n=getVerbosityLevel(),{docId:g,apiVersion:o}=e,c="4.10.38";if(o!==c)throw new Error(`The API version "${o}" does not match the Worker version "${c}".`);const C=[];for(const e in[])C.push(e);if(C.length)throw new Error("The `Array.prototype` contains unexpected enumerable properties: "+C.join(", ")+"; thus breaking e.g. `for...in` iteration of `Array`s.");const h=g+"_worker";let l=new MessageHandler(h,g,t);function ensureNotTerminated(){if(a)throw new Error("Worker was terminated")}function startWorkerTask(e){r.add(e)}function finishWorkerTask(e){e.finish();r.delete(e)}async function loadDocument(e){await i.ensureDoc("checkHeader");await i.ensureDoc("parseStartXRef");await i.ensureDoc("parse",[e]);await i.ensureDoc("checkFirstPage",[e]);await i.ensureDoc("checkLastPage",[e]);const t=await i.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaFonts");startWorkerTask(e);await Promise.all([i.loadXfaFonts(l,e).catch((e=>{})).then((()=>finishWorkerTask(e))),i.loadXfaImages()])}const[a,s]=await Promise.all([i.ensureDoc("numPages"),i.ensureDoc("fingerprints")]);return{numPages:a,fingerprints:s,htmlForXfa:t?await i.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();l.send("GetDoc",{pdfInfo:e})}function onFailure(e){ensureNotTerminated();if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);l.sendWithPromise("PasswordRequest",e).then((function({password:e}){finishWorkerTask(t);i.updatePassword(e);pdfManagerReady()})).catch((function(){finishWorkerTask(t);l.send("DocException",e)}))}else l.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,(function(e){ensureNotTerminated();e instanceof XRefParseException?i.requestLoadedStream().then((function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)})):onFailure(e)}))}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:i,rangeChunkSize:a,length:r,docBaseUrl:n,enableXfa:o,evaluatorOptions:c}){const C={source:null,disableAutoFetch:i,docBaseUrl:n,docId:g,enableXfa:o,evaluatorOptions:c,handler:l,length:r,password:t,rangeChunkSize:a};if(e){C.source=e;return new LocalPdfManager(C)}const h=new PDFWorkerStream(l),Q=h.getFullReader(),E=Promise.withResolvers();let u,d=[],f=0;Q.headersReady.then((function(){if(Q.isRangeSupported){C.source=h;C.length=Q.contentLength;C.disableAutoFetch||=Q.isStreamingSupported;u=new NetworkPdfManager(C);for(const e of d)u.sendProgressiveData(e);d=[];E.resolve(u);s=null}})).catch((function(e){E.reject(e);s=null}));new Promise((function(e,t){const readChunk=function({value:e,done:i}){try{ensureNotTerminated();if(i){if(!u){const e=arrayBuffersToBytes(d);d=[];r&&e.length!==r&&warn("reported HTTP length is different from actual");C.source=e;u=new LocalPdfManager(C);E.resolve(u)}s=null;return}f+=e.byteLength;Q.isStreamingSupported||l.send("DocProgress",{loaded:f,total:Math.max(f,Q.contentLength||0)});u?u.sendProgressiveData(e):d.push(e);Q.read().then(readChunk,t)}catch(e){t(e)}};Q.read().then(readChunk,t)})).catch((function(e){E.reject(e);s=null}));s=e=>{h.cancelAllRequests(e)};return E.promise})(e).then((function(e){if(a){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}i=e;i.requestLoadedStream(!0).then((e=>{l.send("DataLoaded",{length:e.bytes.byteLength})}))})).then(pdfManagerReady,onFailure)}l.on("GetPage",(function(e){return i.getPage(e.pageIndex).then((function(e){return Promise.all([i.ensure(e,"rotate"),i.ensure(e,"ref"),i.ensure(e,"userUnit"),i.ensure(e,"view")]).then((function([e,t,i,a]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:i,view:a}}))}))}));l.on("GetPageIndex",(function(e){const t=Ref.get(e.num,e.gen);return i.ensureCatalog("getPageIndex",[t])}));l.on("GetDestinations",(function(e){return i.ensureCatalog("destinations")}));l.on("GetDestination",(function(e){return i.ensureCatalog("getDestination",[e.id])}));l.on("GetPageLabels",(function(e){return i.ensureCatalog("pageLabels")}));l.on("GetPageLayout",(function(e){return i.ensureCatalog("pageLayout")}));l.on("GetPageMode",(function(e){return i.ensureCatalog("pageMode")}));l.on("GetViewerPreferences",(function(e){return i.ensureCatalog("viewerPreferences")}));l.on("GetOpenAction",(function(e){return i.ensureCatalog("openAction")}));l.on("GetAttachments",(function(e){return i.ensureCatalog("attachments")}));l.on("GetDocJSActions",(function(e){return i.ensureCatalog("jsActions")}));l.on("GetPageJSActions",(function({pageIndex:e}){return i.getPage(e).then((function(e){return i.ensure(e,"jsActions")}))}));l.on("GetOutline",(function(e){return i.ensureCatalog("documentOutline")}));l.on("GetOptionalContentConfig",(function(e){return i.ensureCatalog("optionalContentConfig")}));l.on("GetPermissions",(function(e){return i.ensureCatalog("permissions")}));l.on("GetMetadata",(function(e){return Promise.all([i.ensureDoc("documentInfo"),i.ensureCatalog("metadata")])}));l.on("GetMarkInfo",(function(e){return i.ensureCatalog("markInfo")}));l.on("GetData",(function(e){return i.requestLoadedStream().then((function(e){return e.bytes}))}));l.on("GetAnnotations",(function({pageIndex:e,intent:t}){return i.getPage(e).then((function(i){const a=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(a);return i.getAnnotationsData(l,a,t).then((e=>{finishWorkerTask(a);return e}),(e=>{finishWorkerTask(a);throw e}))}))}));l.on("GetFieldObjects",(function(e){return i.ensureDoc("fieldObjects").then((e=>e?.allFields||null))}));l.on("HasJSActions",(function(e){return i.ensureDoc("hasJSActions")}));l.on("GetCalculationOrderIds",(function(e){return i.ensureDoc("calculationOrderIds")}));l.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:a,filename:s}){const r=[i.requestLoadedStream(),i.ensureCatalog("acroForm"),i.ensureCatalog("acroFormRef"),i.ensureDoc("startXRef"),i.ensureDoc("xref"),i.ensureDoc("linearization"),i.ensureCatalog("structTreeRoot")],n=new RefSetCache,g=[],o=e?null:getNewAnnotationsMap(a),[c,C,h,Q,E,u,d]=await Promise.all(r),f=E.trailer.getRaw("Root")||null;let p;if(o){d?await d.canUpdateStructTree({pdfManager:i,xref:E,newAnnotationsByPage:o})&&(p=d):await StructTreeRoot.canCreateStructureTree({catalogRef:f,pdfManager:i,newAnnotationsByPage:o})&&(p=null);const e=AnnotationFactory.generateImages(a.values(),E,i.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===p?g:[];for(const[a,s]of o)t.push(i.getPage(a).then((t=>{const i=new WorkerTask(`Save (editor): page ${a}`);startWorkerTask(i);return t.saveNewAnnotations(l,i,s,e,n).finally((function(){finishWorkerTask(i)}))})));null===p?g.push(Promise.all(t).then((async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:o,xref:E,catalogRef:f,pdfManager:i,changes:n})}))):p&&g.push(Promise.all(t).then((async()=>{await p.updateStructureTree({newAnnotationsByPage:o,pdfManager:i,changes:n})})))}if(e)g.push(i.serializeXfaData(a));else for(let e=0;ee.needAppearances)),D=C instanceof Dict&&C.get("XFA")||null;let b=null,F=!1;if(Array.isArray(D)){for(let e=0,t=D.length;e{E.resetNewTemporaryRef()}))}));l.on("GetOperatorList",(function(e,t){const a=e.pageIndex;i.getPage(a).then((function(i){const s=new WorkerTask(`GetOperatorList: page ${a}`);startWorkerTask(s);const r=n>=yA?Date.now():0;i.getOperatorList({handler:l,sink:t,task:s,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds}).then((function(e){finishWorkerTask(s);r&&info(`page=${a+1} - getOperatorList: time=${Date.now()-r}ms, len=${e.length}`);t.close()}),(function(e){finishWorkerTask(s);s.terminated||t.error(e)}))}))}));l.on("GetTextContent",(function(e,t){const{pageIndex:a,includeMarkedContent:s,disableNormalization:r}=e;i.getPage(a).then((function(e){const i=new WorkerTask("GetTextContent: page "+a);startWorkerTask(i);const g=n>=yA?Date.now():0;e.extractTextContent({handler:l,task:i,sink:t,includeMarkedContent:s,disableNormalization:r}).then((function(){finishWorkerTask(i);g&&info(`page=${a+1} - getTextContent: time=`+(Date.now()-g)+"ms");t.close()}),(function(e){finishWorkerTask(i);i.terminated||t.error(e)}))}))}));l.on("GetStructTree",(function(e){return i.getPage(e.pageIndex).then((function(e){return i.ensure(e,"getStructTree")}))}));l.on("FontFallback",(function(e){return i.fontFallback(e.id,l)}));l.on("Cleanup",(function(e){return i.cleanup(!0)}));l.on("Terminate",(function(e){a=!0;const t=[];if(i){i.terminate(new AbortException("Worker was terminated."));const e=i.cleanup();t.push(e);i=null}else clearGlobalCaches();s?.(new AbortException("Worker was terminated."));for(const e of r){t.push(e.finished);e.terminate()}return Promise.all(t).then((function(){l.destroy();l=null}))}));l.on("Ready",(function(t){setupDoc(e);e=null}));return h}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}var Qg=__webpack_exports__.WorkerMessageHandler;export{Qg as WorkerMessageHandler}; \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/images/loading-icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c72ebb554be018511ae972c3f2361dff02dce02 GIT binary patch literal 2545 zcma*pX;2es8VB%~zPr=ibVMCx-JQ^BhLDAsK)^**h(ZDp9YGuzZ%~j!}+w%FI;|aC7){7CdVvG)P{bng1y9Te*f}~*`1kQl$jwb z$tlW~rRS!X?#xfm_&6tTdp_`cjgYwbRFLNdoJCN$S-yhg`ZnC-yvedRSmOh%;Y`Gl6bY$Z-}#C=#F4%9!I1b zWQ~f+9P?;vhCxWwlwl=lrWG|7IYo;{jjmzJ5R9?f>n%-d@>kLINUc z4wM5dAO;kq<$}Dk{2-u0$I6@2N}&cUx9nmV1dYc8jfC}%=F9WCg^OQK9C6poh#2!A z3^EU*UFZvS^)?bu3T?J;@Ahb~%I?+@4!l5!*TjC}GIslNan-RCrrd~PdHYnNLJk+m&`$Y+NV(e>CCu%R#_8GqY4cv#j`#uRWdsg9DxWy(?oOvgCU}&@jy%c!H&-Q zqXJxajAtmQRoRa9V-RFXXh-bK*;Fum{BjpkYQGX~i@OZ^Dx0n&H}kvGKqQ?w(6iGXu_g08T|_hp#ZvFzIwKF*a=oMJ~3UGAjZ?g}GOxm44td zXoyYrU*I=y*vHv89hkYH(v5R#wc)BC3dZJKb3K)f>zaM3%JP(mpecViP0eKKYf3zy z->jx_mc?mCtPEvCQ?uppk?eLJt}_IR7giW%Jr)RyI!+E-voIs*lXI*z`GQc_&D#X( z{6G};HPYj6O|$lXxBJeDaweqa{4L=tOZCjTI^&UOxXg})LRG_cr^B9Rqt(i5ORbQX zq`_xCRsH>xEYY%&*Nyi#{S_JZNlTm#K56`RI%7^amom;*h90Si&g1CfaFV3D|a!`3Y-GKKbL*KSbl z>I96`TR@CqPJl(>QqB~RvK~-U)`e`l4LIqj+IU^~yyIe*|BRVB>4Bup%j{tLdKz4j zY^<8P8m~GRGz*yv0&-RJE+-keJ+%m3wNeopzsltWd->eWmBVwUr)pX` zK~CD<;~Z*Uy3W`3+MrEYxm5qYQ!z%YI;y7DTG`UVH0;@{M{!B&id_}3DBQ?zsotuR zEGLdRx25nLm%-wjlnEi;-aN_1S7???rO~WgA67jjr&(vRa3y$u#kqJbeKnw z{!T!1li9>M+sJ6AUe+*9d}2uGjhzd z|L1Rtp8uTGYyZoQ*`DS^m2dw-X{a)l+3m?ncvn^+O>)hdd3(hMtlhkRGns{<8c0I! zDDjpmwtj?@!6kA|iu3q+Ai;@JR+ zfk+ln&YFC{4bhK6IxVgLs4W%^8Lk`qzWU*L>yq0A3;l}{!wKZ!ue)C)SKI)9dl1hl zhIRLV@8E}rwvE{gX(}$f6x*k)_`*Ijt1=EU-Ls6-(phomeQBgtUs z5Xz~Cd*nE)Ac!0i4ep}Z1AugMB(&F?)#CU{Qc{Sp^vKsdL}vRB30H+Bbzrn`M##H3 z{W8dc_mDroEE+p8_}mnJtzZ4!RNe)zhB)Ds;S57nYSJxtek>^~&(7B+N5MPf2+2xx z5Dl&4X|c@f{Kd|z1r+N|$DmsoVp*3yOdxT^J^-VAk)Z@$4^XrPrFP-Co+MXZ+KJ(W z{JNYvraLLWA;&tRhIKOvhW|HC|L-dLvAUF(MG0(Nl?4tB{RzN7I(}Cb%hwN{crFC8 zji#aJElKvDFV+&VI1V?oUMA>*kto0^;3W8FQBSZ|{ z$v~TqE=(8DZa^i$^oht&h};P1N&wMXorKh*Z68gPV&ouy>%f36Oqkwemyeas$Qbz# zV?7Jy%o7KY6^I=P@eCji%W`o5sf(5hySYo9$l4e2`(hIV_?=H-#R6}0$WVA|*(K@3 z=5?@RlcLh(meW%A4)hGzcvEpm(_w?>zhL*i&s9$2>r zAtk{8Cia|+Y+V!uX9BtpXoF%lswuRKsM!pSs!?yhlCy!269K0|b M?FSZn2B>%I-}ej|s{jB1 literal 0 HcmV?d00001 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css new file mode 100644 index 00000000000..86a3716c501 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.css @@ -0,0 +1,3274 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.messageBar{ + --closing-button-icon:url(images/messageBar_closingButton.svg); + --message-bar-close-button-color:var(--text-primary-color); + --message-bar-close-button-color-hover:var(--text-primary-color); + --message-bar-close-button-border-radius:4px; + --message-bar-close-button-border:none; + --message-bar-close-button-hover-bg-color:rgb(21 20 26 / 0.14); + --message-bar-close-button-active-bg-color:rgb(21 20 26 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(21 20 26 / 0.07); +} + +@media (prefers-color-scheme: dark){ + +.messageBar{ + --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); +} + } + +@media screen and (forced-colors: active){ + +.messageBar{ + --message-bar-close-button-color:ButtonText; + --message-bar-close-button-border:1px solid ButtonText; + --message-bar-close-button-hover-bg-color:ButtonText; + --message-bar-close-button-active-bg-color:ButtonText; + --message-bar-close-button-focus-bg-color:ButtonText; + --message-bar-close-button-color-hover:HighlightText; +} + } + +.messageBar{ + + display:flex; + position:relative; + padding:8px 8px 8px 16px; + flex-direction:column; + justify-content:center; + align-items:center; + gap:8px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + + border-radius:4px; + + border:1px solid var(--message-bar-border-color); + background:var(--message-bar-bg-color); + color:var(--message-bar-fg-color); +} + +.messageBar > div{ + display:flex; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +:is(.messageBar > div)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--message-bar-icon); + mask-image:var(--message-bar-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-icon-color); + flex-shrink:0; + } + +.messageBar button{ + cursor:pointer; + } + +:is(.messageBar button):focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; + } + +.messageBar .closeButton{ + width:32px; + height:32px; + background:none; + border-radius:var(--message-bar-close-button-border-radius); + border:var(--message-bar-close-button-border); + + display:flex; + align-items:center; + justify-content:center; + } + +:is(.messageBar .closeButton)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--closing-button-icon); + mask-image:var(--closing-button-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-close-button-color); + } + +:is(.messageBar .closeButton):is(:hover,:active,:focus)::before{ + background-color:var(--message-bar-close-button-color-hover); + } + +:is(.messageBar .closeButton):hover{ + background-color:var(--message-bar-close-button-hover-bg-color); + } + +:is(.messageBar .closeButton):active{ + background-color:var(--message-bar-close-button-active-bg-color); + } + +:is(.messageBar .closeButton):focus{ + background-color:var(--message-bar-close-button-focus-bg-color); + } + +:is(.messageBar .closeButton) > span{ + display:inline-block; + width:0; + height:0; + overflow:hidden; + } + +#editorUndoBar{ + --text-primary-color:#15141a; + + --message-bar-icon:url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color:#0060df; + --message-bar-bg-color:#deeafc; + --message-bar-fg-color:var(--text-primary-color); + --message-bar-border-color:rgb(0 0 0 / 0.08); + + --undo-button-bg-color:rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover:rgb(21 20 26 / 0.14); + --undo-button-bg-color-active:rgb(21 20 26 / 0.21); + + --undo-button-fg-color:var(--message-bar-fg-color); + --undo-button-fg-color-hover:var(--undo-button-fg-color); + --undo-button-fg-color-active:var(--undo-button-fg-color); + + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); +} + +@media (prefers-color-scheme: dark){ + +#editorUndoBar{ + --text-primary-color:#fbfbfe; + + --message-bar-icon-color:#73a7f3; + --message-bar-bg-color:#003070; + --message-bar-border-color:rgb(255 255 255 / 0.08); + + --undo-button-bg-color:rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); + --undo-button-bg-color-active:rgb(255 255 255 / 0.21); +} + } + +@media screen and (forced-colors: active){ + +#editorUndoBar{ + --text-primary-color:CanvasText; + + --message-bar-icon-color:CanvasText; + --message-bar-bg-color:Canvas; + --message-bar-border-color:CanvasText; + + --undo-button-bg-color:ButtonText; + --undo-button-bg-color-hover:SelectedItem; + --undo-button-bg-color-active:SelectedItem; + + --undo-button-fg-color:ButtonFace; + --undo-button-fg-color-hover:SelectedItemText; + --undo-button-fg-color-active:SelectedItemText; + + --focus-ring-color:CanvasText; +} + } + +#editorUndoBar{ + + position:fixed; + top:50px; + left:50%; + transform:translateX(-50%); + z-index:10; + + padding-block:8px; + padding-inline:16px 8px; + + font:menu; + font-size:15px; + + cursor:default; +} + +#editorUndoBar button{ + cursor:pointer; + } + +#editorUndoBar #editorUndoBarUndoButton{ + border-radius:4px; + font-weight:590; + line-height:19.5px; + color:var(--undo-button-fg-color); + border:none; + padding:4px 16px; + margin-inline-start:8px; + height:32px; + + background-color:var(--undo-button-bg-color); + } + +:is(#editorUndoBar #editorUndoBarUndoButton):hover{ + background-color:var(--undo-button-bg-color-hover); + color:var(--undo-button-fg-color-hover); + } + +:is(#editorUndoBar #editorUndoBarUndoButton):active{ + background-color:var(--undo-button-bg-color-active); + color:var(--undo-button-fg-color-active); + } + +#editorUndoBar > div{ + align-items:center; + } + +.dialog{ + --dialog-bg-color:white; + --dialog-border-color:white; + --dialog-shadow:0 2px 14px 0 rgb(58 57 68 / 0.2); + --text-primary-color:#15141a; + --text-secondary-color:#5b5b66; + --hover-filter:brightness(0.9); + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); + --link-fg-color:#0060df; + --link-hover-fg-color:#0250bb; + --separator-color:#f0f0f4; + + --textarea-border-color:#8f8f9d; + --textarea-bg-color:white; + --textarea-fg-color:var(--text-secondary-color); + + --radio-bg-color:#f0f0f4; + --radio-checked-bg-color:#fbfbfe; + --radio-border-color:#8f8f9d; + --radio-checked-border-color:#0060df; + + --button-secondary-bg-color:#f0f0f4; + --button-secondary-fg-color:var(--text-primary-color); + --button-secondary-border-color:var(--button-secondary-bg-color); + --button-secondary-hover-bg-color:var(--button-secondary-bg-color); + --button-secondary-hover-fg-color:var(--button-secondary-fg-color); + --button-secondary-hover-border-color:var(--button-secondary-hover-bg-color); + + --button-primary-bg-color:#0060df; + --button-primary-fg-color:#fbfbfe; + --button-primary-border-color:var(--button-primary-bg-color); + --button-primary-hover-bg-color:var(--button-primary-bg-color); + --button-primary-hover-fg-color:var(--button-primary-fg-color); + --button-primary-hover-border-color:var(--button-primary-hover-bg-color); +} + +@media (prefers-color-scheme: dark){ + +.dialog{ + --dialog-bg-color:#1c1b22; + --dialog-border-color:#1c1b22; + --dialog-shadow:0 2px 14px 0 #15141a; + --text-primary-color:#fbfbfe; + --text-secondary-color:#cfcfd8; + --focus-ring-color:#0df; + --hover-filter:brightness(1.4); + --link-fg-color:#0df; + --link-hover-fg-color:#80ebff; + --separator-color:#52525e; + + --textarea-bg-color:#42414d; + + --radio-bg-color:#2b2a33; + --radio-checked-bg-color:#15141a; + --radio-checked-border-color:#0df; + + --button-secondary-bg-color:#2b2a33; + --button-primary-bg-color:#0df; + --button-primary-fg-color:#15141a; +} + } + +@media screen and (forced-colors: active){ + +.dialog{ + --dialog-bg-color:Canvas; + --dialog-border-color:CanvasText; + --dialog-shadow:none; + --text-primary-color:CanvasText; + --text-secondary-color:CanvasText; + --hover-filter:none; + --focus-ring-color:ButtonBorder; + --link-fg-color:LinkText; + --link-hover-fg-color:LinkText; + --separator-color:CanvasText; + + --textarea-border-color:ButtonBorder; + --textarea-bg-color:Field; + --textarea-fg-color:ButtonText; + + --radio-bg-color:ButtonFace; + --radio-checked-bg-color:ButtonFace; + --radio-border-color:ButtonText; + --radio-checked-border-color:ButtonText; + + --button-secondary-bg-color:ButtonFace; + --button-secondary-fg-color:ButtonText; + --button-secondary-border-color:ButtonText; + --button-secondary-hover-bg-color:AccentColor; + --button-secondary-hover-fg-color:AccentColorText; + + --button-primary-bg-color:ButtonText; + --button-primary-fg-color:ButtonFace; + --button-primary-hover-bg-color:AccentColor; + --button-primary-hover-fg-color:AccentColorText; +} + } + +.dialog{ + + font:message-box; + font-size:13px; + font-weight:400; + line-height:150%; + border-radius:4px; + padding:12px 16px; + border:1px solid var(--dialog-border-color); + background:var(--dialog-bg-color); + color:var(--text-primary-color); + box-shadow:var(--dialog-shadow); +} + +:is(.dialog .mainContainer) *:focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; + } + +:is(.dialog .mainContainer) .title{ + display:flex; + width:auto; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + } + +:is(:is(.dialog .mainContainer) .title) > span{ + font-size:13px; + font-style:normal; + font-weight:590; + line-height:150%; + } + +:is(.dialog .mainContainer) .dialogSeparator{ + width:100%; + height:0; + margin-block:4px; + border-top:1px solid var(--separator-color); + border-bottom:none; + } + +:is(.dialog .mainContainer) .dialogButtonsGroup{ + display:flex; + gap:12px; + align-self:flex-end; + } + +:is(.dialog .mainContainer) .radio{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + } + +:is(:is(.dialog .mainContainer) .radio) > .radioButton{ + display:flex; + gap:8px; + align-self:stretch; + align-items:center; + } + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + box-sizing:border-box; + width:16px; + height:16px; + border-radius:50%; + background-color:var(--radio-bg-color); + border:1px solid var(--radio-border-color); + } + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):hover{ + filter:var(--hover-filter); + } + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):checked{ + background-color:var(--radio-checked-bg-color); + border:4px solid var(--radio-checked-border-color); + } + +:is(:is(.dialog .mainContainer) .radio) > .radioLabel{ + display:flex; + padding-inline-start:24px; + align-items:flex-start; + gap:10px; + align-self:stretch; + } + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioLabel) > span{ + flex:1 0 0; + font-size:11px; + color:var(--text-secondary-color); + } + +:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton)){ + border-radius:4px; + border:1px solid; + font:menu; + font-weight:600; + padding:4px 16px; + width:auto; + height:32px; + } + +:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + cursor:pointer; + filter:var(--hover-filter); + } + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ + color:var(--button-secondary-fg-color); + background-color:var(--button-secondary-bg-color); + border-color:var(--button-secondary-border-color); + } + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + color:var(--button-secondary-hover-fg-color); + background-color:var(--button-secondary-hover-bg-color); + border-color:var(--button-secondary-hover-border-color); + } + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))){ + color:var(--button-primary-fg-color); + background-color:var(--button-primary-bg-color); + border-color:var(--button-primary-border-color); + opacity:1; + } + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton))):hover{ + color:var(--button-primary-hover-fg-color); + background-color:var(--button-primary-hover-bg-color); + border-color:var(--button-primary-hover-border-color); + } + +:is(.dialog .mainContainer) a{ + color:var(--link-fg-color); + } + +:is(:is(.dialog .mainContainer) a):hover{ + color:var(--link-hover-fg-color); + } + +:is(.dialog .mainContainer) textarea{ + font:inherit; + padding:8px; + resize:none; + margin:0; + box-sizing:border-box; + border-radius:4px; + border:1px solid var(--textarea-border-color); + background:var(--textarea-bg-color); + color:var(--textarea-fg-color); + } + +:is(:is(.dialog .mainContainer) textarea):focus{ + outline-offset:0; + border-color:transparent; + } + +:is(:is(.dialog .mainContainer) textarea):disabled{ + pointer-events:none; + opacity:0.4; + } + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#ffebcd; + --message-bar-fg-color:#15141a; + --message-bar-border-color:rgb(0 0 0 / 0.08); + --message-bar-icon:url(images/messageBar_warning.svg); + --message-bar-icon-color:#cd411e; + } + +@media (prefers-color-scheme: dark){ + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#5a3100; + --message-bar-fg-color:#fbfbfe; + --message-bar-border-color:rgb(255 255 255 / 0.08); + --message-bar-icon-color:#e49c49; + } + } + +@media screen and (forced-colors: active){ + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:HighlightText; + --message-bar-fg-color:CanvasText; + --message-bar-border-color:CanvasText; + --message-bar-icon-color:CanvasText; + } + } + +:is(.dialog .mainContainer) .messageBar{ + + align-self:stretch; + } + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div)::before,:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + margin-block:4px; + } + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + flex:1 0 0; + } + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .title{ + font-size:13px; + font-weight:590; + } + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .description{ + font-size:13px; + } + +:is(.dialog .mainContainer) .toggler{ + display:flex; + align-items:center; + gap:8px; + align-self:stretch; + } + +:is(:is(.dialog .mainContainer) .toggler) > .togglerLabel{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.textLayer{ + position:absolute; + text-align:initial; + inset:0; + overflow:clip; + opacity:1; + line-height:1; + -webkit-text-size-adjust:none; + -moz-text-size-adjust:none; + text-size-adjust:none; + forced-color-adjust:none; + transform-origin:0 0; + caret-color:CanvasText; + z-index:0; +} + +.textLayer.highlighting{ + touch-action:none; + } + +.textLayer :is(span,br){ + color:transparent; + position:absolute; + white-space:pre; + cursor:text; + transform-origin:0% 0%; + } + +.textLayer > :not(.markedContent),.textLayer .markedContent span:not(.markedContent){ + z-index:1; + } + +.textLayer span.markedContent{ + top:0; + height:0; + } + +.textLayer span[role="img"]{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; + } + +.textLayer .highlight{ + --highlight-bg-color:rgb(180 0 170 / 0.25); + --highlight-selected-bg-color:rgb(0 100 0 / 0.25); + --highlight-backdrop-filter:none; + --highlight-selected-backdrop-filter:none; + } + +@media screen and (forced-colors: active){ + +.textLayer .highlight{ + --highlight-bg-color:transparent; + --highlight-selected-bg-color:transparent; + --highlight-backdrop-filter:var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter:var( + --hcm-highlight-selected-filter + ); + } + } + +.textLayer .highlight{ + + margin:-1px; + padding:1px; + background-color:var(--highlight-bg-color); + -webkit-backdrop-filter:var(--highlight-backdrop-filter); + backdrop-filter:var(--highlight-backdrop-filter); + border-radius:4px; + } + +.appended:is(.textLayer .highlight){ + position:initial; + } + +.begin:is(.textLayer .highlight){ + border-radius:4px 0 0 4px; + } + +.end:is(.textLayer .highlight){ + border-radius:0 4px 4px 0; + } + +.middle:is(.textLayer .highlight){ + border-radius:0; + } + +.selected:is(.textLayer .highlight){ + background-color:var(--highlight-selected-bg-color); + -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); + backdrop-filter:var(--highlight-selected-backdrop-filter); + } + +.textLayer ::-moz-selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); + } + +.textLayer ::selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); + } + +.textLayer br::-moz-selection{ + background:transparent; + } + +.textLayer br::selection{ + background:transparent; + } + +.textLayer .endOfContent{ + display:block; + position:absolute; + inset:100% 0 0; + z-index:0; + cursor:default; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.textLayer.selecting .endOfContent{ + top:0; + } + +.annotationLayer{ + --annotation-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color:Highlight; + --input-focus-outline:1px solid Canvas; + --input-unfocused-border-color:transparent; + --input-disabled-border-color:transparent; + --input-hover-border-color:black; + --link-outline:none; +} + +@media screen and (forced-colors: active){ + +.annotationLayer{ + --input-focus-border-color:CanvasText; + --input-unfocused-border-color:ActiveText; + --input-disabled-border-color:GrayText; + --input-hover-border-color:Highlight; + --link-outline:1.5px solid LinkText; +} + + .annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid selectedItem; + } + + .annotationLayer .linkAnnotation{ + outline:var(--link-outline); + } + + :is(.annotationLayer .linkAnnotation):hover{ + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + } + + :is(.annotationLayer .linkAnnotation) > a:hover{ + opacity:0 !important; + background:none !important; + box-shadow:none; + } + + .annotationLayer .popupAnnotation .popup{ + outline:calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color:ButtonFace !important; + color:ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + content:""; + pointer-events:none; + } + + .annotationLayer .popupAnnotation.focused .popup{ + outline:calc(3px * var(--scale-factor)) solid Highlight !important; + } + } + +.annotationLayer{ + + position:absolute; + top:0; + left:0; + pointer-events:none; + transform-origin:0 0; +} + +.annotationLayer[data-main-rotation="90"] .norotate{ + transform:rotate(270deg) translateX(-100%); + } + +.annotationLayer[data-main-rotation="180"] .norotate{ + transform:rotate(180deg) translate(-100%, -100%); + } + +.annotationLayer[data-main-rotation="270"] .norotate{ + transform:rotate(90deg) translateY(-100%); + } + +.annotationLayer.disabled section,.annotationLayer.disabled .popup{ + pointer-events:none; + } + +.annotationLayer .annotationContent{ + position:absolute; + width:100%; + height:100%; + pointer-events:none; + } + +.freetext:is(.annotationLayer .annotationContent){ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:1.35; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + } + +.annotationLayer section{ + position:absolute; + text-align:initial; + pointer-events:auto; + box-sizing:border-box; + transform-origin:0 0; + } + +:is(.annotationLayer section):has(div.annotationContent) canvas.annotationContent{ + display:none; + } + +.textLayer.selecting ~ .annotationLayer section{ + pointer-events:none; + } + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton) > a{ + position:absolute; + font-size:1em; + top:0; + left:0; + width:100%; + height:100%; + } + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover{ + opacity:0.2; + background-color:rgb(255 255 0); + box-shadow:0 2px 10px rgb(255 255 0); + } + +.annotationLayer .linkAnnotation.hasBorder:hover{ + background-color:rgb(255 255 0 / 0.2); + } + +.annotationLayer .hasBorder{ + background-size:100% 100%; + } + +.annotationLayer .textAnnotation img{ + position:absolute; + cursor:pointer; + width:100%; + height:100%; + top:0; + left:0; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea),.annotationLayer .choiceWidgetAnnotation select,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + background-image:var(--annotation-unfocused-field-background); + border:2px solid var(--input-unfocused-border-color); + box-sizing:border-box; + font:calc(9px * var(--scale-factor)) sans-serif; + height:100%; + margin:0; + vertical-align:top; + width:100%; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid red; + } + +.annotationLayer .choiceWidgetAnnotation select option{ + padding:0; + } + +.annotationLayer .buttonWidgetAnnotation.radioButton input{ + border-radius:50%; + } + +.annotationLayer .textWidgetAnnotation textarea{ + resize:none; + } + +.annotationLayer .textWidgetAnnotation [disabled]:is(input,textarea),.annotationLayer .choiceWidgetAnnotation select[disabled],.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input[disabled]{ + background:none; + border:2px solid var(--input-disabled-border-color); + cursor:not-allowed; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:hover{ + border:2px solid var(--input-hover-border-color); + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation.checkBox input:hover{ + border-radius:2px; + } + +.annotationLayer .textWidgetAnnotation :is(input,textarea):focus,.annotationLayer .choiceWidgetAnnotation select:focus{ + background:none; + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) :focus{ + background-image:none; + background-color:transparent; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox :focus{ + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation.radioButton :focus{ + border:2px solid var(--input-focus-border-color); + outline:var(--input-focus-outline); + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after,.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + background-color:CanvasText; + content:""; + display:block; + position:absolute; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + height:80%; + left:45%; + width:1px; + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before{ + transform:rotate(45deg); + } + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + transform:rotate(-45deg); + } + +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + border-radius:50%; + height:50%; + left:25%; + top:25%; + width:50%; + } + +.annotationLayer .textWidgetAnnotation input.comb{ + font-family:monospace; + padding-left:2px; + padding-right:0; + } + +.annotationLayer .textWidgetAnnotation input.comb:focus{ + width:103%; + } + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + } + +.annotationLayer .fileAttachmentAnnotation .popupTriggerArea{ + height:100%; + width:100%; + } + +.annotationLayer .popupAnnotation{ + position:absolute; + font-size:calc(9px * var(--scale-factor)); + pointer-events:none; + width:-moz-max-content; + width:max-content; + max-width:45%; + height:auto; + } + +.annotationLayer .popup{ + background-color:rgb(255 255 153); + box-shadow:0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) rgb(136 136 136); + border-radius:calc(2px * var(--scale-factor)); + outline:1.5px solid rgb(255 255 74); + padding:calc(6px * var(--scale-factor)); + cursor:pointer; + font:message-box; + white-space:normal; + word-wrap:break-word; + pointer-events:auto; + } + +.annotationLayer .popupAnnotation.focused .popup{ + outline-width:3px; + } + +.annotationLayer .popup *{ + font-size:calc(9px * var(--scale-factor)); + } + +.annotationLayer .popup > .header{ + display:inline-block; + } + +.annotationLayer .popup > .header h1{ + display:inline; + } + +.annotationLayer .popup > .header .popupDate{ + display:inline-block; + margin-left:calc(5px * var(--scale-factor)); + width:-moz-fit-content; + width:fit-content; + } + +.annotationLayer .popupContent{ + border-top:1px solid rgb(51 51 51); + margin-top:calc(2px * var(--scale-factor)); + padding-top:calc(2px * var(--scale-factor)); + } + +.annotationLayer .richText > *{ + white-space:pre-wrap; + font-size:calc(9px * var(--scale-factor)); + } + +.annotationLayer .popupTriggerArea{ + cursor:pointer; + } + +.annotationLayer section svg{ + position:absolute; + width:100%; + height:100%; + top:0; + left:0; + } + +.annotationLayer .annotationTextContent{ + position:absolute; + width:100%; + height:100%; + opacity:0; + color:transparent; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + pointer-events:none; + } + +:is(.annotationLayer .annotationTextContent) span{ + width:100%; + display:inline-block; + } + +.annotationLayer svg.quadrilateralsContainer{ + contain:strict; + width:0; + height:0; + position:absolute; + top:0; + left:0; + z-index:-1; + } + +:root{ + --xfa-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --xfa-focus-outline:auto; +} + +@media screen and (forced-colors: active){ + :root{ + --xfa-focus-outline:2px solid CanvasText; + } + .xfaLayer *:required{ + outline:1.5px solid selectedItem; + } +} + +.xfaLayer{ + background-color:transparent; +} + +.xfaLayer .highlight{ + margin:-1px; + padding:1px; + background-color:rgb(239 203 237); + border-radius:4px; +} + +.xfaLayer .highlight.appended{ + position:initial; +} + +.xfaLayer .highlight.begin{ + border-radius:4px 0 0 4px; +} + +.xfaLayer .highlight.end{ + border-radius:0 4px 4px 0; +} + +.xfaLayer .highlight.middle{ + border-radius:0; +} + +.xfaLayer .highlight.selected{ + background-color:rgb(203 223 203); +} + +.xfaPage{ + overflow:hidden; + position:relative; +} + +.xfaContentarea{ + position:absolute; +} + +.xfaPrintOnly{ + display:none; +} + +.xfaLayer{ + position:absolute; + text-align:initial; + top:0; + left:0; + transform-origin:0 0; + line-height:1.2; +} + +.xfaLayer *{ + color:inherit; + font:inherit; + font-style:inherit; + font-weight:inherit; + font-kerning:inherit; + letter-spacing:-0.01px; + text-align:inherit; + text-decoration:inherit; + box-sizing:border-box; + background-color:transparent; + padding:0; + margin:0; + pointer-events:auto; + line-height:inherit; +} + +.xfaLayer *:required{ + outline:1.5px solid red; +} + +.xfaLayer div, +.xfaLayer svg, +.xfaLayer svg *{ + pointer-events:none; +} + +.xfaLayer a{ + color:blue; +} + +.xfaRich li{ + margin-left:3em; +} + +.xfaFont{ + color:black; + font-weight:normal; + font-kerning:none; + font-size:10px; + font-style:normal; + letter-spacing:0; + text-decoration:none; + vertical-align:0; +} + +.xfaCaption{ + overflow:hidden; + flex:0 0 auto; +} + +.xfaCaptionForCheckButton{ + overflow:hidden; + flex:1 1 auto; +} + +.xfaLabel{ + height:100%; + width:100%; +} + +.xfaLeft{ + display:flex; + flex-direction:row; + align-items:center; +} + +.xfaRight{ + display:flex; + flex-direction:row-reverse; + align-items:center; +} + +:is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + max-height:100%; +} + +.xfaTop{ + display:flex; + flex-direction:column; + align-items:flex-start; +} + +.xfaBottom{ + display:flex; + flex-direction:column-reverse; + align-items:flex-start; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + width:100%; +} + +.xfaBorder{ + background-color:transparent; + position:absolute; + pointer-events:none; +} + +.xfaWrapped{ + width:100%; + height:100%; +} + +:is(.xfaTextfield, .xfaSelect):focus{ + background-image:none; + background-color:transparent; + outline:var(--xfa-focus-outline); + outline-offset:-1px; +} + +:is(.xfaCheckbox, .xfaRadio):focus{ + outline:var(--xfa-focus-outline); +} + +.xfaTextfield, +.xfaSelect{ + height:100%; + width:100%; + flex:1 1 auto; + border:none; + resize:none; + background-image:var(--xfa-unfocused-field-background); +} + +.xfaSelect{ + padding-inline:2px; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect){ + flex:0 1 auto; +} + +.xfaButton{ + cursor:pointer; + width:100%; + height:100%; + border:none; + text-align:center; +} + +.xfaLink{ + width:100%; + height:100%; + position:absolute; + top:0; + left:0; +} + +.xfaCheckbox, +.xfaRadio{ + width:100%; + height:100%; + flex:0 0 auto; + border:none; +} + +.xfaRich{ + white-space:pre-wrap; + width:100%; + height:100%; +} + +.xfaImage{ + -o-object-position:left top; + object-position:left top; + -o-object-fit:contain; + object-fit:contain; + width:100%; + height:100%; +} + +.xfaLrTb, +.xfaRlTb, +.xfaTb{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaLr{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaRl{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; +} + +.xfaTb > div{ + justify-content:left; +} + +.xfaPosition{ + position:relative; +} + +.xfaArea{ + position:relative; +} + +.xfaValignMiddle{ + display:flex; + align-items:center; +} + +.xfaTable{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaTable .xfaRow{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaTable .xfaRlRow{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; + flex:1; +} + +.xfaTable .xfaRlRow > div{ + flex:1; +} + +:is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea){ + background:initial; +} + +@media print{ + .xfaTextfield, + .xfaSelect{ + background:transparent; + } + + .xfaSelect{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + text-indent:1px; + text-overflow:""; + } +} + +.canvasWrapper svg{ + transform:none; + } + +.moving:is(.canvasWrapper svg){ + z-index:100000; + } + +[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, 1, -1, 0, 1, 0); + } + +[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(-1, 0, 0, -1, 1, 1); + } + +[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, -1, 1, 0, 0, 1); + } + +.draw:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; + } + +.draw[data-draw-rotation="90"]:is(.canvasWrapper svg){ + transform:rotate(90deg); + } + +.draw[data-draw-rotation="180"]:is(.canvasWrapper svg){ + transform:rotate(180deg); + } + +.draw[data-draw-rotation="270"]:is(.canvasWrapper svg){ + transform:rotate(270deg); + } + +.highlight:is(.canvasWrapper svg){ + --blend-mode:multiply; + } + +@media screen and (forced-colors: active){ + +.highlight:is(.canvasWrapper svg){ + --blend-mode:difference; + } + } + +.highlight:is(.canvasWrapper svg){ + + position:absolute; + mix-blend-mode:var(--blend-mode); + } + +.highlight:is(.canvasWrapper svg):not(.free){ + fill-rule:evenodd; + } + +.highlightOutline:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; + fill-rule:evenodd; + fill:none; + } + +.highlightOutline.hovered:is(.canvasWrapper svg):not(.free):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:var(--outline-width); + } + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + var(--outline-width) + 2 * var(--outline-around-width) + ); + } + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:var(--outline-width); + } + +.highlightOutline.free.hovered:is(.canvasWrapper svg):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:calc(2 * var(--outline-width)); + } + +.highlightOutline.free.selected:is(.canvasWrapper svg) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + 2 * (var(--outline-width) + var(--outline-around-width)) + ); + } + +.highlightOutline.free.selected:is(.canvasWrapper svg) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:calc(2 * var(--outline-width)); + } + +.toggle-button{ + --button-background-color:#f0f0f4; + --button-background-color-hover:#e0e0e6; + --button-background-color-active:#cfcfd8; + --color-accent-primary:#0060df; + --color-accent-primary-hover:#0250bb; + --color-accent-primary-active:#054096; + --border-interactive-color:#8f8f9d; + --border-radius-circle:9999px; + --border-width:1px; + --size-item-small:16px; + --size-item-large:32px; + --color-canvas:white; +} + +@media (prefers-color-scheme: dark){ + +.toggle-button{ + --button-background-color:color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover:color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active:color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary:#0df; + --color-accent-primary-hover:#80ebff; + --color-accent-primary-active:#aaf2ff; + --border-interactive-color:#bfbfc9; + --color-canvas:#1c1b22; +} + } + +@media (forced-colors: active){ + +.toggle-button{ + --color-accent-primary:ButtonText; + --color-accent-primary-hover:SelectedItem; + --color-accent-primary-active:SelectedItem; + --border-interactive-color:ButtonText; + --button-background-color:ButtonFace; + --border-interactive-color-hover:SelectedItem; + --border-interactive-color-active:SelectedItem; + --border-interactive-color-disabled:GrayText; + --color-canvas:ButtonText; +} + } + +.toggle-button{ + + --toggle-background-color:var(--button-background-color); + --toggle-background-color-hover:var(--button-background-color-hover); + --toggle-background-color-active:var(--button-background-color-active); + --toggle-background-color-pressed:var(--color-accent-primary); + --toggle-background-color-pressed-hover:var(--color-accent-primary-hover); + --toggle-background-color-pressed-active:var(--color-accent-primary-active); + --toggle-border-color:var(--border-interactive-color); + --toggle-border-color-hover:var(--toggle-border-color); + --toggle-border-color-active:var(--toggle-border-color); + --toggle-border-radius:var(--border-radius-circle); + --toggle-border-width:var(--border-width); + --toggle-height:var(--size-item-small); + --toggle-width:var(--size-item-large); + --toggle-dot-background-color:var(--toggle-border-color); + --toggle-dot-background-color-hover:var(--toggle-dot-background-color); + --toggle-dot-background-color-active:var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed:var(--color-canvas); + --toggle-dot-margin:1px; + --toggle-dot-height:calc( + var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * + var(--toggle-border-width) + ); + --toggle-dot-width:var(--toggle-dot-height); + --toggle-dot-transform-x:calc( + var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) + ); + + -webkit-appearance:none; + + -moz-appearance:none; + + appearance:none; + padding:0; + margin:0; + border:var(--toggle-border-width) solid var(--toggle-border-color); + height:var(--toggle-height); + width:var(--toggle-width); + border-radius:var(--toggle-border-radius); + background:var(--toggle-background-color); + box-sizing:border-box; + flex-shrink:0; +} + +.toggle-button:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); + } + +.toggle-button:enabled:hover{ + background:var(--toggle-background-color-hover); + border-color:var(--toggle-border-color); + } + +.toggle-button:enabled:active{ + background:var(--toggle-background-color-active); + border-color:var(--toggle-border-color); + } + +.toggle-button[aria-pressed="true"]{ + background:var(--toggle-background-color-pressed); + border-color:transparent; + } + +.toggle-button[aria-pressed="true"]:enabled:hover{ + background:var(--toggle-background-color-pressed-hover); + border-color:transparent; + } + +.toggle-button[aria-pressed="true"]:enabled:active{ + background:var(--toggle-background-color-pressed-active); + border-color:transparent; + } + +.toggle-button::before{ + display:block; + content:""; + background-color:var(--toggle-dot-background-color); + height:var(--toggle-dot-height); + width:var(--toggle-dot-width); + margin:var(--toggle-dot-margin); + border-radius:var(--toggle-border-radius); + translate:0; + } + +.toggle-button[aria-pressed="true"]::before{ + translate:var(--toggle-dot-transform-x); + background-color:var(--toggle-dot-background-color-on-pressed); + } + +.toggle-button[aria-pressed="true"]:enabled:hover::before,.toggle-button[aria-pressed="true"]:enabled:active::before{ + background-color:var(--toggle-dot-background-color-on-pressed); + } + +[dir="rtl"] .toggle-button[aria-pressed="true"]::before{ + translate:calc(-1 * var(--toggle-dot-transform-x)); + } + +@media (prefers-reduced-motion: no-preference){ + .toggle-button::before{ + transition:translate 100ms; + } + } + +@media (prefers-contrast){ + .toggle-button:enabled:hover{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button:enabled:active{ + border-color:var(--toggle-border-color-active); + } + + .toggle-button[aria-pressed="true"]:enabled{ + border-color:var(--toggle-border-color); + position:relative; + } + + .toggle-button[aria-pressed="true"]:enabled:hover,.toggle-button[aria-pressed="true"]:enabled:hover:active{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button[aria-pressed="true"]:enabled:active{ + background-color:var(--toggle-dot-background-color-active); + border-color:var(--toggle-dot-background-color-hover); + } + + .toggle-button:hover::before,.toggle-button:active::before{ + background-color:var(--toggle-dot-background-color-hover); + } + } + +@media (forced-colors){ + +.toggle-button{ + --toggle-dot-background-color:var(--color-accent-primary); + --toggle-dot-background-color-hover:var(--color-accent-primary-hover); + --toggle-dot-background-color-active:var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed:var(--button-background-color); + --toggle-background-color-disabled:var(--button-background-color-disabled); + --toggle-border-color-hover:var(--border-interactive-color-hover); + --toggle-border-color-active:var(--border-interactive-color-active); + --toggle-border-color-disabled:var(--border-interactive-color-disabled); +} + + .toggle-button[aria-pressed="true"]:enabled::after{ + border:1px solid var(--button-background-color); + content:""; + position:absolute; + height:var(--toggle-height); + width:var(--toggle-width); + display:block; + border-radius:var(--toggle-border-radius); + inset:-2px; + } + + .toggle-button[aria-pressed="true"]:enabled:active::after{ + border-color:var(--toggle-border-color-active); + } + } + +:root{ + --outline-width:2px; + --outline-color:#0060df; + --outline-around-width:1px; + --outline-around-color:#f0f0f4; + --hover-outline-around-color:var(--outline-around-color); + --focus-outline:solid var(--outline-width) var(--outline-color); + --unfocus-outline:solid var(--outline-width) transparent; + --focus-outline-around:solid var(--outline-around-width) var(--outline-around-color); + --hover-outline-color:#8f8f9d; + --hover-outline:solid var(--outline-width) var(--hover-outline-color); + --hover-outline-around:solid var(--outline-around-width) var(--hover-outline-around-color); + --freetext-line-height:1.35; + --freetext-padding:2px; + --resizer-bg-color:var(--outline-color); + --resizer-size:6px; + --resizer-shift:calc( + 0px - (var(--outline-width) + var(--resizer-size)) / 2 - + var(--outline-around-width) + ); + --editorFreeText-editing-cursor:text; + --editorInk-editing-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; + --editorHighlight-editing-cursor:url(images/cursor-editorTextHighlight.svg) 24 24, text; + --editorFreeHighlight-editing-cursor:url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; + + --new-alt-text-warning-image:url(images/altText_warning.svg); +} +.visuallyHidden{ + position:absolute; + top:0; + left:0; + border:0; + margin:0; + padding:0; + width:0; + height:0; + overflow:hidden; + white-space:nowrap; + font-size:0; +} + +.textLayer.highlighting{ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +.textLayer.highlighting:not(.free) span{ + cursor:var(--editorHighlight-editing-cursor); + } + +[role="img"]:is(.textLayer.highlighting:not(.free) span){ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +.textLayer.highlighting.free span{ + cursor:var(--editorFreeHighlight-editing-cursor); + } + +:is(#viewerContainer.pdfPresentationMode:fullscreen,.annotationEditorLayer.disabled) .noAltTextBadge{ + display:none !important; + } + +@media (min-resolution: 1.1dppx){ + :root{ + --editorFreeText-editing-cursor:url(images/cursor-editorFreeText.svg) 0 16, text; + } +} + +@media screen and (forced-colors: active){ + :root{ + --outline-color:CanvasText; + --outline-around-color:ButtonFace; + --resizer-bg-color:ButtonText; + --hover-outline-color:Highlight; + --hover-outline-around-color:SelectedItemText; + } +} + +[data-editor-rotation="90"]{ + transform:rotate(90deg); +} + +[data-editor-rotation="180"]{ + transform:rotate(180deg); +} + +[data-editor-rotation="270"]{ + transform:rotate(270deg); +} + +.annotationEditorLayer{ + background:transparent; + position:absolute; + inset:0; + font-size:calc(100px * var(--scale-factor)); + transform-origin:0 0; + cursor:auto; +} + +.annotationEditorLayer .selectedEditor{ + z-index:100000 !important; + } + +.annotationEditorLayer.drawing *{ + pointer-events:none !important; + } + +.annotationEditorLayer.waiting{ + content:""; + cursor:wait; + position:absolute; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer.disabled{ + pointer-events:none; +} + +.annotationEditorLayer.freetextEditing{ + cursor:var(--editorFreeText-editing-cursor); +} + +.annotationEditorLayer.inkEditing{ + cursor:var(--editorInk-editing-cursor); +} + +.annotationEditorLayer .draw{ + box-sizing:border-box; +} + +.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor){ + position:absolute; + background:transparent; + z-index:1; + transform-origin:0 0; + cursor:auto; + max-width:100%; + max-height:100%; + border:var(--unfocus-outline); +} + +.draggable.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ + cursor:move; + } + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)){ + border:var(--focus-outline); + outline:var(--focus-outline-around); + } + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor))::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + pointer-events:none; + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor){ + border:var(--hover-outline); + outline:var(--hover-outline-around); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)):hover:not(.selectedEditor)::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-delete-image:url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color:#f0f0f4; + --editor-toolbar-highlight-image:url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-fg-color:#2e2e56; + --editor-toolbar-border-color:#8f8f9d; + --editor-toolbar-hover-border-color:var(--editor-toolbar-border-color); + --editor-toolbar-hover-bg-color:#e0e0e6; + --editor-toolbar-hover-fg-color:var(--editor-toolbar-fg-color); + --editor-toolbar-hover-outline:none; + --editor-toolbar-focus-outline-color:#0060df; + --editor-toolbar-shadow:0 2px 6px 0 rgb(58 57 68 / 0.2); + --editor-toolbar-vert-offset:6px; + --editor-toolbar-height:28px; + --editor-toolbar-padding:2px; + --alt-text-done-color:#2ac3a2; + --alt-text-warning-color:#0090ed; + --alt-text-hover-done-color:var(--alt-text-done-color); + --alt-text-hover-warning-color:var(--alt-text-warning-color); + } + +@media (prefers-color-scheme: dark){ + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:#2b2a33; + --editor-toolbar-fg-color:#fbfbfe; + --editor-toolbar-hover-bg-color:#52525e; + --editor-toolbar-focus-outline-color:#0df; + --alt-text-done-color:#54ffbd; + --alt-text-warning-color:#80ebff; + } + } + +@media screen and (forced-colors: active){ + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:ButtonFace; + --editor-toolbar-fg-color:ButtonText; + --editor-toolbar-border-color:ButtonText; + --editor-toolbar-hover-border-color:AccentColor; + --editor-toolbar-hover-bg-color:ButtonFace; + --editor-toolbar-hover-fg-color:AccentColor; + --editor-toolbar-hover-outline:2px solid var(--editor-toolbar-hover-border-color); + --editor-toolbar-focus-outline-color:ButtonBorder; + --editor-toolbar-shadow:none; + --alt-text-done-color:var(--editor-toolbar-fg-color); + --alt-text-warning-color:var(--editor-toolbar-fg-color); + --alt-text-hover-done-color:var(--editor-toolbar-hover-fg-color); + --alt-text-hover-warning-color:var(--editor-toolbar-hover-fg-color); + } + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar{ + + display:flex; + width:-moz-fit-content; + width:fit-content; + height:var(--editor-toolbar-height); + flex-direction:column; + justify-content:center; + align-items:center; + cursor:default; + pointer-events:auto; + box-sizing:content-box; + padding:var(--editor-toolbar-padding); + + position:absolute; + inset-inline-end:0; + inset-block-start:calc(100% + var(--editor-toolbar-vert-offset)); + + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + } + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + display:none; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar):has(:focus-visible){ + border-color:transparent; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + transform-origin:100% 0; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar){ + transform-origin:0 0; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons{ + display:flex; + justify-content:center; + align-items:center; + gap:0; + height:100%; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) button{ + padding:0; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .divider{ + width:0; + height:calc( + 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) + ); + border-left:1px solid var(--editor-toolbar-border-color); + border-right:none; + display:inline-block; + margin-inline:2px; + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton{ + width:var(--editor-toolbar-height); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-highlight-image); + mask-image:var(--editor-toolbar-highlight-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .highlightButton):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete{ + width:var(--editor-toolbar-height); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-delete-image); + mask-image:var(--editor-toolbar-delete-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .delete):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > *{ + height:var(--editor-toolbar-height); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider){ + border:none; + background-color:transparent; + cursor:pointer; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover{ + border-radius:2px; + background-color:var(--editor-toolbar-hover-bg-color); + color:var(--editor-toolbar-hover-fg-color); + outline:var(--editor-toolbar-hover-outline); + outline-offset:1px; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover:active{ + outline:none; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):focus-visible{ + border-radius:2px; + outline:2px solid var(--editor-toolbar-focus-outline-color); + } + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText{ + --alt-text-add-image:url(images/altText_add.svg); + --alt-text-done-image:url(images/altText_done.svg); + + display:flex; + align-items:center; + justify-content:center; + width:-moz-max-content; + width:max-content; + padding-inline:8px; + pointer-events:all; + font:menu; + font-weight:590; + font-size:12px; + color:var(--editor-toolbar-fg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):disabled{ + pointer-events:none; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + content:""; + -webkit-mask-image:var(--alt-text-add-image); + mask-image:var(--alt-text-add-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + width:12px; + height:13px; + background-color:var(--editor-toolbar-fg-color); + margin-inline-end:4px; + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); + } + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + background-color:var(--alt-text-warning-color); + -webkit-mask-size:cover; + mask-size:cover; + } + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-warning-color); + } + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); + background-color:var(--alt-text-done-color); + } + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-done-color); + } + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip{ + display:none; + word-wrap:anywhere; + } + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#f0f0f4; + --alt-text-tooltip-fg:#15141a; + --alt-text-tooltip-border:#8f8f9d; + --alt-text-tooltip-shadow:0px 2px 6px 0px rgb(58 57 68 / 0.2); + } + +@media (prefers-color-scheme: dark){ + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#1c1b22; + --alt-text-tooltip-fg:#fbfbfe; + --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; + } + } + +@media screen and (forced-colors: active){ + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:Canvas; + --alt-text-tooltip-fg:CanvasText; + --alt-text-tooltip-border:CanvasText; + --alt-text-tooltip-shadow:none; + } + } + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + + display:inline-flex; + flex-direction:column; + align-items:center; + justify-content:center; + position:absolute; + top:calc(100% + 2px); + inset-inline-start:0; + padding-block:2px 3px; + padding-inline:3px; + max-width:300px; + width:-moz-max-content; + width:max-content; + height:auto; + font-size:12px; + + border:0.5px solid var(--alt-text-tooltip-border); + background:var(--alt-text-tooltip-bg); + box-shadow:var(--alt-text-tooltip-shadow); + color:var(--alt-text-tooltip-fg); + + pointer-events:none; + } + +.annotationEditorLayer .freeTextEditor{ + padding:calc(var(--freetext-padding) * var(--scale-factor)); + width:auto; + height:auto; + touch-action:none; +} + +.annotationEditorLayer .freeTextEditor .internal{ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:var(--freetext-line-height); + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.annotationEditorLayer .freeTextEditor .overlay{ + position:absolute; + display:none; + background:transparent; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer freeTextEditor .overlay.enabled{ + display:block; +} + +.annotationEditorLayer .freeTextEditor .internal:empty::before{ + content:attr(default-content); + color:gray; +} + +.annotationEditorLayer .freeTextEditor .internal:focus{ + outline:none; + -webkit-user-select:auto; + -moz-user-select:auto; + user-select:auto; +} + +.annotationEditorLayer .inkEditor{ + width:100%; + height:100%; +} + +.annotationEditorLayer .inkEditor.editing{ + cursor:inherit; +} + +.annotationEditorLayer .inkEditor .inkEditorCanvas{ + position:absolute; + inset:0; + width:100%; + height:100%; + touch-action:none; +} + +.annotationEditorLayer .stampEditor{ + width:auto; + height:auto; +} + +:is(.annotationEditorLayer .stampEditor) canvas{ + position:absolute; + width:100%; + height:100%; + margin:0; + top:0; + left:0; + } + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#f0f0f4; + --no-alt-text-badge-bg-color:#cfcfd8; + --no-alt-text-badge-fg-color:#5b5b66; + } + +@media (prefers-color-scheme: dark){ + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#52525e; + --no-alt-text-badge-bg-color:#fbfbfe; + --no-alt-text-badge-fg-color:#15141a; + } + } + +@media screen and (forced-colors: active){ + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:ButtonText; + --no-alt-text-badge-bg-color:ButtonFace; + --no-alt-text-badge-fg-color:ButtonText; + } + } + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + + position:absolute; + inset-inline-end:5px; + inset-block-end:5px; + display:inline-flex; + width:32px; + height:32px; + padding:3px; + justify-content:center; + align-items:center; + pointer-events:none; + z-index:1; + + border-radius:2px; + border:1px solid var(--no-alt-text-badge-border-color); + background:var(--no-alt-text-badge-bg-color); + } + +:is(:is(.annotationEditorLayer .stampEditor) .noAltTextBadge)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--no-alt-text-badge-fg-color); + } + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers{ + position:absolute; + inset:0; + } + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers){ + display:none; + } + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer{ + width:var(--resizer-size); + height:var(--resizer-size); + background:content-box var(--resizer-bg-color); + border:var(--focus-outline-around); + border-radius:2px; + position:absolute; + } + +.topLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:var(--resizer-shift); + } + +.topMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); + } + +.topRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + right:var(--resizer-shift); + } + +.middleRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + right:var(--resizer-shift); + } + +.bottomRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + right:var(--resizer-shift); + } + +.bottomMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); + } + +.bottomLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:var(--resizer-shift); + } + +.middleLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + left:var(--resizer-shift); + } + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nwse-resize; + } + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ns-resize; + } + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nesw-resize; + } + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ew-resize; + } + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nesw-resize; + } + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ew-resize; + } + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nwse-resize; + } + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ns-resize; + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar{ + rotate:270deg; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="180"],[data-main-rotation="90"] [data-editor-rotation="90"],[data-main-rotation="180"] [data-editor-rotation="0"],[data-main-rotation="270"] [data-editor-rotation="270"])) .editToolbar{ + rotate:180deg; + inset-inline-end:100%; + inset-block-start:calc(0pc - var(--editor-toolbar-vert-offset)); + } + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar{ + rotate:90deg; + } + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:100%; + } + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-start:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; + } + +.dialog.altText::backdrop{ + -webkit-mask:url(#alttext-manager-mask); + mask:url(#alttext-manager-mask); + } + +.dialog.altText.positioned{ + margin:0; + } + +.dialog.altText #altTextContainer{ + width:300px; + height:-moz-fit-content; + height:fit-content; + display:inline-flex; + flex-direction:column; + align-items:flex-start; + gap:16px; + } + +:is(.dialog.altText #altTextContainer) #overallDescription{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + align-self:stretch; + } + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) span{ + align-self:stretch; + } + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) .title{ + font-size:13px; + font-style:normal; + font-weight:590; + } + +:is(.dialog.altText #altTextContainer) #addDescription{ + display:flex; + flex-direction:column; + align-items:stretch; + gap:8px; + } + +:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea{ + flex:1; + padding-inline:24px 10px; + } + +:is(:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea) textarea{ + width:100%; + min-height:75px; + } + +:is(.dialog.altText #altTextContainer) #buttons{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +.dialog.newAltText{ + --new-alt-text-ai-disclaimer-icon:url(images/altText_disclaimer.svg); + --new-alt-text-spinner-icon:url(images/altText_spinner.svg); + --preview-image-bg-color:#f0f0f4; + --preview-image-border:none; +} + +@media (prefers-color-scheme: dark){ + +.dialog.newAltText{ + --preview-image-bg-color:#2b2a33; +} + } + +@media screen and (forced-colors: active){ + +.dialog.newAltText{ + --preview-image-bg-color:ButtonFace; + --preview-image-border:1px solid ButtonText; +} + } + +.dialog.newAltText{ + + width:80%; + max-width:570px; + min-width:300px; + padding:0; +} + +.dialog.newAltText.noAi #newAltTextDisclaimer,.dialog.newAltText.noAi #newAltTextCreateAutomatically{ + display:none !important; + } + +.dialog.newAltText.aiInstalling #newAltTextCreateAutomatically{ + display:none !important; + } + +.dialog.newAltText.aiInstalling #newAltTextDownloadModel{ + display:flex !important; + } + +.dialog.newAltText.error #newAltTextNotNow{ + display:none !important; + } + +.dialog.newAltText.error #newAltTextCancel{ + display:inline-block !important; + } + +.dialog.newAltText:not(.error) #newAltTextError{ + display:none !important; + } + +.dialog.newAltText #newAltTextContainer{ + display:flex; + width:auto; + padding:16px; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + flex:0 1 auto; + line-height:normal; + } + +:is(.dialog.newAltText #newAltTextContainer) #mainContent{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + align-self:stretch; + flex:1 1 auto; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionAndSettings{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:16px; + flex:1 0 0; + align-self:stretch; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; + flex:1 1 auto; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer{ + width:100%; + height:70px; + position:relative; + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea{ + width:100%; + height:100%; + padding:8px; + } + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::-moz-placeholder{ + color:var(--text-secondary-color); + } + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::placeholder{ + color:var(--text-secondary-color); + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:none; + position:absolute; + width:16px; + height:16px; + inset-inline-start:8px; + inset-block-start:8px; + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + pointer-events:none; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::-moz-placeholder{ + color:transparent; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::placeholder{ + color:transparent; + } + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:inline-block; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescription{ + font-size:11px; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer{ + display:flex; + flex-direction:row; + align-items:flex-start; + gap:4px; + font-size:11px; + } + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer)::before{ + content:""; + display:inline-block; + width:17px; + height:16px; + -webkit-mask-image:var(--new-alt-text-ai-disclaimer-icon); + mask-image:var(--new-alt-text-ai-disclaimer-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + flex:1 0 auto; + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel{ + display:flex; + align-items:center; + gap:4px; + align-self:stretch; + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + } + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview{ + width:180px; + aspect-ratio:1; + display:flex; + justify-content:center; + align-items:center; + flex:0 0 auto; + background-color:var(--preview-image-bg-color); + border:var(--preview-image-border); + } + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview) > canvas{ + max-width:100%; + max-height:100%; + } + +.colorPicker{ + --hover-outline-color:#0250bb; + --selected-outline-color:#0060df; + --swatch-border-color:#cfcfd8; +} + +@media (prefers-color-scheme: dark){ + +.colorPicker{ + --hover-outline-color:#80ebff; + --selected-outline-color:#aaf2ff; + --swatch-border-color:#52525e; +} + } + +@media screen and (forced-colors: active){ + +.colorPicker{ + --hover-outline-color:Highlight; + --selected-outline-color:var(--hover-outline-color); + --swatch-border-color:ButtonText; +} + } + +.colorPicker .swatch{ + width:16px; + height:16px; + border:1px solid var(--swatch-border-color); + border-radius:100%; + outline-offset:2px; + box-sizing:border-box; + forced-color-adjust:none; + } + +.colorPicker button:is(:hover,.selected) > .swatch{ + border:none; + } + +.annotationEditorLayer[data-main-rotation="0"] .highlightEditor:not(.free) > .editToolbar{ + rotate:0deg; + } + +.annotationEditorLayer[data-main-rotation="90"] .highlightEditor:not(.free) > .editToolbar{ + rotate:270deg; + } + +.annotationEditorLayer[data-main-rotation="180"] .highlightEditor:not(.free) > .editToolbar{ + rotate:180deg; + } + +.annotationEditorLayer[data-main-rotation="270"] .highlightEditor:not(.free) > .editToolbar{ + rotate:90deg; + } + +.annotationEditorLayer .highlightEditor{ + position:absolute; + background:transparent; + z-index:1; + cursor:auto; + max-width:100%; + max-height:100%; + border:none; + outline:none; + pointer-events:none; + transform-origin:0 0; + } + +:is(.annotationEditorLayer .highlightEditor):not(.free){ + transform:none; + } + +:is(.annotationEditorLayer .highlightEditor) .internal{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + pointer-events:auto; + } + +.disabled:is(.annotationEditorLayer .highlightEditor) .internal{ + pointer-events:none; + } + +.selectedEditor:is(.annotationEditorLayer .highlightEditor) .internal{ + cursor:pointer; + } + +:is(.annotationEditorLayer .highlightEditor) .editToolbar{ + --editor-toolbar-colorpicker-arrow-image:url(images/toolbarButton-menuArrow.svg); + + transform-origin:center !important; + } + +:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker{ + position:relative; + width:auto; + display:flex; + justify-content:center; + align-items:center; + gap:4px; + padding:4px; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker)::after{ + content:""; + -webkit-mask-image:var(--editor-toolbar-colorpicker-arrow-image); + mask-image:var(--editor-toolbar-colorpicker-arrow-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:12px; + height:12px; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):hover::after{ + background-color:var(--editor-toolbar-hover-fg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden)){ + background-color:var(--editor-toolbar-hover-bg-color); + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden))::after{ + scale:-1; + } + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown{ + position:absolute; + display:flex; + justify-content:center; + align-items:center; + flex-direction:column; + gap:11px; + padding-block:8px; + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + inset-block-start:calc(100% + 4px); + width:calc(100% + 2 * var(--editor-toolbar-padding)); + } + +:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button{ + width:100%; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline-offset:2px; + } + +[aria-selected="true"]:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); + } + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); + } + +.editorParamsToolbar:has(#highlightParamsToolbarContainer){ + padding:unset; +} + +#highlightParamsToolbarContainer{ + gap:16px; + padding-inline:10px; + padding-block-end:12px; +} + +#highlightParamsToolbarContainer .colorPicker{ + display:flex; + flex-direction:column; + gap:8px; + } + +:is(#highlightParamsToolbarContainer .colorPicker) .dropdown{ + display:flex; + justify-content:space-between; + align-items:center; + flex-direction:row; + height:auto; + } + +:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button{ + width:auto; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + flex:0 0 auto; + padding:0; + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) .swatch{ + width:24px; + height:24px; + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; + } + +[aria-selected="true"]:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); + } + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); + } + +#highlightParamsToolbarContainer #editorHighlightThickness{ + display:flex; + flex-direction:column; + align-items:center; + gap:4px; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .editorParamsLabel{ + height:auto; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + + --example-color:#bfbfc9; + } + +@media (prefers-color-scheme: dark){ + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:#80808e; + } + } + +@media screen and (forced-colors: active){ + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:CanvasText; + } + } + +:is(:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) > .editorParamsSlider[disabled]){ + opacity:0.4; + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::before,:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + content:""; + width:8px; + aspect-ratio:1; + display:block; + border-radius:100%; + background-color:var(--example-color); + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + width:24px; + } + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) .editorParamsSlider{ + width:unset; + height:14px; + } + +#highlightParamsToolbarContainer #editorHighlightVisibility{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#d7d7db; + } + +@media (prefers-color-scheme: dark){ + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#8f8f9d; + } + } + +@media screen and (forced-colors: active){ + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:CanvasText; + } + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + + margin-block:4px; + width:100%; + height:1px; + background-color:var(--divider-color); + } + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .toggler{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + } + +#altTextSettingsDialog{ + padding:16px; +} + +#altTextSettingsDialog #altTextSettingsContainer{ + display:flex; + width:573px; + flex-direction:column; + gap:16px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) .mainContainer{ + gap:16px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) .description{ + color:var(--text-secondary-color); + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings{ + display:flex; + flex-direction:column; + gap:12px; + } + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) button{ + width:-moz-fit-content; + width:fit-content; + } + +.download:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) #deleteModelButton{ + display:none; + } + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings):not(.download) #downloadModelButton{ + display:none; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticAltText,:is(#altTextSettingsDialog #altTextSettingsContainer) #altTextEditor{ + display:flex; + flex-direction:column; + gap:8px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #createModelDescription,:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings,:is(#altTextSettingsDialog #altTextSettingsContainer) #showAltTextDialogDescription{ + padding-inline-start:40px; + } + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticSettings{ + display:flex; + flex-direction:column; + gap:16px; + } + +:root{ + --viewer-container-height:0; + --pdfViewer-padding-bottom:0; + --page-margin:1px auto -8px; + --page-border:9px solid transparent; + --spreadHorizontalWrapped-margin-LR:-3.5px; + --loading-icon-delay:400ms; +} + +@media screen and (forced-colors: active){ + :root{ + --pdfViewer-padding-bottom:9px; + --page-margin:8px auto -1px; + --page-border:1px solid CanvasText; + --spreadHorizontalWrapped-margin-LR:3.5px; + } +} + +[data-main-rotation="90"]{ + transform:rotate(90deg) translateY(-100%); +} +[data-main-rotation="180"]{ + transform:rotate(180deg) translate(-100%, -100%); +} +[data-main-rotation="270"]{ + transform:rotate(270deg) translateX(-100%); +} + +#hiddenCopyElement, +.hiddenCanvasElement{ + position:absolute; + top:0; + left:0; + width:0; + height:0; + display:none; +} + +.pdfViewer{ + --scale-factor:1; + --page-bg-color:unset; + + padding-bottom:var(--pdfViewer-padding-bottom); + + --hcm-highlight-filter:none; + --hcm-highlight-selected-filter:none; +} + +@media screen and (forced-colors: active){ + +.pdfViewer{ + --hcm-highlight-filter:invert(100%); +} + } + +.pdfViewer.copyAll{ + cursor:wait; + } + +.pdfViewer .canvasWrapper{ + overflow:hidden; + width:100%; + height:100%; + } + +:is(.pdfViewer .canvasWrapper) canvas{ + position:absolute; + top:0; + left:0; + margin:0; + display:block; + width:100%; + height:100%; + contain:content; + } + +:is(:is(.pdfViewer .canvasWrapper) canvas) .structTree{ + contain:strict; + } + +.pdfViewer .page{ + --scale-round-x:1px; + --scale-round-y:1px; + + direction:ltr; + width:816px; + height:1056px; + margin:var(--page-margin); + position:relative; + overflow:visible; + border:var(--page-border); + background-clip:content-box; + background-color:var(--page-bg-color, rgb(255 255 255)); +} + +.pdfViewer .dummyPage{ + position:relative; + width:0; + height:var(--viewer-container-height); +} + +.pdfViewer.noUserSelect{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.pdfViewer.removePageBorders .page{ + margin:0 auto 10px; + border:none; +} + +.pdfViewer.singlePageView{ + display:inline-block; +} + +.pdfViewer.singlePageView .page{ + margin:0; + border:none; +} + +.pdfViewer:is(.scrollHorizontal, .scrollWrapped), +.spread{ + margin-inline:3.5px; + text-align:center; +} + +.pdfViewer.scrollHorizontal, +.spread{ + white-space:nowrap; +} + +.pdfViewer.removePageBorders, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread{ + margin-inline:0; +} + +.spread :is(.page, .dummyPage), +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread){ + display:inline-block; + vertical-align:middle; +} + +.spread .page, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:var(--spreadHorizontalWrapped-margin-LR); +} + +.pdfViewer.removePageBorders .spread .page, +.pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:5px; +} + +.pdfViewer .page.loadingIcon::after{ + position:absolute; + top:0; + left:0; + content:""; + width:100%; + height:100%; + background:url("images/loading-icon.gif") center no-repeat; + display:none; + transition-property:display; + transition-delay:var(--loading-icon-delay); + z-index:5; + contain:strict; +} + +.pdfViewer .page.loading::after{ + display:block; +} + +.pdfViewer .page:not(.loading)::after{ + transition-property:none; + display:none; +} + +.pdfPresentationMode .pdfViewer{ + padding-bottom:0; +} + +.pdfPresentationMode .spread{ + margin:0; +} + +.pdfPresentationMode .pdfViewer .page{ + margin:0 auto; + border:2px solid transparent; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs new file mode 100644 index 00000000000..9b2c200c99e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/pdfjs-dist/dist/web/pdf_viewer.mjs @@ -0,0 +1,8435 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ + +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = globalThis.pdfjsViewer = {}; + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + AnnotationLayerBuilder: () => (/* reexport */ AnnotationLayerBuilder), + DownloadManager: () => (/* reexport */ DownloadManager), + EventBus: () => (/* reexport */ EventBus), + FindState: () => (/* reexport */ FindState), + GenericL10n: () => (/* reexport */ genericl10n_GenericL10n), + LinkTarget: () => (/* reexport */ LinkTarget), + PDFFindController: () => (/* reexport */ PDFFindController), + PDFHistory: () => (/* reexport */ PDFHistory), + PDFLinkService: () => (/* reexport */ PDFLinkService), + PDFPageView: () => (/* reexport */ PDFPageView), + PDFScriptingManager: () => (/* reexport */ PDFScriptingManagerComponents), + PDFSinglePageViewer: () => (/* reexport */ PDFSinglePageViewer), + PDFViewer: () => (/* reexport */ PDFViewer), + ProgressBar: () => (/* reexport */ ProgressBar), + RenderingStates: () => (/* reexport */ RenderingStates), + ScrollMode: () => (/* reexport */ ScrollMode), + SimpleLinkService: () => (/* reexport */ SimpleLinkService), + SpreadMode: () => (/* reexport */ SpreadMode), + StructTreeLayerBuilder: () => (/* reexport */ StructTreeLayerBuilder), + TextLayerBuilder: () => (/* reexport */ TextLayerBuilder), + XfaLayerBuilder: () => (/* reexport */ XfaLayerBuilder), + parseQueryString: () => (/* reexport */ parseQueryString) +}); + +;// ./web/ui_utils.js +const DEFAULT_SCALE_VALUE = "auto"; +const DEFAULT_SCALE = 1.0; +const DEFAULT_SCALE_DELTA = 1.1; +const MIN_SCALE = 0.1; +const MAX_SCALE = 10.0; +const UNKNOWN_SCALE = 0; +const MAX_AUTO_SCALE = 1.25; +const SCROLLBAR_PADDING = 40; +const VERTICAL_PADDING = 5; +const RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; +const PresentationModeState = { + UNKNOWN: 0, + NORMAL: 1, + CHANGING: 2, + FULLSCREEN: 3 +}; +const SidebarView = { + UNKNOWN: -1, + NONE: 0, + THUMBS: 1, + OUTLINE: 2, + ATTACHMENTS: 3, + LAYERS: 4 +}; +const TextLayerMode = { + DISABLE: 0, + ENABLE: 1, + ENABLE_PERMISSIONS: 2 +}; +const ScrollMode = { + UNKNOWN: -1, + VERTICAL: 0, + HORIZONTAL: 1, + WRAPPED: 2, + PAGE: 3 +}; +const SpreadMode = { + UNKNOWN: -1, + NONE: 0, + ODD: 1, + EVEN: 2 +}; +const CursorTool = { + SELECT: 0, + HAND: 1, + ZOOM: 2 +}; +const AutoPrintRegExp = /\bprint\s*\(/; +function scrollIntoView(element, spot, scrollMatches = false) { + let parent = element.offsetParent; + if (!parent) { + console.error("offsetParent is not set -- cannot scroll"); + return; + } + let offsetY = element.offsetTop + element.clientTop; + let offsetX = element.offsetLeft + element.clientLeft; + while (parent.clientHeight === parent.scrollHeight && parent.clientWidth === parent.scrollWidth || scrollMatches && (parent.classList.contains("markedContent") || getComputedStyle(parent).overflow === "hidden")) { + offsetY += parent.offsetTop; + offsetX += parent.offsetLeft; + parent = parent.offsetParent; + if (!parent) { + return; + } + } + if (spot) { + if (spot.top !== undefined) { + offsetY += spot.top; + } + if (spot.left !== undefined) { + offsetX += spot.left; + parent.scrollLeft = offsetX; + } + } + parent.scrollTop = offsetY; +} +function watchScroll(viewAreaElement, callback, abortSignal = undefined) { + const debounceScroll = function (evt) { + if (rAF) { + return; + } + rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { + rAF = null; + const currentX = viewAreaElement.scrollLeft; + const lastX = state.lastX; + if (currentX !== lastX) { + state.right = currentX > lastX; + } + state.lastX = currentX; + const currentY = viewAreaElement.scrollTop; + const lastY = state.lastY; + if (currentY !== lastY) { + state.down = currentY > lastY; + } + state.lastY = currentY; + callback(state); + }); + }; + const state = { + right: true, + down: true, + lastX: viewAreaElement.scrollLeft, + lastY: viewAreaElement.scrollTop, + _eventHandler: debounceScroll + }; + let rAF = null; + viewAreaElement.addEventListener("scroll", debounceScroll, { + useCapture: true, + signal: abortSignal + }); + abortSignal?.addEventListener("abort", () => window.cancelAnimationFrame(rAF), { + once: true + }); + return state; +} +function parseQueryString(query) { + const params = new Map(); + for (const [key, value] of new URLSearchParams(query)) { + params.set(key.toLowerCase(), value); + } + return params; +} +const InvisibleCharsRegExp = /[\x00-\x1F]/g; +function removeNullCharacters(str, replaceInvisible = false) { + if (!InvisibleCharsRegExp.test(str)) { + return str; + } + if (replaceInvisible) { + return str.replaceAll(InvisibleCharsRegExp, m => m === "\x00" ? "" : " "); + } + return str.replaceAll("\x00", ""); +} +function binarySearchFirstItem(items, condition, start = 0) { + let minIndex = start; + let maxIndex = items.length - 1; + if (maxIndex < 0 || !condition(items[maxIndex])) { + return items.length; + } + if (condition(items[minIndex])) { + return minIndex; + } + while (minIndex < maxIndex) { + const currentIndex = minIndex + maxIndex >> 1; + const currentItem = items[currentIndex]; + if (condition(currentItem)) { + maxIndex = currentIndex; + } else { + minIndex = currentIndex + 1; + } + } + return minIndex; +} +function approximateFraction(x) { + if (Math.floor(x) === x) { + return [x, 1]; + } + const xinv = 1 / x; + const limit = 8; + if (xinv > limit) { + return [1, limit]; + } else if (Math.floor(xinv) === xinv) { + return [1, xinv]; + } + const x_ = x > 1 ? xinv : x; + let a = 0, + b = 1, + c = 1, + d = 1; + while (true) { + const p = a + c, + q = b + d; + if (q > limit) { + break; + } + if (x_ <= p / q) { + c = p; + d = q; + } else { + a = p; + b = q; + } + } + let result; + if (x_ - a / b < c / d - x_) { + result = x_ === x ? [a, b] : [b, a]; + } else { + result = x_ === x ? [c, d] : [d, c]; + } + return result; +} +function floorToDivide(x, div) { + return x - x % div; +} +function getPageSizeInches({ + view, + userUnit, + rotate +}) { + const [x1, y1, x2, y2] = view; + const changeOrientation = rotate % 180 !== 0; + const width = (x2 - x1) / 72 * userUnit; + const height = (y2 - y1) / 72 * userUnit; + return { + width: changeOrientation ? height : width, + height: changeOrientation ? width : height + }; +} +function backtrackBeforeAllVisibleElements(index, views, top) { + if (index < 2) { + return index; + } + let elt = views[index].div; + let pageTop = elt.offsetTop + elt.clientTop; + if (pageTop >= top) { + elt = views[index - 1].div; + pageTop = elt.offsetTop + elt.clientTop; + } + for (let i = index - 2; i >= 0; --i) { + elt = views[i].div; + if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) { + break; + } + index = i; + } + return index; +} +function getVisibleElements({ + scrollEl, + views, + sortByVisibility = false, + horizontal = false, + rtl = false +}) { + const top = scrollEl.scrollTop, + bottom = top + scrollEl.clientHeight; + const left = scrollEl.scrollLeft, + right = left + scrollEl.clientWidth; + function isElementBottomAfterViewTop(view) { + const element = view.div; + const elementBottom = element.offsetTop + element.clientTop + element.clientHeight; + return elementBottom > top; + } + function isElementNextAfterViewHorizontally(view) { + const element = view.div; + const elementLeft = element.offsetLeft + element.clientLeft; + const elementRight = elementLeft + element.clientWidth; + return rtl ? elementLeft < right : elementRight > left; + } + const visible = [], + ids = new Set(), + numViews = views.length; + let firstVisibleElementInd = binarySearchFirstItem(views, horizontal ? isElementNextAfterViewHorizontally : isElementBottomAfterViewTop); + if (firstVisibleElementInd > 0 && firstVisibleElementInd < numViews && !horizontal) { + firstVisibleElementInd = backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top); + } + let lastEdge = horizontal ? right : -1; + for (let i = firstVisibleElementInd; i < numViews; i++) { + const view = views[i], + element = view.div; + const currentWidth = element.offsetLeft + element.clientLeft; + const currentHeight = element.offsetTop + element.clientTop; + const viewWidth = element.clientWidth, + viewHeight = element.clientHeight; + const viewRight = currentWidth + viewWidth; + const viewBottom = currentHeight + viewHeight; + if (lastEdge === -1) { + if (viewBottom >= bottom) { + lastEdge = viewBottom; + } + } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) { + break; + } + if (viewBottom <= top || currentHeight >= bottom || viewRight <= left || currentWidth >= right) { + continue; + } + const hiddenHeight = Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom); + const hiddenWidth = Math.max(0, left - currentWidth) + Math.max(0, viewRight - right); + const fractionHeight = (viewHeight - hiddenHeight) / viewHeight, + fractionWidth = (viewWidth - hiddenWidth) / viewWidth; + const percent = fractionHeight * fractionWidth * 100 | 0; + visible.push({ + id: view.id, + x: currentWidth, + y: currentHeight, + view, + percent, + widthPercent: fractionWidth * 100 | 0 + }); + ids.add(view.id); + } + const first = visible[0], + last = visible.at(-1); + if (sortByVisibility) { + visible.sort(function (a, b) { + const pc = a.percent - b.percent; + if (Math.abs(pc) > 0.001) { + return -pc; + } + return a.id - b.id; + }); + } + return { + first, + last, + views: visible, + ids + }; +} +function normalizeWheelEventDirection(evt) { + let delta = Math.hypot(evt.deltaX, evt.deltaY); + const angle = Math.atan2(evt.deltaY, evt.deltaX); + if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) { + delta = -delta; + } + return delta; +} +function normalizeWheelEventDelta(evt) { + const deltaMode = evt.deltaMode; + let delta = normalizeWheelEventDirection(evt); + const MOUSE_PIXELS_PER_LINE = 30; + const MOUSE_LINES_PER_PAGE = 30; + if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE; + } else if (deltaMode === WheelEvent.DOM_DELTA_LINE) { + delta /= MOUSE_LINES_PER_PAGE; + } + return delta; +} +function isValidRotation(angle) { + return Number.isInteger(angle) && angle % 90 === 0; +} +function isValidScrollMode(mode) { + return Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) && mode !== ScrollMode.UNKNOWN; +} +function isValidSpreadMode(mode) { + return Number.isInteger(mode) && Object.values(SpreadMode).includes(mode) && mode !== SpreadMode.UNKNOWN; +} +function isPortraitOrientation(size) { + return size.width <= size.height; +} +const animationStarted = new Promise(function (resolve) { + window.requestAnimationFrame(resolve); +}); +const docStyle = document.documentElement.style; +function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); +} +class ProgressBar { + #classList = null; + #disableAutoFetchTimeout = null; + #percent = 0; + #style = null; + #visible = true; + constructor(bar) { + this.#classList = bar.classList; + this.#style = bar.style; + } + get percent() { + return this.#percent; + } + set percent(val) { + this.#percent = clamp(val, 0, 100); + if (isNaN(val)) { + this.#classList.add("indeterminate"); + return; + } + this.#classList.remove("indeterminate"); + this.#style.setProperty("--progressBar-percent", `${this.#percent}%`); + } + setWidth(viewer) { + if (!viewer) { + return; + } + const container = viewer.parentNode; + const scrollbarWidth = container.offsetWidth - viewer.offsetWidth; + if (scrollbarWidth > 0) { + this.#style.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`); + } + } + setDisableAutoFetch(delay = 5000) { + if (this.#percent === 100 || isNaN(this.#percent)) { + return; + } + if (this.#disableAutoFetchTimeout) { + clearTimeout(this.#disableAutoFetchTimeout); + } + this.show(); + this.#disableAutoFetchTimeout = setTimeout(() => { + this.#disableAutoFetchTimeout = null; + this.hide(); + }, delay); + } + hide() { + if (!this.#visible) { + return; + } + this.#visible = false; + this.#classList.add("hidden"); + } + show() { + if (this.#visible) { + return; + } + this.#visible = true; + this.#classList.remove("hidden"); + } +} +function getActiveOrFocusedElement() { + let curRoot = document; + let curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus"); + while (curActiveOrFocused?.shadowRoot) { + curRoot = curActiveOrFocused.shadowRoot; + curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus"); + } + return curActiveOrFocused; +} +function apiPageLayoutToViewerModes(layout) { + let scrollMode = ScrollMode.VERTICAL, + spreadMode = SpreadMode.NONE; + switch (layout) { + case "SinglePage": + scrollMode = ScrollMode.PAGE; + break; + case "OneColumn": + break; + case "TwoPageLeft": + scrollMode = ScrollMode.PAGE; + case "TwoColumnLeft": + spreadMode = SpreadMode.ODD; + break; + case "TwoPageRight": + scrollMode = ScrollMode.PAGE; + case "TwoColumnRight": + spreadMode = SpreadMode.EVEN; + break; + } + return { + scrollMode, + spreadMode + }; +} +function apiPageModeToSidebarView(mode) { + switch (mode) { + case "UseNone": + return SidebarView.NONE; + case "UseThumbs": + return SidebarView.THUMBS; + case "UseOutlines": + return SidebarView.OUTLINE; + case "UseAttachments": + return SidebarView.ATTACHMENTS; + case "UseOC": + return SidebarView.LAYERS; + } + return SidebarView.NONE; +} +function toggleCheckedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-checked", toggle); + view?.classList.toggle("hidden", !toggle); +} +function toggleExpandedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-expanded", toggle); + view?.classList.toggle("hidden", !toggle); +} +const calcRound = function () { + const e = document.createElement("div"); + e.style.width = "round(down, calc(1.6666666666666665 * 792px), 1px)"; + return e.style.width === "calc(1320px)" ? Math.fround : x => x; +}(); + +;// ./web/pdf_find_utils.js +const CharacterType = { + SPACE: 0, + ALPHA_LETTER: 1, + PUNCT: 2, + HAN_LETTER: 3, + KATAKANA_LETTER: 4, + HIRAGANA_LETTER: 5, + HALFWIDTH_KATAKANA_LETTER: 6, + THAI_LETTER: 7 +}; +function isAlphabeticalScript(charCode) { + return charCode < 0x2e80; +} +function isAscii(charCode) { + return (charCode & 0xff80) === 0; +} +function isAsciiAlpha(charCode) { + return charCode >= 0x61 && charCode <= 0x7a || charCode >= 0x41 && charCode <= 0x5a; +} +function isAsciiDigit(charCode) { + return charCode >= 0x30 && charCode <= 0x39; +} +function isAsciiSpace(charCode) { + return charCode === 0x20 || charCode === 0x09 || charCode === 0x0d || charCode === 0x0a; +} +function isHan(charCode) { + return charCode >= 0x3400 && charCode <= 0x9fff || charCode >= 0xf900 && charCode <= 0xfaff; +} +function isKatakana(charCode) { + return charCode >= 0x30a0 && charCode <= 0x30ff; +} +function isHiragana(charCode) { + return charCode >= 0x3040 && charCode <= 0x309f; +} +function isHalfwidthKatakana(charCode) { + return charCode >= 0xff60 && charCode <= 0xff9f; +} +function isThai(charCode) { + return (charCode & 0xff80) === 0x0e00; +} +function getCharacterType(charCode) { + if (isAlphabeticalScript(charCode)) { + if (isAscii(charCode)) { + if (isAsciiSpace(charCode)) { + return CharacterType.SPACE; + } else if (isAsciiAlpha(charCode) || isAsciiDigit(charCode) || charCode === 0x5f) { + return CharacterType.ALPHA_LETTER; + } + return CharacterType.PUNCT; + } else if (isThai(charCode)) { + return CharacterType.THAI_LETTER; + } else if (charCode === 0xa0) { + return CharacterType.SPACE; + } + return CharacterType.ALPHA_LETTER; + } + if (isHan(charCode)) { + return CharacterType.HAN_LETTER; + } else if (isKatakana(charCode)) { + return CharacterType.KATAKANA_LETTER; + } else if (isHiragana(charCode)) { + return CharacterType.HIRAGANA_LETTER; + } else if (isHalfwidthKatakana(charCode)) { + return CharacterType.HALFWIDTH_KATAKANA_LETTER; + } + return CharacterType.ALPHA_LETTER; +} +let NormalizeWithNFKC; +function getNormalizeWithNFKC() { + NormalizeWithNFKC ||= ` ¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰꟲ-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`; + return NormalizeWithNFKC; +} + +;// ./web/pdf_find_controller.js + + +const FindState = { + FOUND: 0, + NOT_FOUND: 1, + WRAPPED: 2, + PENDING: 3 +}; +const FIND_TIMEOUT = 250; +const MATCH_SCROLL_OFFSET_TOP = -50; +const MATCH_SCROLL_OFFSET_LEFT = -400; +const CHARACTERS_TO_NORMALIZE = { + "\u2010": "-", + "\u2018": "'", + "\u2019": "'", + "\u201A": "'", + "\u201B": "'", + "\u201C": '"', + "\u201D": '"', + "\u201E": '"', + "\u201F": '"', + "\u00BC": "1/4", + "\u00BD": "1/2", + "\u00BE": "3/4" +}; +const DIACRITICS_EXCEPTION = new Set([0x3099, 0x309a, 0x094d, 0x09cd, 0x0a4d, 0x0acd, 0x0b4d, 0x0bcd, 0x0c4d, 0x0ccd, 0x0d3b, 0x0d3c, 0x0d4d, 0x0dca, 0x0e3a, 0x0eba, 0x0f84, 0x1039, 0x103a, 0x1714, 0x1734, 0x17d2, 0x1a60, 0x1b44, 0x1baa, 0x1bab, 0x1bf2, 0x1bf3, 0x2d7f, 0xa806, 0xa82c, 0xa8c4, 0xa953, 0xa9c0, 0xaaf6, 0xabed, 0x0c56, 0x0f71, 0x0f72, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f80, 0x0f74]); +let DIACRITICS_EXCEPTION_STR; +const DIACRITICS_REG_EXP = /\p{M}+/gu; +const SPECIAL_CHARS_REG_EXP = /([.*+?^${}()|[\]\\])|(\p{P})|(\s+)|(\p{M})|(\p{L})/gu; +const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u; +const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u; +const SYLLABLES_REG_EXP = /[\uAC00-\uD7AF\uFA6C\uFACF-\uFAD1\uFAD5-\uFAD7]+/g; +const SYLLABLES_LENGTHS = new Map(); +const FIRST_CHAR_SYLLABLES_REG_EXP = "[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]"; +const NFKC_CHARS_TO_NORMALIZE = new Map(); +let noSyllablesRegExp = null; +let withSyllablesRegExp = null; +function normalize(text) { + const syllablePositions = []; + let m; + while ((m = SYLLABLES_REG_EXP.exec(text)) !== null) { + let { + index + } = m; + for (const char of m[0]) { + let len = SYLLABLES_LENGTHS.get(char); + if (!len) { + len = char.normalize("NFD").length; + SYLLABLES_LENGTHS.set(char, len); + } + syllablePositions.push([len, index++]); + } + } + let normalizationRegex; + if (syllablePositions.length === 0 && noSyllablesRegExp) { + normalizationRegex = noSyllablesRegExp; + } else if (syllablePositions.length > 0 && withSyllablesRegExp) { + normalizationRegex = withSyllablesRegExp; + } else { + const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); + const toNormalizeWithNFKC = getNormalizeWithNFKC(); + const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])"; + const HKDiacritics = "(?:\u3099|\u309A)"; + const CompoundWord = "\\p{Ll}-\\n\\p{Lu}"; + const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(${CompoundWord})|(\\S-\\n)|(${CJK}\\n)|(\\n)`; + if (syllablePositions.length === 0) { + normalizationRegex = noSyllablesRegExp = new RegExp(regexp + "|(\\u0000)", "gum"); + } else { + normalizationRegex = withSyllablesRegExp = new RegExp(regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`, "gum"); + } + } + const rawDiacriticsPositions = []; + while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) { + rawDiacriticsPositions.push([m[0].length, m.index]); + } + let normalized = text.normalize("NFD"); + const positions = [0, 0]; + let rawDiacriticsIndex = 0; + let syllableIndex = 0; + let shift = 0; + let shiftOrigin = 0; + let eol = 0; + let hasDiacritics = false; + normalized = normalized.replace(normalizationRegex, (match, p1, p2, p3, p4, p5, p6, p7, p8, p9, i) => { + i -= shiftOrigin; + if (p1) { + const replacement = CHARACTERS_TO_NORMALIZE[p1]; + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p2) { + let replacement = NFKC_CHARS_TO_NORMALIZE.get(p2); + if (!replacement) { + replacement = p2.normalize("NFKC"); + NFKC_CHARS_TO_NORMALIZE.set(p2, replacement); + } + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p3) { + hasDiacritics = true; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + ++rawDiacriticsIndex; + } else { + positions.push(i - 1 - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + } + positions.push(i - shift + 1, shift); + shiftOrigin += 1; + eol += 1; + return p3.charAt(0); + } + if (p4) { + const hasTrailingDashEOL = p4.endsWith("\n"); + const len = hasTrailingDashEOL ? p4.length - 2 : p4.length; + hasDiacritics = true; + let jj = len; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + jj -= rawDiacriticsPositions[rawDiacriticsIndex][0]; + ++rawDiacriticsIndex; + } + for (let j = 1; j <= jj; j++) { + positions.push(i - 1 - shift + j, shift - j); + } + shift -= jj; + shiftOrigin += jj; + if (hasTrailingDashEOL) { + i += len - 1; + positions.push(i - shift + 1, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p4.slice(0, len); + } + return p4; + } + if (p5) { + shiftOrigin += 1; + eol += 1; + return p5.replace("\n", ""); + } + if (p6) { + const len = p6.length - 2; + positions.push(i - shift + len, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p6.slice(0, -2); + } + if (p7) { + const len = p7.length - 1; + positions.push(i - shift + len, shift); + shiftOrigin += 1; + eol += 1; + return p7.slice(0, -1); + } + if (p8) { + positions.push(i - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + eol += 1; + return " "; + } + if (i + eol === syllablePositions[syllableIndex]?.[1]) { + const newCharLen = syllablePositions[syllableIndex][0] - 1; + ++syllableIndex; + for (let j = 1; j <= newCharLen; j++) { + positions.push(i - (shift - j), shift - j); + } + shift -= newCharLen; + shiftOrigin += newCharLen; + } + return p9; + }); + positions.push(normalized.length, shift); + const starts = new Uint32Array(positions.length >> 1); + const shifts = new Int32Array(positions.length >> 1); + for (let i = 0, ii = positions.length; i < ii; i += 2) { + starts[i >> 1] = positions[i]; + shifts[i >> 1] = positions[i + 1]; + } + return [normalized, [starts, shifts], hasDiacritics]; +} +function getOriginalIndex(diffs, pos, len) { + if (!diffs) { + return [pos, len]; + } + const [starts, shifts] = diffs; + const start = pos; + const end = pos + len - 1; + let i = binarySearchFirstItem(starts, x => x >= start); + if (starts[i] > start) { + --i; + } + let j = binarySearchFirstItem(starts, x => x >= end, i); + if (starts[j] > end) { + --j; + } + const oldStart = start + shifts[i]; + const oldEnd = end + shifts[j]; + const oldLen = oldEnd + 1 - oldStart; + return [oldStart, oldLen]; +} +class PDFFindController { + #state = null; + #updateMatchesCountOnProgress = true; + #visitedPagesCount = 0; + constructor({ + linkService, + eventBus, + updateMatchesCountOnProgress = true + }) { + this._linkService = linkService; + this._eventBus = eventBus; + this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress; + this.onIsPageVisible = null; + this.#reset(); + eventBus._on("find", this.#onFind.bind(this)); + eventBus._on("findbarclose", this.#onFindBarClose.bind(this)); + } + get highlightMatches() { + return this._highlightMatches; + } + get pageMatches() { + return this._pageMatches; + } + get pageMatchesLength() { + return this._pageMatchesLength; + } + get selected() { + return this._selected; + } + get state() { + return this.#state; + } + setDocument(pdfDocument) { + if (this._pdfDocument) { + this.#reset(); + } + if (!pdfDocument) { + return; + } + this._pdfDocument = pdfDocument; + this._firstPageCapability.resolve(); + } + #onFind(state) { + if (!state) { + return; + } + const pdfDocument = this._pdfDocument; + const { + type + } = state; + if (this.#state === null || this.#shouldDirtyMatch(state)) { + this._dirtyMatch = true; + } + this.#state = state; + if (type !== "highlightallchange") { + this.#updateUIState(FindState.PENDING); + } + this._firstPageCapability.promise.then(() => { + if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { + return; + } + this.#extractText(); + const findbarClosed = !this._highlightMatches; + const pendingTimeout = !!this._findTimeout; + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (!type) { + this._findTimeout = setTimeout(() => { + this.#nextMatch(); + this._findTimeout = null; + }, FIND_TIMEOUT); + } else if (this._dirtyMatch) { + this.#nextMatch(); + } else if (type === "again") { + this.#nextMatch(); + if (findbarClosed && this.#state.highlightAll) { + this.#updateAllPages(); + } + } else if (type === "highlightallchange") { + if (pendingTimeout) { + this.#nextMatch(); + } else { + this._highlightMatches = true; + } + this.#updateAllPages(); + } else { + this.#nextMatch(); + } + }); + } + scrollMatchIntoView({ + element = null, + selectedLeft = 0, + pageIndex = -1, + matchIndex = -1 + }) { + if (!this._scrollMatches || !element) { + return; + } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { + return; + } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) { + return; + } + this._scrollMatches = false; + const spot = { + top: MATCH_SCROLL_OFFSET_TOP, + left: selectedLeft + MATCH_SCROLL_OFFSET_LEFT + }; + scrollIntoView(element, spot, true); + } + #reset() { + this._highlightMatches = false; + this._scrollMatches = false; + this._pdfDocument = null; + this._pageMatches = []; + this._pageMatchesLength = []; + this.#visitedPagesCount = 0; + this.#state = null; + this._selected = { + pageIdx: -1, + matchIdx: -1 + }; + this._offset = { + pageIdx: null, + matchIdx: null, + wrapped: false + }; + this._extractTextPromises = []; + this._pageContents = []; + this._pageDiffs = []; + this._hasDiacritics = []; + this._matchesCountTotal = 0; + this._pagesToSearch = null; + this._pendingFindMatches = new Set(); + this._resumePageIdx = null; + this._dirtyMatch = false; + clearTimeout(this._findTimeout); + this._findTimeout = null; + this._firstPageCapability = Promise.withResolvers(); + } + get #query() { + const { + query + } = this.#state; + if (typeof query === "string") { + if (query !== this._rawQuery) { + this._rawQuery = query; + [this._normalizedQuery] = normalize(query); + } + return this._normalizedQuery; + } + return (query || []).filter(q => !!q).map(q => normalize(q)[0]); + } + #shouldDirtyMatch(state) { + const newQuery = state.query, + prevQuery = this.#state.query; + const newType = typeof newQuery, + prevType = typeof prevQuery; + if (newType !== prevType) { + return true; + } + if (newType === "string") { + if (newQuery !== prevQuery) { + return true; + } + } else if (JSON.stringify(newQuery) !== JSON.stringify(prevQuery)) { + return true; + } + switch (state.type) { + case "again": + const pageNumber = this._selected.pageIdx + 1; + const linkService = this._linkService; + return pageNumber >= 1 && pageNumber <= linkService.pagesCount && pageNumber !== linkService.page && !(this.onIsPageVisible?.(pageNumber) ?? true); + case "highlightallchange": + return false; + } + return true; + } + #isEntireWord(content, startIdx, length) { + let match = content.slice(0, startIdx).match(NOT_DIACRITIC_FROM_END_REG_EXP); + if (match) { + const first = content.charCodeAt(startIdx); + const limit = match[1].charCodeAt(0); + if (getCharacterType(first) === getCharacterType(limit)) { + return false; + } + } + match = content.slice(startIdx + length).match(NOT_DIACRITIC_FROM_START_REG_EXP); + if (match) { + const last = content.charCodeAt(startIdx + length - 1); + const limit = match[1].charCodeAt(0); + if (getCharacterType(last) === getCharacterType(limit)) { + return false; + } + } + return true; + } + #convertToRegExpString(query, hasDiacritics) { + const { + matchDiacritics + } = this.#state; + let isUnicode = false; + query = query.replaceAll(SPECIAL_CHARS_REG_EXP, (match, p1, p2, p3, p4, p5) => { + if (p1) { + return `[ ]*\\${p1}[ ]*`; + } + if (p2) { + return `[ ]*${p2}[ ]*`; + } + if (p3) { + return "[ ]+"; + } + if (matchDiacritics) { + return p4 || p5; + } + if (p4) { + return DIACRITICS_EXCEPTION.has(p4.charCodeAt(0)) ? p4 : ""; + } + if (hasDiacritics) { + isUnicode = true; + return `${p5}\\p{M}*`; + } + return p5; + }); + const trailingSpaces = "[ ]*"; + if (query.endsWith(trailingSpaces)) { + query = query.slice(0, query.length - trailingSpaces.length); + } + if (matchDiacritics) { + if (hasDiacritics) { + DIACRITICS_EXCEPTION_STR ||= String.fromCharCode(...DIACRITICS_EXCEPTION); + isUnicode = true; + query = `${query}(?=[${DIACRITICS_EXCEPTION_STR}]|[^\\p{M}]|$)`; + } + } + return [isUnicode, query]; + } + #calculateMatch(pageIndex) { + const query = this.#query; + if (query.length === 0) { + return; + } + const pageContent = this._pageContents[pageIndex]; + const matcherResult = this.match(query, pageContent, pageIndex); + const matches = this._pageMatches[pageIndex] = []; + const matchesLength = this._pageMatchesLength[pageIndex] = []; + const diffs = this._pageDiffs[pageIndex]; + matcherResult?.forEach(({ + index, + length + }) => { + const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); + if (matchLen) { + matches.push(matchPos); + matchesLength.push(matchLen); + } + }); + if (this.#state.highlightAll) { + this.#updatePage(pageIndex); + } + if (this._resumePageIdx === pageIndex) { + this._resumePageIdx = null; + this.#nextPageMatch(); + } + const pageMatchesCount = matches.length; + this._matchesCountTotal += pageMatchesCount; + if (this.#updateMatchesCountOnProgress) { + if (pageMatchesCount > 0) { + this.#updateUIResultsCount(); + } + } else if (++this.#visitedPagesCount === this._linkService.pagesCount) { + this.#updateUIResultsCount(); + } + } + match(query, pageContent, pageIndex) { + const hasDiacritics = this._hasDiacritics[pageIndex]; + let isUnicode = false; + if (typeof query === "string") { + [isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics); + } else { + query = query.sort().reverse().map(q => { + const [isUnicodePart, queryPart] = this.#convertToRegExpString(q, hasDiacritics); + isUnicode ||= isUnicodePart; + return `(${queryPart})`; + }).join("|"); + } + if (!query) { + return undefined; + } + const { + caseSensitive, + entireWord + } = this.#state; + const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`; + query = new RegExp(query, flags); + const matches = []; + let match; + while ((match = query.exec(pageContent)) !== null) { + if (entireWord && !this.#isEntireWord(pageContent, match.index, match[0].length)) { + continue; + } + matches.push({ + index: match.index, + length: match[0].length + }); + } + return matches; + } + #extractText() { + if (this._extractTextPromises.length > 0) { + return; + } + let deferred = Promise.resolve(); + const textOptions = { + disableNormalization: true + }; + for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { + const { + promise, + resolve + } = Promise.withResolvers(); + this._extractTextPromises[i] = promise; + deferred = deferred.then(() => { + return this._pdfDocument.getPage(i + 1).then(pdfPage => pdfPage.getTextContent(textOptions)).then(textContent => { + const strBuf = []; + for (const textItem of textContent.items) { + strBuf.push(textItem.str); + if (textItem.hasEOL) { + strBuf.push("\n"); + } + } + [this._pageContents[i], this._pageDiffs[i], this._hasDiacritics[i]] = normalize(strBuf.join("")); + resolve(); + }, reason => { + console.error(`Unable to get text content for page ${i + 1}`, reason); + this._pageContents[i] = ""; + this._pageDiffs[i] = null; + this._hasDiacritics[i] = false; + resolve(); + }); + }); + } + } + #updatePage(index) { + if (this._scrollMatches && this._selected.pageIdx === index) { + this._linkService.page = index + 1; + } + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: index + }); + } + #updateAllPages() { + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: -1 + }); + } + #nextMatch() { + const previous = this.#state.findPrevious; + const currentPageIndex = this._linkService.page - 1; + const numPages = this._linkService.pagesCount; + this._highlightMatches = true; + if (this._dirtyMatch) { + this._dirtyMatch = false; + this._selected.pageIdx = this._selected.matchIdx = -1; + this._offset.pageIdx = currentPageIndex; + this._offset.matchIdx = null; + this._offset.wrapped = false; + this._resumePageIdx = null; + this._pageMatches.length = 0; + this._pageMatchesLength.length = 0; + this.#visitedPagesCount = 0; + this._matchesCountTotal = 0; + this.#updateAllPages(); + for (let i = 0; i < numPages; i++) { + if (this._pendingFindMatches.has(i)) { + continue; + } + this._pendingFindMatches.add(i); + this._extractTextPromises[i].then(() => { + this._pendingFindMatches.delete(i); + this.#calculateMatch(i); + }); + } + } + const query = this.#query; + if (query.length === 0) { + this.#updateUIState(FindState.FOUND); + return; + } + if (this._resumePageIdx) { + return; + } + const offset = this._offset; + this._pagesToSearch = numPages; + if (offset.matchIdx !== null) { + const numPageMatches = this._pageMatches[offset.pageIdx].length; + if (!previous && offset.matchIdx + 1 < numPageMatches || previous && offset.matchIdx > 0) { + offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; + this.#updateMatch(true); + return; + } + this.#advanceOffsetPage(previous); + } + this.#nextPageMatch(); + } + #matchesReady(matches) { + const offset = this._offset; + const numMatches = matches.length; + const previous = this.#state.findPrevious; + if (numMatches) { + offset.matchIdx = previous ? numMatches - 1 : 0; + this.#updateMatch(true); + return true; + } + this.#advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (this._pagesToSearch < 0) { + this.#updateMatch(false); + return true; + } + } + return false; + } + #nextPageMatch() { + if (this._resumePageIdx !== null) { + console.error("There can only be one pending page."); + } + let matches = null; + do { + const pageIdx = this._offset.pageIdx; + matches = this._pageMatches[pageIdx]; + if (!matches) { + this._resumePageIdx = pageIdx; + break; + } + } while (!this.#matchesReady(matches)); + } + #advanceOffsetPage(previous) { + const offset = this._offset; + const numPages = this._linkService.pagesCount; + offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; + offset.matchIdx = null; + this._pagesToSearch--; + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = previous ? numPages - 1 : 0; + offset.wrapped = true; + } + } + #updateMatch(found = false) { + let state = FindState.NOT_FOUND; + const wrapped = this._offset.wrapped; + this._offset.wrapped = false; + if (found) { + const previousPage = this._selected.pageIdx; + this._selected.pageIdx = this._offset.pageIdx; + this._selected.matchIdx = this._offset.matchIdx; + state = wrapped ? FindState.WRAPPED : FindState.FOUND; + if (previousPage !== -1 && previousPage !== this._selected.pageIdx) { + this.#updatePage(previousPage); + } + } + this.#updateUIState(state, this.#state.findPrevious); + if (this._selected.pageIdx !== -1) { + this._scrollMatches = true; + this.#updatePage(this._selected.pageIdx); + } + } + #onFindBarClose(evt) { + const pdfDocument = this._pdfDocument; + this._firstPageCapability.promise.then(() => { + if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { + return; + } + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (this._resumePageIdx) { + this._resumePageIdx = null; + this._dirtyMatch = true; + } + this.#updateUIState(FindState.FOUND); + this._highlightMatches = false; + this.#updateAllPages(); + }); + } + #requestMatchesCount() { + const { + pageIdx, + matchIdx + } = this._selected; + let current = 0, + total = this._matchesCountTotal; + if (matchIdx !== -1) { + for (let i = 0; i < pageIdx; i++) { + current += this._pageMatches[i]?.length || 0; + } + current += matchIdx + 1; + } + if (current < 1 || current > total) { + current = total = 0; + } + return { + current, + total + }; + } + #updateUIResultsCount() { + this._eventBus.dispatch("updatefindmatchescount", { + source: this, + matchesCount: this.#requestMatchesCount() + }); + } + #updateUIState(state, previous = false) { + if (!this.#updateMatchesCountOnProgress && (this.#visitedPagesCount !== this._linkService.pagesCount || state === FindState.PENDING)) { + return; + } + this._eventBus.dispatch("updatefindcontrolstate", { + source: this, + state, + previous, + entireWord: this.#state?.entireWord ?? null, + matchesCount: this.#requestMatchesCount(), + rawQuery: this.#state?.query ?? null + }); + } +} + +;// ./web/pdf_link_service.js + +const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; +const LinkTarget = { + NONE: 0, + SELF: 1, + BLANK: 2, + PARENT: 3, + TOP: 4 +}; +class PDFLinkService { + externalLinkEnabled = true; + constructor({ + eventBus, + externalLinkTarget = null, + externalLinkRel = null, + ignoreDestinationZoom = false + } = {}) { + this.eventBus = eventBus; + this.externalLinkTarget = externalLinkTarget; + this.externalLinkRel = externalLinkRel; + this._ignoreDestinationZoom = ignoreDestinationZoom; + this.baseUrl = null; + this.pdfDocument = null; + this.pdfViewer = null; + this.pdfHistory = null; + } + setDocument(pdfDocument, baseUrl = null) { + this.baseUrl = baseUrl; + this.pdfDocument = pdfDocument; + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setHistory(pdfHistory) { + this.pdfHistory = pdfHistory; + } + get pagesCount() { + return this.pdfDocument ? this.pdfDocument.numPages : 0; + } + get page() { + return this.pdfDocument ? this.pdfViewer.currentPageNumber : 1; + } + set page(value) { + if (this.pdfDocument) { + this.pdfViewer.currentPageNumber = value; + } + } + get rotation() { + return this.pdfDocument ? this.pdfViewer.pagesRotation : 0; + } + set rotation(value) { + if (this.pdfDocument) { + this.pdfViewer.pagesRotation = value; + } + } + get isInPresentationMode() { + return this.pdfDocument ? this.pdfViewer.isInPresentationMode : false; + } + async goToDestination(dest) { + if (!this.pdfDocument) { + return; + } + let namedDest, explicitDest, pageNumber; + if (typeof dest === "string") { + namedDest = dest; + explicitDest = await this.pdfDocument.getDestination(dest); + } else { + namedDest = null; + explicitDest = await dest; + } + if (!Array.isArray(explicitDest)) { + console.error(`goToDestination: "${explicitDest}" is not a valid destination array, for dest="${dest}".`); + return; + } + const [destRef] = explicitDest; + if (destRef && typeof destRef === "object") { + pageNumber = this.pdfDocument.cachedPageNumber(destRef); + if (!pageNumber) { + try { + pageNumber = (await this.pdfDocument.getPageIndex(destRef)) + 1; + } catch { + console.error(`goToDestination: "${destRef}" is not a valid page reference, for dest="${dest}".`); + return; + } + } + } else if (Number.isInteger(destRef)) { + pageNumber = destRef + 1; + } + if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) { + console.error(`goToDestination: "${pageNumber}" is not a valid page number, for dest="${dest}".`); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.push({ + namedDest, + explicitDest, + pageNumber + }); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber, + destArray: explicitDest, + ignoreDestinationZoom: this._ignoreDestinationZoom + }); + } + goToPage(val) { + if (!this.pdfDocument) { + return; + } + const pageNumber = typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val) || val | 0; + if (!(Number.isInteger(pageNumber) && pageNumber > 0 && pageNumber <= this.pagesCount)) { + console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.pushPage(pageNumber); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber + }); + } + addLinkAttributes(link, url, newWindow = false) { + if (!url || typeof url !== "string") { + throw new Error('A valid "url" parameter must provided.'); + } + const target = newWindow ? LinkTarget.BLANK : this.externalLinkTarget, + rel = this.externalLinkRel; + if (this.externalLinkEnabled) { + link.href = link.title = url; + } else { + link.href = ""; + link.title = `Disabled: ${url}`; + link.onclick = () => false; + } + let targetStr = ""; + switch (target) { + case LinkTarget.NONE: + break; + case LinkTarget.SELF: + targetStr = "_self"; + break; + case LinkTarget.BLANK: + targetStr = "_blank"; + break; + case LinkTarget.PARENT: + targetStr = "_parent"; + break; + case LinkTarget.TOP: + targetStr = "_top"; + break; + } + link.target = targetStr; + link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL; + } + getDestinationHash(dest) { + if (typeof dest === "string") { + if (dest.length > 0) { + return this.getAnchorUrl("#" + escape(dest)); + } + } else if (Array.isArray(dest)) { + const str = JSON.stringify(dest); + if (str.length > 0) { + return this.getAnchorUrl("#" + escape(str)); + } + } + return this.getAnchorUrl(""); + } + getAnchorUrl(anchor) { + return this.baseUrl ? this.baseUrl + anchor : anchor; + } + setHash(hash) { + if (!this.pdfDocument) { + return; + } + let pageNumber, dest; + if (hash.includes("=")) { + const params = parseQueryString(hash); + if (params.has("search")) { + const query = params.get("search").replaceAll('"', ""), + phrase = params.get("phrase") === "true"; + this.eventBus.dispatch("findfromurlhash", { + source: this, + query: phrase ? query : query.match(/\S+/g) + }); + } + if (params.has("page")) { + pageNumber = params.get("page") | 0 || 1; + } + if (params.has("zoom")) { + const zoomArgs = params.get("zoom").split(","); + const zoomArg = zoomArgs[0]; + const zoomArgNumber = parseFloat(zoomArg); + if (!zoomArg.includes("Fit")) { + dest = [null, { + name: "XYZ" + }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null, zoomArgs.length > 2 ? zoomArgs[2] | 0 : null, zoomArgNumber ? zoomArgNumber / 100 : zoomArg]; + } else if (zoomArg === "Fit" || zoomArg === "FitB") { + dest = [null, { + name: zoomArg + }]; + } else if (zoomArg === "FitH" || zoomArg === "FitBH" || zoomArg === "FitV" || zoomArg === "FitBV") { + dest = [null, { + name: zoomArg + }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null]; + } else if (zoomArg === "FitR") { + if (zoomArgs.length !== 5) { + console.error('PDFLinkService.setHash: Not enough parameters for "FitR".'); + } else { + dest = [null, { + name: zoomArg + }, zoomArgs[1] | 0, zoomArgs[2] | 0, zoomArgs[3] | 0, zoomArgs[4] | 0]; + } + } else { + console.error(`PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`); + } + } + if (dest) { + this.pdfViewer.scrollPageIntoView({ + pageNumber: pageNumber || this.page, + destArray: dest, + allowNegativeOffset: true + }); + } else if (pageNumber) { + this.page = pageNumber; + } + if (params.has("pagemode")) { + this.eventBus.dispatch("pagemode", { + source: this, + mode: params.get("pagemode") + }); + } + if (params.has("nameddest")) { + this.goToDestination(params.get("nameddest")); + } + return; + } + dest = unescape(hash); + try { + dest = JSON.parse(dest); + if (!Array.isArray(dest)) { + dest = dest.toString(); + } + } catch {} + if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) { + this.goToDestination(dest); + return; + } + console.error(`PDFLinkService.setHash: "${unescape(hash)}" is not a valid destination.`); + } + executeNamedAction(action) { + if (!this.pdfDocument) { + return; + } + switch (action) { + case "GoBack": + this.pdfHistory?.back(); + break; + case "GoForward": + this.pdfHistory?.forward(); + break; + case "NextPage": + this.pdfViewer.nextPage(); + break; + case "PrevPage": + this.pdfViewer.previousPage(); + break; + case "LastPage": + this.page = this.pagesCount; + break; + case "FirstPage": + this.page = 1; + break; + default: + break; + } + this.eventBus.dispatch("namedaction", { + source: this, + action + }); + } + async executeSetOCGState(action) { + if (!this.pdfDocument) { + return; + } + const pdfDocument = this.pdfDocument, + optionalContentConfig = await this.pdfViewer.optionalContentConfigPromise; + if (pdfDocument !== this.pdfDocument) { + return; + } + optionalContentConfig.setOCGState(action); + this.pdfViewer.optionalContentConfigPromise = Promise.resolve(optionalContentConfig); + } + static #isValidExplicitDest(dest) { + if (!Array.isArray(dest) || dest.length < 2) { + return false; + } + const [page, zoom, ...args] = dest; + if (!(typeof page === "object" && Number.isInteger(page?.num) && Number.isInteger(page?.gen)) && !Number.isInteger(page)) { + return false; + } + if (!(typeof zoom === "object" && typeof zoom?.name === "string")) { + return false; + } + const argsLen = args.length; + let allowNull = true; + switch (zoom.name) { + case "XYZ": + if (argsLen < 2 || argsLen > 3) { + return false; + } + break; + case "Fit": + case "FitB": + return argsLen === 0; + case "FitH": + case "FitBH": + case "FitV": + case "FitBV": + if (argsLen > 1) { + return false; + } + break; + case "FitR": + if (argsLen !== 4) { + return false; + } + allowNull = false; + break; + default: + return false; + } + for (const arg of args) { + if (!(typeof arg === "number" || allowNull && arg === null)) { + return false; + } + } + return true; + } +} +class SimpleLinkService extends PDFLinkService { + setDocument(pdfDocument, baseUrl = null) {} +} + +;// ./web/pdfjs.js +const { + AbortException, + AnnotationEditorLayer, + AnnotationEditorParamsType, + AnnotationEditorType, + AnnotationEditorUIManager, + AnnotationLayer, + AnnotationMode, + build, + ColorPicker, + createValidAbsoluteUrl, + DOMSVGFactory, + DrawLayer, + FeatureTest, + fetchData, + getDocument, + getFilenameFromUrl, + getPdfFilenameFromUrl, + getXfaPageViewport, + GlobalWorkerOptions, + ImageKind, + InvalidPDFException, + isDataScheme, + isPdfFile, + MissingPDFException, + noContextMenu, + normalizeUnicode, + OPS, + OutputScale, + PasswordResponses, + PDFDataRangeTransport, + PDFDateString, + PDFWorker, + PermissionFlag, + PixelsPerInch, + RenderingCancelledException, + setLayerDimensions, + shadow, + stopEvent, + TextLayer, + TouchManager, + UnexpectedResponseException, + Util, + VerbosityLevel, + version, + XfaLayer +} = globalThis.pdfjsLib; + +;// ./web/annotation_layer_builder.js + + +class AnnotationLayerBuilder { + #onAppend = null; + #eventAbortController = null; + constructor({ + pdfPage, + linkService, + downloadManager, + annotationStorage = null, + imageResourcesPath = "", + renderForms = true, + enableScripting = false, + hasJSActionsPromise = null, + fieldObjectsPromise = null, + annotationCanvasMap = null, + accessibilityManager = null, + annotationEditorUIManager = null, + onAppend = null + }) { + this.pdfPage = pdfPage; + this.linkService = linkService; + this.downloadManager = downloadManager; + this.imageResourcesPath = imageResourcesPath; + this.renderForms = renderForms; + this.annotationStorage = annotationStorage; + this.enableScripting = enableScripting; + this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false); + this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null); + this._annotationCanvasMap = annotationCanvasMap; + this._accessibilityManager = accessibilityManager; + this._annotationEditorUIManager = annotationEditorUIManager; + this.#onAppend = onAppend; + this.annotationLayer = null; + this.div = null; + this._cancelled = false; + this._eventBus = linkService.eventBus; + } + async render(viewport, options, intent = "display") { + if (this.div) { + if (this._cancelled || !this.annotationLayer) { + return; + } + this.annotationLayer.update({ + viewport: viewport.clone({ + dontFlip: true + }) + }); + return; + } + const [annotations, hasJSActions, fieldObjects] = await Promise.all([this.pdfPage.getAnnotations({ + intent + }), this._hasJSActionsPromise, this._fieldObjectsPromise]); + if (this._cancelled) { + return; + } + const div = this.div = document.createElement("div"); + div.className = "annotationLayer"; + this.#onAppend?.(div); + if (annotations.length === 0) { + this.hide(); + return; + } + this.annotationLayer = new AnnotationLayer({ + div, + accessibilityManager: this._accessibilityManager, + annotationCanvasMap: this._annotationCanvasMap, + annotationEditorUIManager: this._annotationEditorUIManager, + page: this.pdfPage, + viewport: viewport.clone({ + dontFlip: true + }), + structTreeLayer: options?.structTreeLayer || null + }); + await this.annotationLayer.render({ + annotations, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.renderForms, + linkService: this.linkService, + downloadManager: this.downloadManager, + annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, + hasJSActions, + fieldObjects + }); + if (this.linkService.isInPresentationMode) { + this.#updatePresentationModeState(PresentationModeState.FULLSCREEN); + } + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this._eventBus?._on("presentationmodechanged", evt => { + this.#updatePresentationModeState(evt.state); + }, { + signal: this.#eventAbortController.signal + }); + } + } + cancel() { + this._cancelled = true; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { + if (!this.div) { + return; + } + let disableFormElements = false; + switch (state) { + case PresentationModeState.FULLSCREEN: + disableFormElements = true; + break; + case PresentationModeState.NORMAL: + break; + default: + return; + } + for (const section of this.div.childNodes) { + if (section.hasAttribute("data-internal-link")) { + continue; + } + section.inert = disableFormElements; + } + } +} + +;// ./web/download_manager.js + +function download(blobUrl, filename) { + const a = document.createElement("a"); + if (!a.click) { + throw new Error('DownloadManager: "a.click()" is not supported.'); + } + a.href = blobUrl; + a.target = "_parent"; + if ("download" in a) { + a.download = filename; + } + (document.body || document.documentElement).append(a); + a.click(); + a.remove(); +} +class DownloadManager { + #openBlobUrls = new WeakMap(); + downloadData(data, filename, contentType) { + const blobUrl = URL.createObjectURL(new Blob([data], { + type: contentType + })); + download(blobUrl, filename); + } + openOrDownloadData(data, filename, dest = null) { + const isPdfData = isPdfFile(filename); + const contentType = isPdfData ? "application/pdf" : ""; + this.downloadData(data, filename, contentType); + return false; + } + download(data, url, filename) { + let blobUrl; + if (data) { + blobUrl = URL.createObjectURL(new Blob([data], { + type: "application/pdf" + })); + } else { + if (!createValidAbsoluteUrl(url, "http://example.com")) { + console.error(`download - not a valid URL: ${url}`); + return; + } + blobUrl = url + "#pdfjs.action=download"; + } + download(blobUrl, filename); + } +} + +;// ./web/event_utils.js +const WaitOnType = { + EVENT: "event", + TIMEOUT: "timeout" +}; +async function waitOnEventOrTimeout({ + target, + name, + delay = 0 +}) { + if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { + throw new Error("waitOnEventOrTimeout - invalid parameters."); + } + const { + promise, + resolve + } = Promise.withResolvers(); + const ac = new AbortController(); + function handler(type) { + ac.abort(); + clearTimeout(timeout); + resolve(type); + } + const evtMethod = target instanceof EventBus ? "_on" : "addEventListener"; + target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), { + signal: ac.signal + }); + const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay); + return promise; +} +class EventBus { + #listeners = Object.create(null); + on(eventName, listener, options = null) { + this._on(eventName, listener, { + external: true, + once: options?.once, + signal: options?.signal + }); + } + off(eventName, listener, options = null) { + this._off(eventName, listener); + } + dispatch(eventName, data) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; + } + let externalListeners; + for (const { + listener, + external, + once + } of eventListeners.slice(0)) { + if (once) { + this._off(eventName, listener); + } + if (external) { + (externalListeners ||= []).push(listener); + continue; + } + listener(data); + } + if (externalListeners) { + for (const listener of externalListeners) { + listener(data); + } + externalListeners = null; + } + } + _on(eventName, listener, options = null) { + let rmAbort = null; + if (options?.signal instanceof AbortSignal) { + const { + signal + } = options; + if (signal.aborted) { + console.error("Cannot use an `aborted` signal."); + return; + } + const onAbort = () => this._off(eventName, listener); + rmAbort = () => signal.removeEventListener("abort", onAbort); + signal.addEventListener("abort", onAbort); + } + const eventListeners = this.#listeners[eventName] ||= []; + eventListeners.push({ + listener, + external: options?.external === true, + once: options?.once === true, + rmAbort + }); + } + _off(eventName, listener, options = null) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners) { + return; + } + for (let i = 0, ii = eventListeners.length; i < ii; i++) { + const evt = eventListeners[i]; + if (evt.listener === listener) { + evt.rmAbort?.(); + eventListeners.splice(i, 1); + return; + } + } + } +} +class FirefoxEventBus extends EventBus { + #externalServices; + #globalEventNames; + #isInAutomation; + constructor(globalEventNames, externalServices, isInAutomation) { + super(); + this.#globalEventNames = globalEventNames; + this.#externalServices = externalServices; + this.#isInAutomation = isInAutomation; + } + dispatch(eventName, data) { + throw new Error("Not implemented: FirefoxEventBus.dispatch"); + } +} + +;// ./node_modules/@fluent/bundle/esm/types.js +class FluentType { + constructor(value) { + this.value = value; + } + valueOf() { + return this.value; + } +} +class FluentNone extends FluentType { + constructor(value = "???") { + super(value); + } + toString(scope) { + return `{${this.value}}`; + } +} +class FluentNumber extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); + return nf.format(this.value); + } catch (err) { + scope.reportError(err); + return this.value.toString(10); + } + } +} +class FluentDateTime extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts); + return dtf.format(this.value); + } catch (err) { + scope.reportError(err); + return new Date(this.value).toISOString(); + } + } +} +;// ./node_modules/@fluent/bundle/esm/resolver.js + +const MAX_PLACEABLES = 100; +const FSI = "\u2068"; +const PDI = "\u2069"; +function match(scope, selector, key) { + if (key === selector) { + return true; + } + if (key instanceof FluentNumber && selector instanceof FluentNumber && key.value === selector.value) { + return true; + } + if (selector instanceof FluentNumber && typeof key === "string") { + let category = scope.memoizeIntlObject(Intl.PluralRules, selector.opts).select(selector.value); + if (key === category) { + return true; + } + } + return false; +} +function getDefault(scope, variants, star) { + if (variants[star]) { + return resolvePattern(scope, variants[star].value); + } + scope.reportError(new RangeError("No default")); + return new FluentNone(); +} +function getArguments(scope, args) { + const positional = []; + const named = Object.create(null); + for (const arg of args) { + if (arg.type === "narg") { + named[arg.name] = resolveExpression(scope, arg.value); + } else { + positional.push(resolveExpression(scope, arg)); + } + } + return { + positional, + named + }; +} +function resolveExpression(scope, expr) { + switch (expr.type) { + case "str": + return expr.value; + case "num": + return new FluentNumber(expr.value, { + minimumFractionDigits: expr.precision + }); + case "var": + return resolveVariableReference(scope, expr); + case "mesg": + return resolveMessageReference(scope, expr); + case "term": + return resolveTermReference(scope, expr); + case "func": + return resolveFunctionReference(scope, expr); + case "select": + return resolveSelectExpression(scope, expr); + default: + return new FluentNone(); + } +} +function resolveVariableReference(scope, { + name +}) { + let arg; + if (scope.params) { + if (Object.prototype.hasOwnProperty.call(scope.params, name)) { + arg = scope.params[name]; + } else { + return new FluentNone(`$${name}`); + } + } else if (scope.args && Object.prototype.hasOwnProperty.call(scope.args, name)) { + arg = scope.args[name]; + } else { + scope.reportError(new ReferenceError(`Unknown variable: $${name}`)); + return new FluentNone(`$${name}`); + } + if (arg instanceof FluentType) { + return arg; + } + switch (typeof arg) { + case "string": + return arg; + case "number": + return new FluentNumber(arg); + case "object": + if (arg instanceof Date) { + return new FluentDateTime(arg.getTime()); + } + default: + scope.reportError(new TypeError(`Variable type not supported: $${name}, ${typeof arg}`)); + return new FluentNone(`$${name}`); + } +} +function resolveMessageReference(scope, { + name, + attr +}) { + const message = scope.bundle._messages.get(name); + if (!message) { + scope.reportError(new ReferenceError(`Unknown message: ${name}`)); + return new FluentNone(name); + } + if (attr) { + const attribute = message.attributes[attr]; + if (attribute) { + return resolvePattern(scope, attribute); + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${name}.${attr}`); + } + if (message.value) { + return resolvePattern(scope, message.value); + } + scope.reportError(new ReferenceError(`No value: ${name}`)); + return new FluentNone(name); +} +function resolveTermReference(scope, { + name, + attr, + args +}) { + const id = `-${name}`; + const term = scope.bundle._terms.get(id); + if (!term) { + scope.reportError(new ReferenceError(`Unknown term: ${id}`)); + return new FluentNone(id); + } + if (attr) { + const attribute = term.attributes[attr]; + if (attribute) { + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, attribute); + scope.params = null; + return resolved; + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${id}.${attr}`); + } + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, term.value); + scope.params = null; + return resolved; +} +function resolveFunctionReference(scope, { + name, + args +}) { + let func = scope.bundle._functions[name]; + if (!func) { + scope.reportError(new ReferenceError(`Unknown function: ${name}()`)); + return new FluentNone(`${name}()`); + } + if (typeof func !== "function") { + scope.reportError(new TypeError(`Function ${name}() is not callable`)); + return new FluentNone(`${name}()`); + } + try { + let resolved = getArguments(scope, args); + return func(resolved.positional, resolved.named); + } catch (err) { + scope.reportError(err); + return new FluentNone(`${name}()`); + } +} +function resolveSelectExpression(scope, { + selector, + variants, + star +}) { + let sel = resolveExpression(scope, selector); + if (sel instanceof FluentNone) { + return getDefault(scope, variants, star); + } + for (const variant of variants) { + const key = resolveExpression(scope, variant.key); + if (match(scope, sel, key)) { + return resolvePattern(scope, variant.value); + } + } + return getDefault(scope, variants, star); +} +function resolveComplexPattern(scope, ptn) { + if (scope.dirty.has(ptn)) { + scope.reportError(new RangeError("Cyclic reference")); + return new FluentNone(); + } + scope.dirty.add(ptn); + const result = []; + const useIsolating = scope.bundle._useIsolating && ptn.length > 1; + for (const elem of ptn) { + if (typeof elem === "string") { + result.push(scope.bundle._transform(elem)); + continue; + } + scope.placeables++; + if (scope.placeables > MAX_PLACEABLES) { + scope.dirty.delete(ptn); + throw new RangeError(`Too many placeables expanded: ${scope.placeables}, ` + `max allowed is ${MAX_PLACEABLES}`); + } + if (useIsolating) { + result.push(FSI); + } + result.push(resolveExpression(scope, elem).toString(scope)); + if (useIsolating) { + result.push(PDI); + } + } + scope.dirty.delete(ptn); + return result.join(""); +} +function resolvePattern(scope, value) { + if (typeof value === "string") { + return scope.bundle._transform(value); + } + return resolveComplexPattern(scope, value); +} +;// ./node_modules/@fluent/bundle/esm/scope.js +class Scope { + constructor(bundle, errors, args) { + this.dirty = new WeakSet(); + this.params = null; + this.placeables = 0; + this.bundle = bundle; + this.errors = errors; + this.args = args; + } + reportError(error) { + if (!this.errors || !(error instanceof Error)) { + throw error; + } + this.errors.push(error); + } + memoizeIntlObject(ctor, opts) { + let cache = this.bundle._intls.get(ctor); + if (!cache) { + cache = {}; + this.bundle._intls.set(ctor, cache); + } + let id = JSON.stringify(opts); + if (!cache[id]) { + cache[id] = new ctor(this.bundle.locales, opts); + } + return cache[id]; + } +} +;// ./node_modules/@fluent/bundle/esm/builtins.js + +function values(opts, allowed) { + const unwrapped = Object.create(null); + for (const [name, opt] of Object.entries(opts)) { + if (allowed.includes(name)) { + unwrapped[name] = opt.valueOf(); + } + } + return unwrapped; +} +const NUMBER_ALLOWED = ["unitDisplay", "currencyDisplay", "useGrouping", "minimumIntegerDigits", "minimumFractionDigits", "maximumFractionDigits", "minimumSignificantDigits", "maximumSignificantDigits"]; +function NUMBER(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`NUMBER(${arg.valueOf()})`); + } + if (arg instanceof FluentNumber) { + return new FluentNumber(arg.valueOf(), { + ...arg.opts, + ...values(opts, NUMBER_ALLOWED) + }); + } + if (arg instanceof FluentDateTime) { + return new FluentNumber(arg.valueOf(), { + ...values(opts, NUMBER_ALLOWED) + }); + } + throw new TypeError("Invalid argument to NUMBER"); +} +const DATETIME_ALLOWED = ["dateStyle", "timeStyle", "fractionalSecondDigits", "dayPeriod", "hour12", "weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName"]; +function DATETIME(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`DATETIME(${arg.valueOf()})`); + } + if (arg instanceof FluentDateTime) { + return new FluentDateTime(arg.valueOf(), { + ...arg.opts, + ...values(opts, DATETIME_ALLOWED) + }); + } + if (arg instanceof FluentNumber) { + return new FluentDateTime(arg.valueOf(), { + ...values(opts, DATETIME_ALLOWED) + }); + } + throw new TypeError("Invalid argument to DATETIME"); +} +;// ./node_modules/@fluent/bundle/esm/memoizer.js +const cache = new Map(); +function getMemoizerForLocale(locales) { + const stringLocale = Array.isArray(locales) ? locales.join(" ") : locales; + let memoizer = cache.get(stringLocale); + if (memoizer === undefined) { + memoizer = new Map(); + cache.set(stringLocale, memoizer); + } + return memoizer; +} +;// ./node_modules/@fluent/bundle/esm/bundle.js + + + + + +class FluentBundle { + constructor(locales, { + functions, + useIsolating = true, + transform = v => v + } = {}) { + this._terms = new Map(); + this._messages = new Map(); + this.locales = Array.isArray(locales) ? locales : [locales]; + this._functions = { + NUMBER: NUMBER, + DATETIME: DATETIME, + ...functions + }; + this._useIsolating = useIsolating; + this._transform = transform; + this._intls = getMemoizerForLocale(locales); + } + hasMessage(id) { + return this._messages.has(id); + } + getMessage(id) { + return this._messages.get(id); + } + addResource(res, { + allowOverrides = false + } = {}) { + const errors = []; + for (let i = 0; i < res.body.length; i++) { + let entry = res.body[i]; + if (entry.id.startsWith("-")) { + if (allowOverrides === false && this._terms.has(entry.id)) { + errors.push(new Error(`Attempt to override an existing term: "${entry.id}"`)); + continue; + } + this._terms.set(entry.id, entry); + } else { + if (allowOverrides === false && this._messages.has(entry.id)) { + errors.push(new Error(`Attempt to override an existing message: "${entry.id}"`)); + continue; + } + this._messages.set(entry.id, entry); + } + } + return errors; + } + formatPattern(pattern, args = null, errors = null) { + if (typeof pattern === "string") { + return this._transform(pattern); + } + let scope = new Scope(this, errors, args); + try { + let value = resolveComplexPattern(scope, pattern); + return value.toString(scope); + } catch (err) { + if (scope.errors && err instanceof Error) { + scope.errors.push(err); + return new FluentNone().toString(scope); + } + throw err; + } + } +} +;// ./node_modules/@fluent/bundle/esm/resource.js +const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm; +const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y; +const RE_VARIANT_START = /\*?\[/y; +const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y; +const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y; +const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y; +const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/; +const RE_TEXT_RUN = /([^{}\n\r]+)/y; +const RE_STRING_RUN = /([^\\"\n\r]*)/y; +const RE_STRING_ESCAPE = /\\([\\"])/y; +const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y; +const RE_LEADING_NEWLINES = /^\n+/; +const RE_TRAILING_SPACES = / +$/; +const RE_BLANK_LINES = / *\r?\n/g; +const RE_INDENT = /( *)$/; +const TOKEN_BRACE_OPEN = /{\s*/y; +const TOKEN_BRACE_CLOSE = /\s*}/y; +const TOKEN_BRACKET_OPEN = /\[\s*/y; +const TOKEN_BRACKET_CLOSE = /\s*] */y; +const TOKEN_PAREN_OPEN = /\s*\(\s*/y; +const TOKEN_ARROW = /\s*->\s*/y; +const TOKEN_COLON = /\s*:\s*/y; +const TOKEN_COMMA = /\s*,?\s*/y; +const TOKEN_BLANK = /\s+/y; +class FluentResource { + constructor(source) { + this.body = []; + RE_MESSAGE_START.lastIndex = 0; + let cursor = 0; + while (true) { + let next = RE_MESSAGE_START.exec(source); + if (next === null) { + break; + } + cursor = RE_MESSAGE_START.lastIndex; + try { + this.body.push(parseMessage(next[1])); + } catch (err) { + if (err instanceof SyntaxError) { + continue; + } + throw err; + } + } + function test(re) { + re.lastIndex = cursor; + return re.test(source); + } + function consumeChar(char, errorClass) { + if (source[cursor] === char) { + cursor++; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${char}`); + } + return false; + } + function consumeToken(re, errorClass) { + if (test(re)) { + cursor = re.lastIndex; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${re.toString()}`); + } + return false; + } + function match(re) { + re.lastIndex = cursor; + let result = re.exec(source); + if (result === null) { + throw new SyntaxError(`Expected ${re.toString()}`); + } + cursor = re.lastIndex; + return result; + } + function match1(re) { + return match(re)[1]; + } + function parseMessage(id) { + let value = parsePattern(); + let attributes = parseAttributes(); + if (value === null && Object.keys(attributes).length === 0) { + throw new SyntaxError("Expected message value or attributes"); + } + return { + id, + value, + attributes + }; + } + function parseAttributes() { + let attrs = Object.create(null); + while (test(RE_ATTRIBUTE_START)) { + let name = match1(RE_ATTRIBUTE_START); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected attribute value"); + } + attrs[name] = value; + } + return attrs; + } + function parsePattern() { + let first; + if (test(RE_TEXT_RUN)) { + first = match1(RE_TEXT_RUN); + } + if (source[cursor] === "{" || source[cursor] === "}") { + return parsePatternElements(first ? [first] : [], Infinity); + } + let indent = parseIndent(); + if (indent) { + if (first) { + return parsePatternElements([first, indent], indent.length); + } + indent.value = trim(indent.value, RE_LEADING_NEWLINES); + return parsePatternElements([indent], indent.length); + } + if (first) { + return trim(first, RE_TRAILING_SPACES); + } + return null; + } + function parsePatternElements(elements = [], commonIndent) { + while (true) { + if (test(RE_TEXT_RUN)) { + elements.push(match1(RE_TEXT_RUN)); + continue; + } + if (source[cursor] === "{") { + elements.push(parsePlaceable()); + continue; + } + if (source[cursor] === "}") { + throw new SyntaxError("Unbalanced closing brace"); + } + let indent = parseIndent(); + if (indent) { + elements.push(indent); + commonIndent = Math.min(commonIndent, indent.length); + continue; + } + break; + } + let lastIndex = elements.length - 1; + let lastElement = elements[lastIndex]; + if (typeof lastElement === "string") { + elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES); + } + let baked = []; + for (let element of elements) { + if (element instanceof Indent) { + element = element.value.slice(0, element.value.length - commonIndent); + } + if (element) { + baked.push(element); + } + } + return baked; + } + function parsePlaceable() { + consumeToken(TOKEN_BRACE_OPEN, SyntaxError); + let selector = parseInlineExpression(); + if (consumeToken(TOKEN_BRACE_CLOSE)) { + return selector; + } + if (consumeToken(TOKEN_ARROW)) { + let variants = parseVariants(); + consumeToken(TOKEN_BRACE_CLOSE, SyntaxError); + return { + type: "select", + selector, + ...variants + }; + } + throw new SyntaxError("Unclosed placeable"); + } + function parseInlineExpression() { + if (source[cursor] === "{") { + return parsePlaceable(); + } + if (test(RE_REFERENCE)) { + let [, sigil, name, attr = null] = match(RE_REFERENCE); + if (sigil === "$") { + return { + type: "var", + name + }; + } + if (consumeToken(TOKEN_PAREN_OPEN)) { + let args = parseArguments(); + if (sigil === "-") { + return { + type: "term", + name, + attr, + args + }; + } + if (RE_FUNCTION_NAME.test(name)) { + return { + type: "func", + name, + args + }; + } + throw new SyntaxError("Function names must be all upper-case"); + } + if (sigil === "-") { + return { + type: "term", + name, + attr, + args: [] + }; + } + return { + type: "mesg", + name, + attr + }; + } + return parseLiteral(); + } + function parseArguments() { + let args = []; + while (true) { + switch (source[cursor]) { + case ")": + cursor++; + return args; + case undefined: + throw new SyntaxError("Unclosed argument list"); + } + args.push(parseArgument()); + consumeToken(TOKEN_COMMA); + } + } + function parseArgument() { + let expr = parseInlineExpression(); + if (expr.type !== "mesg") { + return expr; + } + if (consumeToken(TOKEN_COLON)) { + return { + type: "narg", + name: expr.name, + value: parseLiteral() + }; + } + return expr; + } + function parseVariants() { + let variants = []; + let count = 0; + let star; + while (test(RE_VARIANT_START)) { + if (consumeChar("*")) { + star = count; + } + let key = parseVariantKey(); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected variant value"); + } + variants[count++] = { + key, + value + }; + } + if (count === 0) { + return null; + } + if (star === undefined) { + throw new SyntaxError("Expected default variant"); + } + return { + variants, + star + }; + } + function parseVariantKey() { + consumeToken(TOKEN_BRACKET_OPEN, SyntaxError); + let key; + if (test(RE_NUMBER_LITERAL)) { + key = parseNumberLiteral(); + } else { + key = { + type: "str", + value: match1(RE_IDENTIFIER) + }; + } + consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError); + return key; + } + function parseLiteral() { + if (test(RE_NUMBER_LITERAL)) { + return parseNumberLiteral(); + } + if (source[cursor] === '"') { + return parseStringLiteral(); + } + throw new SyntaxError("Invalid expression"); + } + function parseNumberLiteral() { + let [, value, fraction = ""] = match(RE_NUMBER_LITERAL); + let precision = fraction.length; + return { + type: "num", + value: parseFloat(value), + precision + }; + } + function parseStringLiteral() { + consumeChar('"', SyntaxError); + let value = ""; + while (true) { + value += match1(RE_STRING_RUN); + if (source[cursor] === "\\") { + value += parseEscapeSequence(); + continue; + } + if (consumeChar('"')) { + return { + type: "str", + value + }; + } + throw new SyntaxError("Unclosed string literal"); + } + } + function parseEscapeSequence() { + if (test(RE_STRING_ESCAPE)) { + return match1(RE_STRING_ESCAPE); + } + if (test(RE_UNICODE_ESCAPE)) { + let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE); + let codepoint = parseInt(codepoint4 || codepoint6, 16); + return codepoint <= 0xd7ff || 0xe000 <= codepoint ? String.fromCodePoint(codepoint) : "�"; + } + throw new SyntaxError("Unknown escape sequence"); + } + function parseIndent() { + let start = cursor; + consumeToken(TOKEN_BLANK); + switch (source[cursor]) { + case ".": + case "[": + case "*": + case "}": + case undefined: + return false; + case "{": + return makeIndent(source.slice(start, cursor)); + } + if (source[cursor - 1] === " ") { + return makeIndent(source.slice(start, cursor)); + } + return false; + } + function trim(text, re) { + return text.replace(re, ""); + } + function makeIndent(blank) { + let value = blank.replace(RE_BLANK_LINES, "\n"); + let length = RE_INDENT.exec(blank)[1].length; + return new Indent(value, length); + } + } +} +class Indent { + constructor(value, length) { + this.value = value; + this.length = length; + } +} +;// ./node_modules/@fluent/bundle/esm/index.js + + + +;// ./node_modules/@fluent/dom/esm/overlay.js +const reOverlay = /<|&#?\w+;/; +const TEXT_LEVEL_ELEMENTS = { + "http://www.w3.org/1999/xhtml": ["em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data", "time", "code", "var", "samp", "kbd", "sub", "sup", "i", "b", "u", "mark", "bdi", "bdo", "span", "br", "wbr"] +}; +const LOCALIZABLE_ATTRIBUTES = { + "http://www.w3.org/1999/xhtml": { + global: ["title", "aria-label", "aria-valuetext"], + a: ["download"], + area: ["download", "alt"], + input: ["alt", "placeholder"], + menuitem: ["label"], + menu: ["label"], + optgroup: ["label"], + option: ["label"], + track: ["label"], + img: ["alt"], + textarea: ["placeholder"], + th: ["abbr"] + }, + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": { + global: ["accesskey", "aria-label", "aria-valuetext", "label", "title", "tooltiptext"], + description: ["value"], + key: ["key", "keycode"], + label: ["value"], + textbox: ["placeholder", "value"] + } +}; +function translateElement(element, translation) { + const { + value + } = translation; + if (typeof value === "string") { + if (element.localName === "title" && element.namespaceURI === "http://www.w3.org/1999/xhtml") { + element.textContent = value; + } else if (!reOverlay.test(value)) { + element.textContent = value; + } else { + const templateElement = element.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "template"); + templateElement.innerHTML = value; + overlayChildNodes(templateElement.content, element); + } + } + overlayAttributes(translation, element); +} +function overlayChildNodes(fromFragment, toElement) { + for (const childNode of fromFragment.childNodes) { + if (childNode.nodeType === childNode.TEXT_NODE) { + continue; + } + if (childNode.hasAttribute("data-l10n-name")) { + const sanitized = getNodeForNamedElement(toElement, childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + if (isElementAllowed(childNode)) { + const sanitized = createSanitizedElement(childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + console.warn(`An element of forbidden type "${childNode.localName}" was found in ` + "the translation. Only safe text-level elements and elements with " + "data-l10n-name are allowed."); + fromFragment.replaceChild(createTextNodeFromTextContent(childNode), childNode); + } + toElement.textContent = ""; + toElement.appendChild(fromFragment); +} +function hasAttribute(attributes, name) { + if (!attributes) { + return false; + } + for (let attr of attributes) { + if (attr.name === name) { + return true; + } + } + return false; +} +function overlayAttributes(fromElement, toElement) { + const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs") ? toElement.getAttribute("data-l10n-attrs").split(",").map(i => i.trim()) : null; + for (const attr of Array.from(toElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && !hasAttribute(fromElement.attributes, attr.name)) { + toElement.removeAttribute(attr.name); + } + } + if (!fromElement.attributes) { + return; + } + for (const attr of Array.from(fromElement.attributes)) { + if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && toElement.getAttribute(attr.name) !== attr.value) { + toElement.setAttribute(attr.name, attr.value); + } + } +} +function getNodeForNamedElement(sourceElement, translatedChild) { + const childName = translatedChild.getAttribute("data-l10n-name"); + const sourceChild = sourceElement.querySelector(`[data-l10n-name="${childName}"]`); + if (!sourceChild) { + console.warn(`An element named "${childName}" wasn't found in the source.`); + return createTextNodeFromTextContent(translatedChild); + } + if (sourceChild.localName !== translatedChild.localName) { + console.warn(`An element named "${childName}" was found in the translation ` + `but its type ${translatedChild.localName} didn't match the ` + `element found in the source (${sourceChild.localName}).`); + return createTextNodeFromTextContent(translatedChild); + } + sourceElement.removeChild(sourceChild); + const clone = sourceChild.cloneNode(false); + return shallowPopulateUsing(translatedChild, clone); +} +function createSanitizedElement(element) { + const clone = element.ownerDocument.createElement(element.localName); + return shallowPopulateUsing(element, clone); +} +function createTextNodeFromTextContent(element) { + return element.ownerDocument.createTextNode(element.textContent); +} +function isElementAllowed(element) { + const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI]; + return allowed && allowed.includes(element.localName); +} +function isAttrNameLocalizable(name, element, explicitlyAllowed = null) { + if (explicitlyAllowed && explicitlyAllowed.includes(name)) { + return true; + } + const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI]; + if (!allowed) { + return false; + } + const attrName = name.toLowerCase(); + const elemName = element.localName; + if (allowed.global.includes(attrName)) { + return true; + } + if (!allowed[elemName]) { + return false; + } + if (allowed[elemName].includes(attrName)) { + return true; + } + if (element.namespaceURI === "http://www.w3.org/1999/xhtml" && elemName === "input" && attrName === "value") { + const type = element.type.toLowerCase(); + if (type === "submit" || type === "button" || type === "reset") { + return true; + } + } + return false; +} +function shallowPopulateUsing(fromElement, toElement) { + toElement.textContent = fromElement.textContent; + overlayAttributes(fromElement, toElement); + return toElement; +} +;// ./node_modules/cached-iterable/src/cached_iterable.mjs +class CachedIterable extends Array { + static from(iterable) { + if (iterable instanceof this) { + return iterable; + } + return new this(iterable); + } +} +;// ./node_modules/cached-iterable/src/cached_sync_iterable.mjs + +class CachedSyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.iterator]() { + const cached = this; + let cur = 0; + return { + next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + } + }; + } + touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && last.done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} +;// ./node_modules/cached-iterable/src/cached_async_iterable.mjs + +class CachedAsyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.asyncIterator in Object(iterable)) { + this.iterator = iterable[Symbol.asyncIterator](); + } else if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.asyncIterator]() { + const cached = this; + let cur = 0; + return { + async next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + } + }; + } + async touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && (await last).done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} +;// ./node_modules/cached-iterable/src/index.mjs + + +;// ./node_modules/@fluent/dom/esm/localization.js + +class Localization { + constructor(resourceIds = [], generateBundles) { + this.resourceIds = resourceIds; + this.generateBundles = generateBundles; + this.onChange(true); + } + addResourceIds(resourceIds, eager = false) { + this.resourceIds.push(...resourceIds); + this.onChange(eager); + return this.resourceIds.length; + } + removeResourceIds(resourceIds) { + this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r)); + this.onChange(); + return this.resourceIds.length; + } + async formatWithFallback(keys, method) { + const translations = []; + let hasAtLeastOneBundle = false; + for await (const bundle of this.bundles) { + hasAtLeastOneBundle = true; + const missingIds = keysFromBundle(method, bundle, keys, translations); + if (missingIds.size === 0) { + break; + } + if (typeof console !== "undefined") { + const locale = bundle.locales[0]; + const ids = Array.from(missingIds).join(", "); + console.warn(`[fluent] Missing translations in ${locale}: ${ids}`); + } + } + if (!hasAtLeastOneBundle && typeof console !== "undefined") { + console.warn(`[fluent] Request for keys failed because no resource bundles got generated. + keys: ${JSON.stringify(keys)}. + resourceIds: ${JSON.stringify(this.resourceIds)}.`); + } + return translations; + } + formatMessages(keys) { + return this.formatWithFallback(keys, messageFromBundle); + } + formatValues(keys) { + return this.formatWithFallback(keys, valueFromBundle); + } + async formatValue(id, args) { + const [val] = await this.formatValues([{ + id, + args + }]); + return val; + } + handleEvent() { + this.onChange(); + } + onChange(eager = false) { + this.bundles = CachedAsyncIterable.from(this.generateBundles(this.resourceIds)); + if (eager) { + this.bundles.touchNext(2); + } + } +} +function valueFromBundle(bundle, errors, message, args) { + if (message.value) { + return bundle.formatPattern(message.value, args, errors); + } + return null; +} +function messageFromBundle(bundle, errors, message, args) { + const formatted = { + value: null, + attributes: null + }; + if (message.value) { + formatted.value = bundle.formatPattern(message.value, args, errors); + } + let attrNames = Object.keys(message.attributes); + if (attrNames.length > 0) { + formatted.attributes = new Array(attrNames.length); + for (let [i, name] of attrNames.entries()) { + let value = bundle.formatPattern(message.attributes[name], args, errors); + formatted.attributes[i] = { + name, + value + }; + } + } + return formatted; +} +function keysFromBundle(method, bundle, keys, translations) { + const messageErrors = []; + const missingIds = new Set(); + keys.forEach(({ + id, + args + }, i) => { + if (translations[i] !== undefined) { + return; + } + let message = bundle.getMessage(id); + if (message) { + messageErrors.length = 0; + translations[i] = method(bundle, messageErrors, message, args); + if (messageErrors.length > 0 && typeof console !== "undefined") { + const locale = bundle.locales[0]; + const errors = messageErrors.join(", "); + console.warn(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`); + } + } else { + missingIds.add(id); + } + }); + return missingIds; +} +;// ./node_modules/@fluent/dom/esm/dom_localization.js + + +const L10NID_ATTR_NAME = "data-l10n-id"; +const L10NARGS_ATTR_NAME = "data-l10n-args"; +const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`; +class DOMLocalization extends Localization { + constructor(resourceIds, generateBundles) { + super(resourceIds, generateBundles); + this.roots = new Set(); + this.pendingrAF = null; + this.pendingElements = new Set(); + this.windowElement = null; + this.mutationObserver = null; + this.observerConfig = { + attributes: true, + characterData: false, + childList: true, + subtree: true, + attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME] + }; + } + onChange(eager = false) { + super.onChange(eager); + if (this.roots) { + this.translateRoots(); + } + } + setAttributes(element, id, args) { + element.setAttribute(L10NID_ATTR_NAME, id); + if (args) { + element.setAttribute(L10NARGS_ATTR_NAME, JSON.stringify(args)); + } else { + element.removeAttribute(L10NARGS_ATTR_NAME); + } + return element; + } + getAttributes(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) + }; + } + connectRoot(newRoot) { + for (const root of this.roots) { + if (root === newRoot || root.contains(newRoot) || newRoot.contains(root)) { + throw new Error("Cannot add a root that overlaps with existing root."); + } + } + if (this.windowElement) { + if (this.windowElement !== newRoot.ownerDocument.defaultView) { + throw new Error(`Cannot connect a root: + DOMLocalization already has a root from a different window.`); + } + } else { + this.windowElement = newRoot.ownerDocument.defaultView; + this.mutationObserver = new this.windowElement.MutationObserver(mutations => this.translateMutations(mutations)); + } + this.roots.add(newRoot); + this.mutationObserver.observe(newRoot, this.observerConfig); + } + disconnectRoot(root) { + this.roots.delete(root); + this.pauseObserving(); + if (this.roots.size === 0) { + this.mutationObserver = null; + if (this.windowElement && this.pendingrAF) { + this.windowElement.cancelAnimationFrame(this.pendingrAF); + } + this.windowElement = null; + this.pendingrAF = null; + this.pendingElements.clear(); + return true; + } + this.resumeObserving(); + return false; + } + translateRoots() { + const roots = Array.from(this.roots); + return Promise.all(roots.map(root => this.translateFragment(root))); + } + pauseObserving() { + if (!this.mutationObserver) { + return; + } + this.translateMutations(this.mutationObserver.takeRecords()); + this.mutationObserver.disconnect(); + } + resumeObserving() { + if (!this.mutationObserver) { + return; + } + for (const root of this.roots) { + this.mutationObserver.observe(root, this.observerConfig); + } + } + translateMutations(mutations) { + for (const mutation of mutations) { + switch (mutation.type) { + case "attributes": + if (mutation.target.hasAttribute("data-l10n-id")) { + this.pendingElements.add(mutation.target); + } + break; + case "childList": + for (const addedNode of mutation.addedNodes) { + if (addedNode.nodeType === addedNode.ELEMENT_NODE) { + if (addedNode.childElementCount) { + for (const element of this.getTranslatables(addedNode)) { + this.pendingElements.add(element); + } + } else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) { + this.pendingElements.add(addedNode); + } + } + } + break; + } + } + if (this.pendingElements.size > 0) { + if (this.pendingrAF === null) { + this.pendingrAF = this.windowElement.requestAnimationFrame(() => { + this.translateElements(Array.from(this.pendingElements)); + this.pendingElements.clear(); + this.pendingrAF = null; + }); + } + } + } + translateFragment(frag) { + return this.translateElements(this.getTranslatables(frag)); + } + async translateElements(elements) { + if (!elements.length) { + return undefined; + } + const keys = elements.map(this.getKeysForElement); + const translations = await this.formatMessages(keys); + return this.applyTranslations(elements, translations); + } + applyTranslations(elements, translations) { + this.pauseObserving(); + for (let i = 0; i < elements.length; i++) { + if (translations[i] !== undefined) { + translateElement(elements[i], translations[i]); + } + } + this.resumeObserving(); + } + getTranslatables(element) { + const nodes = Array.from(element.querySelectorAll(L10N_ELEMENT_QUERY)); + if (typeof element.hasAttribute === "function" && element.hasAttribute(L10NID_ATTR_NAME)) { + nodes.push(element); + } + return nodes; + } + getKeysForElement(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null) + }; + } +} +;// ./node_modules/@fluent/dom/esm/index.js + + +;// ./web/l10n.js +class L10n { + #dir; + #elements; + #lang; + #l10n; + constructor({ + lang, + isRTL + }, l10n = null) { + this.#lang = L10n.#fixupLangCode(lang); + this.#l10n = l10n; + this.#dir = isRTL ?? L10n.#isRTL(this.#lang) ? "rtl" : "ltr"; + } + _setL10n(l10n) { + this.#l10n = l10n; + } + getLanguage() { + return this.#lang; + } + getDirection() { + return this.#dir; + } + async get(ids, args = null, fallback) { + if (Array.isArray(ids)) { + ids = ids.map(id => ({ + id + })); + const messages = await this.#l10n.formatMessages(ids); + return messages.map(message => message.value); + } + const messages = await this.#l10n.formatMessages([{ + id: ids, + args + }]); + return messages[0]?.value || fallback; + } + async translate(element) { + (this.#elements ||= new Set()).add(element); + try { + this.#l10n.connectRoot(element); + await this.#l10n.translateRoots(); + } catch {} + } + async translateOnce(element) { + try { + await this.#l10n.translateElements([element]); + } catch (ex) { + console.error("translateOnce:", ex); + } + } + async destroy() { + if (this.#elements) { + for (const element of this.#elements) { + this.#l10n.disconnectRoot(element); + } + this.#elements.clear(); + this.#elements = null; + } + this.#l10n.pauseObserving(); + } + pause() { + this.#l10n.pauseObserving(); + } + resume() { + this.#l10n.resumeObserving(); + } + static #fixupLangCode(langCode) { + langCode = langCode?.toLowerCase() || "en-us"; + const PARTIAL_LANG_CODES = { + en: "en-us", + es: "es-es", + fy: "fy-nl", + ga: "ga-ie", + gu: "gu-in", + hi: "hi-in", + hy: "hy-am", + nb: "nb-no", + ne: "ne-np", + nn: "nn-no", + pa: "pa-in", + pt: "pt-pt", + sv: "sv-se", + zh: "zh-cn" + }; + return PARTIAL_LANG_CODES[langCode] || langCode; + } + static #isRTL(lang) { + const shortCode = lang.split("-", 1)[0]; + return ["ar", "he", "fa", "ps", "ur"].includes(shortCode); + } +} +const GenericL10n = null; + +;// ./web/genericl10n.js + + + + +function createBundle(lang, text) { + const resource = new FluentResource(text); + const bundle = new FluentBundle(lang); + const errors = bundle.addResource(resource); + if (errors.length) { + console.error("L10n errors", errors); + } + return bundle; +} +class genericl10n_GenericL10n extends L10n { + constructor(lang) { + super({ + lang + }); + const generateBundles = !lang ? genericl10n_GenericL10n.#generateBundlesFallback.bind(genericl10n_GenericL10n, this.getLanguage()) : genericl10n_GenericL10n.#generateBundles.bind(genericl10n_GenericL10n, "en-us", this.getLanguage()); + this._setL10n(new DOMLocalization([], generateBundles)); + } + static async *#generateBundles(defaultLang, baseLang) { + const { + baseURL, + paths + } = await this.#getPaths(); + const langs = [baseLang]; + if (defaultLang !== baseLang) { + const shortLang = baseLang.split("-", 1)[0]; + if (shortLang !== baseLang) { + langs.push(shortLang); + } + langs.push(defaultLang); + } + for (const lang of langs) { + const bundle = await this.#createBundle(lang, baseURL, paths); + if (bundle) { + yield bundle; + } else if (lang === "en-us") { + yield this.#createBundleFallback(lang); + } + } + } + static async #createBundle(lang, baseURL, paths) { + const path = paths[lang]; + if (!path) { + return null; + } + const url = new URL(path, baseURL); + const text = await fetchData(url, "text"); + return createBundle(lang, text); + } + static async #getPaths() { + try { + const { + href + } = document.querySelector(`link[type="application/l10n"]`); + const paths = await fetchData(href, "json"); + return { + baseURL: href.replace(/[^/]*$/, "") || "./", + paths + }; + } catch {} + return { + baseURL: "./", + paths: Object.create(null) + }; + } + static async *#generateBundlesFallback(lang) { + yield this.#createBundleFallback(lang); + } + static async #createBundleFallback(lang) { + const text = "pdfjs-previous-button =\n .title = Previous Page\npdfjs-previous-button-label = Previous\npdfjs-next-button =\n .title = Next Page\npdfjs-next-button-label = Next\npdfjs-page-input =\n .title = Page\npdfjs-of-pages = of { $pagesCount }\npdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount })\npdfjs-zoom-out-button =\n .title = Zoom Out\npdfjs-zoom-out-button-label = Zoom Out\npdfjs-zoom-in-button =\n .title = Zoom In\npdfjs-zoom-in-button-label = Zoom In\npdfjs-zoom-select =\n .title = Zoom\npdfjs-presentation-mode-button =\n .title = Switch to Presentation Mode\npdfjs-presentation-mode-button-label = Presentation Mode\npdfjs-open-file-button =\n .title = Open File\npdfjs-open-file-button-label = Open\npdfjs-print-button =\n .title = Print\npdfjs-print-button-label = Print\npdfjs-save-button =\n .title = Save\npdfjs-save-button-label = Save\npdfjs-download-button =\n .title = Download\npdfjs-download-button-label = Download\npdfjs-bookmark-button =\n .title = Current Page (View URL from Current Page)\npdfjs-bookmark-button-label = Current Page\npdfjs-tools-button =\n .title = Tools\npdfjs-tools-button-label = Tools\npdfjs-first-page-button =\n .title = Go to First Page\npdfjs-first-page-button-label = Go to First Page\npdfjs-last-page-button =\n .title = Go to Last Page\npdfjs-last-page-button-label = Go to Last Page\npdfjs-page-rotate-cw-button =\n .title = Rotate Clockwise\npdfjs-page-rotate-cw-button-label = Rotate Clockwise\npdfjs-page-rotate-ccw-button =\n .title = Rotate Counterclockwise\npdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise\npdfjs-cursor-text-select-tool-button =\n .title = Enable Text Selection Tool\npdfjs-cursor-text-select-tool-button-label = Text Selection Tool\npdfjs-cursor-hand-tool-button =\n .title = Enable Hand Tool\npdfjs-cursor-hand-tool-button-label = Hand Tool\npdfjs-scroll-page-button =\n .title = Use Page Scrolling\npdfjs-scroll-page-button-label = Page Scrolling\npdfjs-scroll-vertical-button =\n .title = Use Vertical Scrolling\npdfjs-scroll-vertical-button-label = Vertical Scrolling\npdfjs-scroll-horizontal-button =\n .title = Use Horizontal Scrolling\npdfjs-scroll-horizontal-button-label = Horizontal Scrolling\npdfjs-scroll-wrapped-button =\n .title = Use Wrapped Scrolling\npdfjs-scroll-wrapped-button-label = Wrapped Scrolling\npdfjs-spread-none-button =\n .title = Do not join page spreads\npdfjs-spread-none-button-label = No Spreads\npdfjs-spread-odd-button =\n .title = Join page spreads starting with odd-numbered pages\npdfjs-spread-odd-button-label = Odd Spreads\npdfjs-spread-even-button =\n .title = Join page spreads starting with even-numbered pages\npdfjs-spread-even-button-label = Even Spreads\npdfjs-document-properties-button =\n .title = Document Properties\u2026\npdfjs-document-properties-button-label = Document Properties\u2026\npdfjs-document-properties-file-name = File name:\npdfjs-document-properties-file-size = File size:\npdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes)\npdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes)\npdfjs-document-properties-title = Title:\npdfjs-document-properties-author = Author:\npdfjs-document-properties-subject = Subject:\npdfjs-document-properties-keywords = Keywords:\npdfjs-document-properties-creation-date = Creation Date:\npdfjs-document-properties-modification-date = Modification Date:\npdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: \"short\", timeStyle: \"medium\") }\npdfjs-document-properties-creator = Creator:\npdfjs-document-properties-producer = PDF Producer:\npdfjs-document-properties-version = PDF Version:\npdfjs-document-properties-page-count = Page Count:\npdfjs-document-properties-page-size = Page Size:\npdfjs-document-properties-page-size-unit-inches = in\npdfjs-document-properties-page-size-unit-millimeters = mm\npdfjs-document-properties-page-size-orientation-portrait = portrait\npdfjs-document-properties-page-size-orientation-landscape = landscape\npdfjs-document-properties-page-size-name-a-three = A3\npdfjs-document-properties-page-size-name-a-four = A4\npdfjs-document-properties-page-size-name-letter = Letter\npdfjs-document-properties-page-size-name-legal = Legal\npdfjs-document-properties-page-size-dimension-string = { $width } \xD7 { $height } { $unit } ({ $orientation })\npdfjs-document-properties-page-size-dimension-name-string = { $width } \xD7 { $height } { $unit } ({ $name }, { $orientation })\npdfjs-document-properties-linearized = Fast Web View:\npdfjs-document-properties-linearized-yes = Yes\npdfjs-document-properties-linearized-no = No\npdfjs-document-properties-close-button = Close\npdfjs-print-progress-message = Preparing document for printing\u2026\npdfjs-print-progress-percent = { $progress }%\npdfjs-print-progress-close-button = Cancel\npdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser.\npdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing.\npdfjs-toggle-sidebar-button =\n .title = Toggle Sidebar\npdfjs-toggle-sidebar-notification-button =\n .title = Toggle Sidebar (document contains outline/attachments/layers)\npdfjs-toggle-sidebar-button-label = Toggle Sidebar\npdfjs-document-outline-button =\n .title = Show Document Outline (double-click to expand/collapse all items)\npdfjs-document-outline-button-label = Document Outline\npdfjs-attachments-button =\n .title = Show Attachments\npdfjs-attachments-button-label = Attachments\npdfjs-layers-button =\n .title = Show Layers (double-click to reset all layers to the default state)\npdfjs-layers-button-label = Layers\npdfjs-thumbs-button =\n .title = Show Thumbnails\npdfjs-thumbs-button-label = Thumbnails\npdfjs-current-outline-item-button =\n .title = Find Current Outline Item\npdfjs-current-outline-item-button-label = Current Outline Item\npdfjs-findbar-button =\n .title = Find in Document\npdfjs-findbar-button-label = Find\npdfjs-additional-layers = Additional Layers\npdfjs-thumb-page-title =\n .title = Page { $page }\npdfjs-thumb-page-canvas =\n .aria-label = Thumbnail of Page { $page }\npdfjs-find-input =\n .title = Find\n .placeholder = Find in document\u2026\npdfjs-find-previous-button =\n .title = Find the previous occurrence of the phrase\npdfjs-find-previous-button-label = Previous\npdfjs-find-next-button =\n .title = Find the next occurrence of the phrase\npdfjs-find-next-button-label = Next\npdfjs-find-highlight-checkbox = Highlight All\npdfjs-find-match-case-checkbox-label = Match Case\npdfjs-find-match-diacritics-checkbox-label = Match Diacritics\npdfjs-find-entire-word-checkbox-label = Whole Words\npdfjs-find-reached-top = Reached top of document, continued from bottom\npdfjs-find-reached-bottom = Reached end of document, continued from top\npdfjs-find-match-count =\n { $total ->\n [one] { $current } of { $total } match\n *[other] { $current } of { $total } matches\n }\npdfjs-find-match-count-limit =\n { $limit ->\n [one] More than { $limit } match\n *[other] More than { $limit } matches\n }\npdfjs-find-not-found = Phrase not found\npdfjs-page-scale-width = Page Width\npdfjs-page-scale-fit = Page Fit\npdfjs-page-scale-auto = Automatic Zoom\npdfjs-page-scale-actual = Actual Size\npdfjs-page-scale-percent = { $scale }%\npdfjs-page-landmark =\n .aria-label = Page { $page }\npdfjs-loading-error = An error occurred while loading the PDF.\npdfjs-invalid-file-error = Invalid or corrupted PDF file.\npdfjs-missing-file-error = Missing PDF file.\npdfjs-unexpected-response-error = Unexpected server response.\npdfjs-rendering-error = An error occurred while rendering the page.\npdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: \"short\", timeStyle: \"medium\") }\npdfjs-text-annotation-type =\n .alt = [{ $type } Annotation]\npdfjs-password-label = Enter the password to open this PDF file.\npdfjs-password-invalid = Invalid password. Please try again.\npdfjs-password-ok-button = OK\npdfjs-password-cancel-button = Cancel\npdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts.\npdfjs-editor-free-text-button =\n .title = Text\npdfjs-editor-free-text-button-label = Text\npdfjs-editor-ink-button =\n .title = Draw\npdfjs-editor-ink-button-label = Draw\npdfjs-editor-stamp-button =\n .title = Add or edit images\npdfjs-editor-stamp-button-label = Add or edit images\npdfjs-editor-highlight-button =\n .title = Highlight\npdfjs-editor-highlight-button-label = Highlight\npdfjs-highlight-floating-button1 =\n .title = Highlight\n .aria-label = Highlight\npdfjs-highlight-floating-button-label = Highlight\npdfjs-editor-remove-ink-button =\n .title = Remove drawing\npdfjs-editor-remove-freetext-button =\n .title = Remove text\npdfjs-editor-remove-stamp-button =\n .title = Remove image\npdfjs-editor-remove-highlight-button =\n .title = Remove highlight\npdfjs-editor-free-text-color-input = Color\npdfjs-editor-free-text-size-input = Size\npdfjs-editor-ink-color-input = Color\npdfjs-editor-ink-thickness-input = Thickness\npdfjs-editor-ink-opacity-input = Opacity\npdfjs-editor-stamp-add-image-button =\n .title = Add image\npdfjs-editor-stamp-add-image-button-label = Add image\npdfjs-editor-free-highlight-thickness-input = Thickness\npdfjs-editor-free-highlight-thickness-title =\n .title = Change thickness when highlighting items other than text\npdfjs-free-text2 =\n .aria-label = Text Editor\n .default-content = Start typing\u2026\npdfjs-ink =\n .aria-label = Draw Editor\npdfjs-ink-canvas =\n .aria-label = User-created image\npdfjs-editor-alt-text-button =\n .aria-label = Alt text\npdfjs-editor-alt-text-button-label = Alt text\npdfjs-editor-alt-text-edit-button =\n .aria-label = Edit alt text\npdfjs-editor-alt-text-dialog-label = Choose an option\npdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can\u2019t see the image or when it doesn\u2019t load.\npdfjs-editor-alt-text-add-description-label = Add a description\npdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions.\npdfjs-editor-alt-text-mark-decorative-label = Mark as decorative\npdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks.\npdfjs-editor-alt-text-cancel-button = Cancel\npdfjs-editor-alt-text-save-button = Save\npdfjs-editor-alt-text-decorative-tooltip = Marked as decorative\npdfjs-editor-alt-text-textarea =\n .placeholder = For example, \u201CA young man sits down at a table to eat a meal\u201D\npdfjs-editor-resizer-top-left =\n .aria-label = Top left corner \u2014 resize\npdfjs-editor-resizer-top-middle =\n .aria-label = Top middle \u2014 resize\npdfjs-editor-resizer-top-right =\n .aria-label = Top right corner \u2014 resize\npdfjs-editor-resizer-middle-right =\n .aria-label = Middle right \u2014 resize\npdfjs-editor-resizer-bottom-right =\n .aria-label = Bottom right corner \u2014 resize\npdfjs-editor-resizer-bottom-middle =\n .aria-label = Bottom middle \u2014 resize\npdfjs-editor-resizer-bottom-left =\n .aria-label = Bottom left corner \u2014 resize\npdfjs-editor-resizer-middle-left =\n .aria-label = Middle left \u2014 resize\npdfjs-editor-highlight-colorpicker-label = Highlight color\npdfjs-editor-colorpicker-button =\n .title = Change color\npdfjs-editor-colorpicker-dropdown =\n .aria-label = Color choices\npdfjs-editor-colorpicker-yellow =\n .title = Yellow\npdfjs-editor-colorpicker-green =\n .title = Green\npdfjs-editor-colorpicker-blue =\n .title = Blue\npdfjs-editor-colorpicker-pink =\n .title = Pink\npdfjs-editor-colorpicker-red =\n .title = Red\npdfjs-editor-highlight-show-all-button-label = Show all\npdfjs-editor-highlight-show-all-button =\n .title = Show all\npdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description)\npdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description)\npdfjs-editor-new-alt-text-textarea =\n .placeholder = Write your description here\u2026\npdfjs-editor-new-alt-text-description = Short description for people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate.\npdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more\npdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically\npdfjs-editor-new-alt-text-not-now-button = Not now\npdfjs-editor-new-alt-text-error-title = Couldn\u2019t create alt text automatically\npdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later.\npdfjs-editor-new-alt-text-error-close-button = Close\npdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\n .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\npdfjs-editor-new-alt-text-added-button =\n .aria-label = Alt text added\npdfjs-editor-new-alt-text-added-button-label = Alt text added\npdfjs-editor-new-alt-text-missing-button =\n .aria-label = Missing alt text\npdfjs-editor-new-alt-text-missing-button-label = Missing alt text\npdfjs-editor-new-alt-text-to-review-button =\n .aria-label = Review alt text\npdfjs-editor-new-alt-text-to-review-button-label = Review alt text\npdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText }\npdfjs-image-alt-text-settings-button =\n .title = Image alt text settings\npdfjs-image-alt-text-settings-button-label = Image alt text settings\npdfjs-editor-alt-text-settings-dialog-label = Image alt text settings\npdfjs-editor-alt-text-settings-automatic-title = Automatic alt text\npdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically\npdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB)\npdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text.\npdfjs-editor-alt-text-settings-delete-model-button = Delete\npdfjs-editor-alt-text-settings-download-model-button = Download\npdfjs-editor-alt-text-settings-downloading-model-button = Downloading\u2026\npdfjs-editor-alt-text-settings-editor-title = Alt text editor\npdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image\npdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text.\npdfjs-editor-alt-text-settings-close-button = Close\npdfjs-editor-undo-bar-message-highlight = Highlight removed\npdfjs-editor-undo-bar-message-freetext = Text removed\npdfjs-editor-undo-bar-message-ink = Drawing removed\npdfjs-editor-undo-bar-message-stamp = Image removed\npdfjs-editor-undo-bar-message-multiple =\n { $count ->\n [one] { $count } annotation removed\n *[other] { $count } annotations removed\n }\npdfjs-editor-undo-bar-undo-button =\n .title = Undo\npdfjs-editor-undo-bar-undo-button-label = Undo\npdfjs-editor-undo-bar-close-button =\n .title = Close\npdfjs-editor-undo-bar-close-button-label = Close"; + return createBundle(lang, text); + } +} + +;// ./web/pdf_history.js + + +const HASH_CHANGE_TIMEOUT = 1000; +const POSITION_UPDATED_THRESHOLD = 50; +const UPDATE_VIEWAREA_TIMEOUT = 1000; +function getCurrentHash() { + return document.location.hash; +} +class PDFHistory { + #eventAbortController = null; + constructor({ + linkService, + eventBus + }) { + this.linkService = linkService; + this.eventBus = eventBus; + this._initialized = false; + this._fingerprint = ""; + this.reset(); + this.eventBus._on("pagesinit", () => { + this._isPagesLoaded = false; + this.eventBus._on("pagesloaded", evt => { + this._isPagesLoaded = !!evt.pagesCount; + }, { + once: true + }); + }); + } + initialize({ + fingerprint, + resetHistory = false, + updateUrl = false + }) { + if (!fingerprint || typeof fingerprint !== "string") { + console.error('PDFHistory.initialize: The "fingerprint" must be a non-empty string.'); + return; + } + if (this._initialized) { + this.reset(); + } + const reInitialized = this._fingerprint !== "" && this._fingerprint !== fingerprint; + this._fingerprint = fingerprint; + this._updateUrl = updateUrl === true; + this._initialized = true; + this.#bindEvents(); + const state = window.history.state; + this._popStateInProgress = false; + this._blockHashChange = 0; + this._currentHash = getCurrentHash(); + this._numPositionUpdates = 0; + this._uid = this._maxUid = 0; + this._destination = null; + this._position = null; + if (!this.#isValidState(state, true) || resetHistory) { + const { + hash, + page, + rotation + } = this.#parseCurrentHash(true); + if (!hash || reInitialized || resetHistory) { + this.#pushOrReplaceState(null, true); + return; + } + this.#pushOrReplaceState({ + hash, + page, + rotation + }, true); + return; + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (destination.rotation !== undefined) { + this._initialRotation = destination.rotation; + } + if (destination.dest) { + this._initialBookmark = JSON.stringify(destination.dest); + this._destination.page = null; + } else if (destination.hash) { + this._initialBookmark = destination.hash; + } else if (destination.page) { + this._initialBookmark = `page=${destination.page}`; + } + } + reset() { + if (this._initialized) { + this.#pageHide(); + this._initialized = false; + this.#unbindEvents(); + } + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._initialBookmark = null; + this._initialRotation = null; + } + push({ + namedDest = null, + explicitDest, + pageNumber + }) { + if (!this._initialized) { + return; + } + if (namedDest && typeof namedDest !== "string") { + console.error("PDFHistory.push: " + `"${namedDest}" is not a valid namedDest parameter.`); + return; + } else if (!Array.isArray(explicitDest)) { + console.error("PDFHistory.push: " + `"${explicitDest}" is not a valid explicitDest parameter.`); + return; + } else if (!this.#isValidPage(pageNumber)) { + if (pageNumber !== null || this._destination) { + console.error("PDFHistory.push: " + `"${pageNumber}" is not a valid pageNumber parameter.`); + return; + } + } + const hash = namedDest || JSON.stringify(explicitDest); + if (!hash) { + return; + } + let forceReplace = false; + if (this._destination && (isDestHashesEqual(this._destination.hash, hash) || isDestArraysEqual(this._destination.dest, explicitDest))) { + if (this._destination.page) { + return; + } + forceReplace = true; + } + if (this._popStateInProgress && !forceReplace) { + return; + } + this.#pushOrReplaceState({ + dest: explicitDest, + hash, + page: pageNumber, + rotation: this.linkService.rotation + }, forceReplace); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushPage(pageNumber) { + if (!this._initialized) { + return; + } + if (!this.#isValidPage(pageNumber)) { + console.error(`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`); + return; + } + if (this._destination?.page === pageNumber) { + return; + } + if (this._popStateInProgress) { + return; + } + this.#pushOrReplaceState({ + dest: null, + hash: `page=${pageNumber}`, + page: pageNumber, + rotation: this.linkService.rotation + }); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushCurrentPosition() { + if (!this._initialized || this._popStateInProgress) { + return; + } + this.#tryPushCurrentPosition(); + } + back() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid > 0) { + window.history.back(); + } + } + forward() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid < this._maxUid) { + window.history.forward(); + } + } + get popStateInProgress() { + return this._initialized && (this._popStateInProgress || this._blockHashChange > 0); + } + get initialBookmark() { + return this._initialized ? this._initialBookmark : null; + } + get initialRotation() { + return this._initialized ? this._initialRotation : null; + } + #pushOrReplaceState(destination, forceReplace = false) { + const shouldReplace = forceReplace || !this._destination; + const newState = { + fingerprint: this._fingerprint, + uid: shouldReplace ? this._uid : this._uid + 1, + destination + }; + this.#updateInternalState(destination, newState.uid); + let newUrl; + if (this._updateUrl && destination?.hash) { + const baseUrl = document.location.href.split("#", 1)[0]; + if (!baseUrl.startsWith("file://")) { + newUrl = `${baseUrl}#${destination.hash}`; + } + } + if (shouldReplace) { + window.history.replaceState(newState, "", newUrl); + } else { + window.history.pushState(newState, "", newUrl); + } + } + #tryPushCurrentPosition(temporary = false) { + if (!this._position) { + return; + } + let position = this._position; + if (temporary) { + position = Object.assign(Object.create(null), this._position); + position.temporary = true; + } + if (!this._destination) { + this.#pushOrReplaceState(position); + return; + } + if (this._destination.temporary) { + this.#pushOrReplaceState(position, true); + return; + } + if (this._destination.hash === position.hash) { + return; + } + if (!this._destination.page && (POSITION_UPDATED_THRESHOLD <= 0 || this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) { + return; + } + let forceReplace = false; + if (this._destination.page >= position.first && this._destination.page <= position.page) { + if (this._destination.dest !== undefined || !this._destination.first) { + return; + } + forceReplace = true; + } + this.#pushOrReplaceState(position, forceReplace); + } + #isValidPage(val) { + return Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount; + } + #isValidState(state, checkReload = false) { + if (!state) { + return false; + } + if (state.fingerprint !== this._fingerprint) { + if (checkReload) { + if (typeof state.fingerprint !== "string" || state.fingerprint.length !== this._fingerprint.length) { + return false; + } + const [perfEntry] = performance.getEntriesByType("navigation"); + if (perfEntry?.type !== "reload") { + return false; + } + } else { + return false; + } + } + if (!Number.isInteger(state.uid) || state.uid < 0) { + return false; + } + if (state.destination === null || typeof state.destination !== "object") { + return false; + } + return true; + } + #updateInternalState(destination, uid, removeTemporary = false) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + if (removeTemporary && destination?.temporary) { + delete destination.temporary; + } + this._destination = destination; + this._uid = uid; + this._maxUid = Math.max(this._maxUid, uid); + this._numPositionUpdates = 0; + } + #parseCurrentHash(checkNameddest = false) { + const hash = unescape(getCurrentHash()).substring(1); + const params = parseQueryString(hash); + const nameddest = params.get("nameddest") || ""; + let page = params.get("page") | 0; + if (!this.#isValidPage(page) || checkNameddest && nameddest.length > 0) { + page = null; + } + return { + hash, + page, + rotation: this.linkService.rotation + }; + } + #updateViewarea({ + location + }) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._position = { + hash: location.pdfOpenParams.substring(1), + page: this.linkService.page, + first: location.pageNumber, + rotation: location.rotation + }; + if (this._popStateInProgress) { + return; + } + if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && this._destination && !this._destination.page) { + this._numPositionUpdates++; + } + if (UPDATE_VIEWAREA_TIMEOUT > 0) { + this._updateViewareaTimeout = setTimeout(() => { + if (!this._popStateInProgress) { + this.#tryPushCurrentPosition(true); + } + this._updateViewareaTimeout = null; + }, UPDATE_VIEWAREA_TIMEOUT); + } + } + #popState({ + state + }) { + const newHash = getCurrentHash(), + hashChanged = this._currentHash !== newHash; + this._currentHash = newHash; + if (!state) { + this._uid++; + const { + hash, + page, + rotation + } = this.#parseCurrentHash(); + this.#pushOrReplaceState({ + hash, + page, + rotation + }, true); + return; + } + if (!this.#isValidState(state)) { + return; + } + this._popStateInProgress = true; + if (hashChanged) { + this._blockHashChange++; + waitOnEventOrTimeout({ + target: window, + name: "hashchange", + delay: HASH_CHANGE_TIMEOUT + }).then(() => { + this._blockHashChange--; + }); + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (isValidRotation(destination.rotation)) { + this.linkService.rotation = destination.rotation; + } + if (destination.dest) { + this.linkService.goToDestination(destination.dest); + } else if (destination.hash) { + this.linkService.setHash(destination.hash); + } else if (destination.page) { + this.linkService.page = destination.page; + } + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + #pageHide() { + if (!this._destination || this._destination.temporary) { + this.#tryPushCurrentPosition(); + } + } + #bindEvents() { + if (this.#eventAbortController) { + return; + } + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), { + signal + }); + window.addEventListener("popstate", this.#popState.bind(this), { + signal + }); + window.addEventListener("pagehide", this.#pageHide.bind(this), { + signal + }); + } + #unbindEvents() { + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } +} +function isDestHashesEqual(destHash, pushHash) { + if (typeof destHash !== "string" || typeof pushHash !== "string") { + return false; + } + if (destHash === pushHash) { + return true; + } + const nameddest = parseQueryString(destHash).get("nameddest"); + if (nameddest === pushHash) { + return true; + } + return false; +} +function isDestArraysEqual(firstDest, secondDest) { + function isEntryEqual(first, second) { + if (typeof first !== typeof second) { + return false; + } + if (Array.isArray(first) || Array.isArray(second)) { + return false; + } + if (first !== null && typeof first === "object" && second !== null) { + if (Object.keys(first).length !== Object.keys(second).length) { + return false; + } + for (const key in first) { + if (!isEntryEqual(first[key], second[key])) { + return false; + } + } + return true; + } + return first === second || Number.isNaN(first) && Number.isNaN(second); + } + if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) { + return false; + } + if (firstDest.length !== secondDest.length) { + return false; + } + for (let i = 0, ii = firstDest.length; i < ii; i++) { + if (!isEntryEqual(firstDest[i], secondDest[i])) { + return false; + } + } + return true; +} + +;// ./web/annotation_editor_layer_builder.js + + +class AnnotationEditorLayerBuilder { + #annotationLayer = null; + #drawLayer = null; + #onAppend = null; + #structTreeLayer = null; + #textLayer = null; + #uiManager; + constructor(options) { + this.pdfPage = options.pdfPage; + this.accessibilityManager = options.accessibilityManager; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.annotationEditorLayer = null; + this.div = null; + this._cancelled = false; + this.#uiManager = options.uiManager; + this.#annotationLayer = options.annotationLayer || null; + this.#textLayer = options.textLayer || null; + this.#drawLayer = options.drawLayer || null; + this.#onAppend = options.onAppend || null; + this.#structTreeLayer = options.structTreeLayer || null; + } + async render(viewport, intent = "display") { + if (intent !== "display") { + return; + } + if (this._cancelled) { + return; + } + const clonedViewport = viewport.clone({ + dontFlip: true + }); + if (this.div) { + this.annotationEditorLayer.update({ + viewport: clonedViewport + }); + this.show(); + return; + } + const div = this.div = document.createElement("div"); + div.className = "annotationEditorLayer"; + div.hidden = true; + div.dir = this.#uiManager.direction; + this.#onAppend?.(div); + this.annotationEditorLayer = new AnnotationEditorLayer({ + uiManager: this.#uiManager, + div, + structTreeLayer: this.#structTreeLayer, + accessibilityManager: this.accessibilityManager, + pageIndex: this.pdfPage.pageNumber - 1, + l10n: this.l10n, + viewport: clonedViewport, + annotationLayer: this.#annotationLayer, + textLayer: this.#textLayer, + drawLayer: this.#drawLayer + }); + const parameters = { + viewport: clonedViewport, + div, + annotations: null, + intent + }; + this.annotationEditorLayer.render(parameters); + this.show(); + } + cancel() { + this._cancelled = true; + if (!this.div) { + return; + } + this.annotationEditorLayer.destroy(); + } + hide() { + if (!this.div) { + return; + } + this.annotationEditorLayer.pause(true); + this.div.hidden = true; + } + show() { + if (!this.div || this.annotationEditorLayer.isInvisible) { + return; + } + this.div.hidden = false; + this.annotationEditorLayer.pause(false); + } +} + +;// ./web/app_options.js +{ + var compatParams = new Map(); + const userAgent = navigator.userAgent || ""; + const platform = navigator.platform || ""; + const maxTouchPoints = navigator.maxTouchPoints || 1; + const isAndroid = /Android/.test(userAgent); + const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1; + (function () { + if (isIOS || isAndroid) { + compatParams.set("maxCanvasPixels", 5242880); + } + })(); + (function () { + if (isAndroid) { + compatParams.set("useSystemFonts", false); + } + })(); +} +const OptionKind = { + BROWSER: 0x01, + VIEWER: 0x02, + API: 0x04, + WORKER: 0x08, + EVENT_DISPATCH: 0x10, + PREFERENCE: 0x80 +}; +const Type = { + BOOLEAN: 0x01, + NUMBER: 0x02, + OBJECT: 0x04, + STRING: 0x08, + UNDEFINED: 0x10 +}; +const defaultOptions = { + allowedGlobalEvents: { + value: null, + kind: OptionKind.BROWSER + }, + canvasMaxAreaInBytes: { + value: -1, + kind: OptionKind.BROWSER + OptionKind.API + }, + isInAutomation: { + value: false, + kind: OptionKind.BROWSER + }, + localeProperties: { + value: { + lang: navigator.language || "en-US" + }, + kind: OptionKind.BROWSER + }, + nimbusDataStr: { + value: "", + kind: OptionKind.BROWSER + }, + supportsCaretBrowsingMode: { + value: false, + kind: OptionKind.BROWSER + }, + supportsDocumentFonts: { + value: true, + kind: OptionKind.BROWSER + }, + supportsIntegratedFind: { + value: false, + kind: OptionKind.BROWSER + }, + supportsMouseWheelZoomCtrlKey: { + value: true, + kind: OptionKind.BROWSER + }, + supportsMouseWheelZoomMetaKey: { + value: true, + kind: OptionKind.BROWSER + }, + supportsPinchToZoom: { + value: true, + kind: OptionKind.BROWSER + }, + toolbarDensity: { + value: 0, + kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH + }, + altTextLearnMoreUrl: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + annotationEditorMode: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + annotationMode: { + value: 2, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + cursorToolOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + debuggerSrc: { + value: "./debugger.mjs", + kind: OptionKind.VIEWER + }, + defaultZoomDelay: { + value: 400, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + defaultZoomValue: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + disableHistory: { + value: false, + kind: OptionKind.VIEWER + }, + disablePageLabels: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableAltText: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableAltTextModelDownload: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH + }, + enableGuessAltText: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH + }, + enableHighlightFloatingButton: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableNewAltTextWhenAddingImage: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enablePermissions: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enablePrintAutoRotate: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableScripting: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableUpdatedAddImage: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + externalLinkRel: { + value: "noopener noreferrer nofollow", + kind: OptionKind.VIEWER + }, + externalLinkTarget: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + highlightEditorColors: { + value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + historyUpdateUrl: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + ignoreDestinationZoom: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + imageResourcesPath: { + value: "./images/", + kind: OptionKind.VIEWER + }, + maxCanvasPixels: { + value: 2 ** 25, + kind: OptionKind.VIEWER + }, + forcePageColors: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pageColorsBackground: { + value: "Canvas", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pageColorsForeground: { + value: "CanvasText", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + pdfBugEnabled: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + printResolution: { + value: 150, + kind: OptionKind.VIEWER + }, + sidebarViewOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + scrollModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + spreadModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + textLayerMode: { + value: 1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + viewOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }, + cMapPacked: { + value: true, + kind: OptionKind.API + }, + cMapUrl: { + value: "../web/cmaps/", + kind: OptionKind.API + }, + disableAutoFetch: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableFontFace: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableRange: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + disableStream: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + docBaseUrl: { + value: "", + kind: OptionKind.API + }, + enableHWA: { + value: true, + kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE + }, + enableXfa: { + value: true, + kind: OptionKind.API + OptionKind.PREFERENCE + }, + fontExtraProperties: { + value: false, + kind: OptionKind.API + }, + isEvalSupported: { + value: true, + kind: OptionKind.API + }, + isOffscreenCanvasSupported: { + value: true, + kind: OptionKind.API + }, + maxImageSize: { + value: -1, + kind: OptionKind.API + }, + pdfBug: { + value: false, + kind: OptionKind.API + }, + standardFontDataUrl: { + value: "../web/standard_fonts/", + kind: OptionKind.API + }, + useSystemFonts: { + value: undefined, + kind: OptionKind.API, + type: Type.BOOLEAN + Type.UNDEFINED + }, + verbosity: { + value: 1, + kind: OptionKind.API + }, + workerPort: { + value: null, + kind: OptionKind.WORKER + }, + workerSrc: { + value: "../build/pdf.worker.mjs", + kind: OptionKind.WORKER + } +}; +{ + defaultOptions.defaultUrl = { + value: "compressed.tracemonkey-pldi-09.pdf", + kind: OptionKind.VIEWER + }; + defaultOptions.sandboxBundleSrc = { + value: "../build/pdf.sandbox.mjs", + kind: OptionKind.VIEWER + }; + defaultOptions.viewerCssTheme = { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + }; + defaultOptions.enableFakeMLManager = { + value: true, + kind: OptionKind.VIEWER + }; +} +{ + defaultOptions.disablePreferences = { + value: false, + kind: OptionKind.VIEWER + }; +} +class AppOptions { + static eventBus; + static #opts = new Map(); + static { + for (const name in defaultOptions) { + this.#opts.set(name, defaultOptions[name].value); + } + for (const [name, value] of compatParams) { + this.#opts.set(name, value); + } + this._hasInvokedSet = false; + this._checkDisablePreferences = () => { + if (this.get("disablePreferences")) { + return true; + } + if (this._hasInvokedSet) { + console.warn("The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option to prevent that.'); + } + return false; + }; + } + static get(name) { + return this.#opts.get(name); + } + static getAll(kind = null, defaultOnly = false) { + const options = Object.create(null); + for (const name in defaultOptions) { + const defaultOpt = defaultOptions[name]; + if (kind && !(kind & defaultOpt.kind)) { + continue; + } + options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value; + } + return options; + } + static set(name, value) { + this.setAll({ + [name]: value + }); + } + static setAll(options, prefs = false) { + this._hasInvokedSet ||= true; + let events; + for (const name in options) { + const defaultOpt = defaultOptions[name], + userOpt = options[name]; + if (!defaultOpt || !(typeof userOpt === typeof defaultOpt.value || Type[(typeof userOpt).toUpperCase()] & defaultOpt.type)) { + continue; + } + const { + kind + } = defaultOpt; + if (prefs && !(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { + continue; + } + if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { + (events ||= new Map()).set(name, userOpt); + } + this.#opts.set(name, userOpt); + } + if (events) { + for (const [name, value] of events) { + this.eventBus.dispatch(name.toLowerCase(), { + source: this, + value + }); + } + } + } +} + +;// ./web/draw_layer_builder.js + +class DrawLayerBuilder { + #drawLayer = null; + constructor(options) { + this.pageIndex = options.pageIndex; + } + async render(intent = "display") { + if (intent !== "display" || this.#drawLayer || this._cancelled) { + return; + } + this.#drawLayer = new DrawLayer({ + pageIndex: this.pageIndex + }); + } + cancel() { + this._cancelled = true; + if (!this.#drawLayer) { + return; + } + this.#drawLayer.destroy(); + this.#drawLayer = null; + } + setParent(parent) { + this.#drawLayer?.setParent(parent); + } + getDrawLayer() { + return this.#drawLayer; + } +} + +;// ./web/struct_tree_layer_builder.js + +const PDF_ROLE_TO_HTML_ROLE = { + Document: null, + DocumentFragment: null, + Part: "group", + Sect: "group", + Div: "group", + Aside: "note", + NonStruct: "none", + P: null, + H: "heading", + Title: null, + FENote: "note", + Sub: "group", + Lbl: null, + Span: null, + Em: null, + Strong: null, + Link: "link", + Annot: "note", + Form: "form", + Ruby: null, + RB: null, + RT: null, + RP: null, + Warichu: null, + WT: null, + WP: null, + L: "list", + LI: "listitem", + LBody: null, + Table: "table", + TR: "row", + TH: "columnheader", + TD: "cell", + THead: "columnheader", + TBody: null, + TFoot: null, + Caption: null, + Figure: "figure", + Formula: null, + Artifact: null +}; +const HEADING_PATTERN = /^H(\d+)$/; +class StructTreeLayerBuilder { + #promise; + #treeDom = null; + #treePromise; + #elementAttributes = new Map(); + #rawDims; + #elementsToAddToTextLayer = null; + constructor(pdfPage, rawDims) { + this.#promise = pdfPage.getStructTree(); + this.#rawDims = rawDims; + } + async render() { + if (this.#treePromise) { + return this.#treePromise; + } + const { + promise, + resolve, + reject + } = Promise.withResolvers(); + this.#treePromise = promise; + try { + this.#treeDom = this.#walk(await this.#promise); + } catch (ex) { + reject(ex); + } + this.#promise = null; + this.#treeDom?.classList.add("structTree"); + resolve(this.#treeDom); + return promise; + } + async getAriaAttributes(annotationId) { + try { + await this.render(); + return this.#elementAttributes.get(annotationId); + } catch {} + return null; + } + hide() { + if (this.#treeDom && !this.#treeDom.hidden) { + this.#treeDom.hidden = true; + } + } + show() { + if (this.#treeDom?.hidden) { + this.#treeDom.hidden = false; + } + } + #setAttributes(structElement, htmlElement) { + const { + alt, + id, + lang + } = structElement; + if (alt !== undefined) { + let added = false; + const label = removeNullCharacters(alt); + for (const child of structElement.children) { + if (child.type === "annotation") { + let attrs = this.#elementAttributes.get(child.id); + if (!attrs) { + attrs = new Map(); + this.#elementAttributes.set(child.id, attrs); + } + attrs.set("aria-label", label); + added = true; + } + } + if (!added) { + htmlElement.setAttribute("aria-label", label); + } + } + if (id !== undefined) { + htmlElement.setAttribute("aria-owns", id); + } + if (lang !== undefined) { + htmlElement.setAttribute("lang", removeNullCharacters(lang, true)); + } + } + #addImageInTextLayer(node, element) { + const { + alt, + bbox, + children + } = node; + const child = children?.[0]; + if (!this.#rawDims || !alt || !bbox || child?.type !== "content") { + return false; + } + const { + id + } = child; + if (!id) { + return false; + } + element.setAttribute("aria-owns", id); + const img = document.createElement("span"); + (this.#elementsToAddToTextLayer ||= new Map()).set(id, img); + img.setAttribute("role", "img"); + img.setAttribute("aria-label", removeNullCharacters(alt)); + const { + pageHeight, + pageX, + pageY + } = this.#rawDims; + const calc = "calc(var(--scale-factor)*"; + const { + style + } = img; + style.width = `${calc}${bbox[2] - bbox[0]}px)`; + style.height = `${calc}${bbox[3] - bbox[1]}px)`; + style.left = `${calc}${bbox[0] - pageX}px)`; + style.top = `${calc}${pageHeight - bbox[3] + pageY}px)`; + return true; + } + addElementsToTextLayer() { + if (!this.#elementsToAddToTextLayer) { + return; + } + for (const [id, img] of this.#elementsToAddToTextLayer) { + document.getElementById(id)?.append(img); + } + this.#elementsToAddToTextLayer.clear(); + this.#elementsToAddToTextLayer = null; + } + #walk(node) { + if (!node) { + return null; + } + const element = document.createElement("span"); + if ("role" in node) { + const { + role + } = node; + const match = role.match(HEADING_PATTERN); + if (match) { + element.setAttribute("role", "heading"); + element.setAttribute("aria-level", match[1]); + } else if (PDF_ROLE_TO_HTML_ROLE[role]) { + element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]); + } + if (role === "Figure" && this.#addImageInTextLayer(node, element)) { + return element; + } + } + this.#setAttributes(node, element); + if (node.children) { + if (node.children.length === 1 && "id" in node.children[0]) { + this.#setAttributes(node.children[0], element); + } else { + for (const kid of node.children) { + element.append(this.#walk(kid)); + } + } + } + return element; + } +} + +;// ./web/text_accessibility.js + +class TextAccessibilityManager { + #enabled = false; + #textChildren = null; + #textNodes = new Map(); + #waitingElements = new Map(); + setTextMapping(textDivs) { + this.#textChildren = textDivs; + } + static #compareElementPositions(e1, e2) { + const rect1 = e1.getBoundingClientRect(); + const rect2 = e2.getBoundingClientRect(); + if (rect1.width === 0 && rect1.height === 0) { + return +1; + } + if (rect2.width === 0 && rect2.height === 0) { + return -1; + } + const top1 = rect1.y; + const bot1 = rect1.y + rect1.height; + const mid1 = rect1.y + rect1.height / 2; + const top2 = rect2.y; + const bot2 = rect2.y + rect2.height; + const mid2 = rect2.y + rect2.height / 2; + if (mid1 <= top2 && mid2 >= bot1) { + return -1; + } + if (mid2 <= top1 && mid1 >= bot2) { + return +1; + } + const centerX1 = rect1.x + rect1.width / 2; + const centerX2 = rect2.x + rect2.width / 2; + return centerX1 - centerX2; + } + enable() { + if (this.#enabled) { + throw new Error("TextAccessibilityManager is already enabled."); + } + if (!this.#textChildren) { + throw new Error("Text divs and strings have not been set."); + } + this.#enabled = true; + this.#textChildren = this.#textChildren.slice(); + this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions); + if (this.#textNodes.size > 0) { + const textChildren = this.#textChildren; + for (const [id, nodeIndex] of this.#textNodes) { + const element = document.getElementById(id); + if (!element) { + this.#textNodes.delete(id); + continue; + } + this.#addIdToAriaOwns(id, textChildren[nodeIndex]); + } + } + for (const [element, isRemovable] of this.#waitingElements) { + this.addPointerInTextLayer(element, isRemovable); + } + this.#waitingElements.clear(); + } + disable() { + if (!this.#enabled) { + return; + } + this.#waitingElements.clear(); + this.#textChildren = null; + this.#enabled = false; + } + removePointerInTextLayer(element) { + if (!this.#enabled) { + this.#waitingElements.delete(element); + return; + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return; + } + const { + id + } = element; + const nodeIndex = this.#textNodes.get(id); + if (nodeIndex === undefined) { + return; + } + const node = children[nodeIndex]; + this.#textNodes.delete(id); + let owns = node.getAttribute("aria-owns"); + if (owns?.includes(id)) { + owns = owns.split(" ").filter(x => x !== id).join(" "); + if (owns) { + node.setAttribute("aria-owns", owns); + } else { + node.removeAttribute("aria-owns"); + node.setAttribute("role", "presentation"); + } + } + } + #addIdToAriaOwns(id, node) { + const owns = node.getAttribute("aria-owns"); + if (!owns?.includes(id)) { + node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id); + } + node.removeAttribute("role"); + } + addPointerInTextLayer(element, isRemovable) { + const { + id + } = element; + if (!id) { + return null; + } + if (!this.#enabled) { + this.#waitingElements.set(element, isRemovable); + return null; + } + if (isRemovable) { + this.removePointerInTextLayer(element); + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return null; + } + const index = binarySearchFirstItem(children, node => TextAccessibilityManager.#compareElementPositions(element, node) < 0); + const nodeIndex = Math.max(0, index - 1); + const child = children[nodeIndex]; + this.#addIdToAriaOwns(id, child); + this.#textNodes.set(id, nodeIndex); + const parent = child.parentNode; + return parent?.classList.contains("markedContent") ? parent.id : null; + } + moveElementInDOM(container, element, contentElement, isRemovable) { + const id = this.addPointerInTextLayer(contentElement, isRemovable); + if (!container.hasChildNodes()) { + container.append(element); + return id; + } + const children = Array.from(container.childNodes).filter(node => node !== element); + if (children.length === 0) { + return id; + } + const elementToCompare = contentElement || element; + const index = binarySearchFirstItem(children, node => TextAccessibilityManager.#compareElementPositions(elementToCompare, node) < 0); + if (index === 0) { + children[0].before(element); + } else { + children[index - 1].after(element); + } + return id; + } +} + +;// ./web/text_highlighter.js +class TextHighlighter { + #eventAbortController = null; + constructor({ + findController, + eventBus, + pageIndex + }) { + this.findController = findController; + this.matches = []; + this.eventBus = eventBus; + this.pageIdx = pageIndex; + this.textDivs = null; + this.textContentItemsStr = null; + this.enabled = false; + } + setTextMapping(divs, texts) { + this.textDivs = divs; + this.textContentItemsStr = texts; + } + enable() { + if (!this.textDivs || !this.textContentItemsStr) { + throw new Error("Text divs and strings have not been set."); + } + if (this.enabled) { + throw new Error("TextHighlighter is already enabled."); + } + this.enabled = true; + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this.eventBus._on("updatetextlayermatches", evt => { + if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) { + this._updateMatches(); + } + }, { + signal: this.#eventAbortController.signal + }); + } + this._updateMatches(); + } + disable() { + if (!this.enabled) { + return; + } + this.enabled = false; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._updateMatches(true); + } + _convertMatches(matches, matchesLength) { + if (!matches) { + return []; + } + const { + textContentItemsStr + } = this; + let i = 0, + iIndex = 0; + const end = textContentItemsStr.length - 1; + const result = []; + for (let m = 0, mm = matches.length; m < mm; m++) { + let matchIdx = matches[m]; + while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + if (i === textContentItemsStr.length) { + console.error("Could not find a matching mapping"); + } + const match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex + } + }; + matchIdx += matchesLength[m]; + while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + match.end = { + divIdx: i, + offset: matchIdx - iIndex + }; + result.push(match); + } + return result; + } + _renderMatches(matches) { + if (matches.length === 0) { + return; + } + const { + findController, + pageIdx + } = this; + const { + textContentItemsStr, + textDivs + } = this; + const isSelectedPage = pageIdx === findController.selected.pageIdx; + const selectedMatchIdx = findController.selected.matchIdx; + const highlightAll = findController.state.highlightAll; + let prevEnd = null; + const infinity = { + divIdx: -1, + offset: undefined + }; + function beginText(begin, className) { + const divIdx = begin.divIdx; + textDivs[divIdx].textContent = ""; + return appendTextToDiv(divIdx, 0, begin.offset, className); + } + function appendTextToDiv(divIdx, fromOffset, toOffset, className) { + let div = textDivs[divIdx]; + if (div.nodeType === Node.TEXT_NODE) { + const span = document.createElement("span"); + div.before(span); + span.append(div); + textDivs[divIdx] = span; + div = span; + } + const content = textContentItemsStr[divIdx].substring(fromOffset, toOffset); + const node = document.createTextNode(content); + if (className) { + const span = document.createElement("span"); + span.className = `${className} appended`; + span.append(node); + div.append(span); + if (className.includes("selected")) { + const { + left + } = span.getClientRects()[0]; + const parentLeft = div.getBoundingClientRect().left; + return left - parentLeft; + } + return 0; + } + div.append(node); + return 0; + } + let i0 = selectedMatchIdx, + i1 = i0 + 1; + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + return; + } + let lastDivIdx = -1; + let lastOffset = -1; + for (let i = i0; i < i1; i++) { + const match = matches[i]; + const begin = match.begin; + if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) { + continue; + } + lastDivIdx = begin.divIdx; + lastOffset = begin.offset; + const end = match.end; + const isSelected = isSelectedPage && i === selectedMatchIdx; + const highlightSuffix = isSelected ? " selected" : ""; + let selectedLeft = 0; + if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { + if (prevEnd !== null) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + beginText(begin); + } else { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); + } + if (begin.divIdx === end.divIdx) { + selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, end.offset, "highlight" + highlightSuffix); + } else { + selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, "highlight begin" + highlightSuffix); + for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { + textDivs[n0].className = "highlight middle" + highlightSuffix; + } + beginText(end, "highlight end" + highlightSuffix); + } + prevEnd = end; + if (isSelected) { + findController.scrollMatchIntoView({ + element: textDivs[begin.divIdx], + selectedLeft, + pageIndex: pageIdx, + matchIndex: selectedMatchIdx + }); + } + } + if (prevEnd) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + } + _updateMatches(reset = false) { + if (!this.enabled && !reset) { + return; + } + const { + findController, + matches, + pageIdx + } = this; + const { + textContentItemsStr, + textDivs + } = this; + let clearedUntilDivIdx = -1; + for (const match of matches) { + const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (let n = begin, end = match.end.divIdx; n <= end; n++) { + const div = textDivs[n]; + div.textContent = textContentItemsStr[n]; + div.className = ""; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } + if (!findController?.highlightMatches || reset) { + return; + } + const pageMatches = findController.pageMatches[pageIdx] || null; + const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null; + this.matches = this._convertMatches(pageMatches, pageMatchesLength); + this._renderMatches(this.matches); + } +} + +;// ./web/text_layer_builder.js + + +class TextLayerBuilder { + #enablePermissions = false; + #onAppend = null; + #renderingDone = false; + #textLayer = null; + static #textLayers = new Map(); + static #selectionChangeAbortController = null; + constructor({ + pdfPage, + highlighter = null, + accessibilityManager = null, + enablePermissions = false, + onAppend = null + }) { + this.pdfPage = pdfPage; + this.highlighter = highlighter; + this.accessibilityManager = accessibilityManager; + this.#enablePermissions = enablePermissions === true; + this.#onAppend = onAppend; + this.div = document.createElement("div"); + this.div.tabIndex = 0; + this.div.className = "textLayer"; + } + async render(viewport, textContentParams = null) { + if (this.#renderingDone && this.#textLayer) { + this.#textLayer.update({ + viewport, + onBefore: this.hide.bind(this) + }); + this.show(); + return; + } + this.cancel(); + this.#textLayer = new TextLayer({ + textContentSource: this.pdfPage.streamTextContent(textContentParams || { + includeMarkedContent: true, + disableNormalization: true + }), + container: this.div, + viewport + }); + const { + textDivs, + textContentItemsStr + } = this.#textLayer; + this.highlighter?.setTextMapping(textDivs, textContentItemsStr); + this.accessibilityManager?.setTextMapping(textDivs); + await this.#textLayer.render(); + this.#renderingDone = true; + const endOfContent = document.createElement("div"); + endOfContent.className = "endOfContent"; + this.div.append(endOfContent); + this.#bindMouse(endOfContent); + this.#onAppend?.(this.div); + this.highlighter?.enable(); + this.accessibilityManager?.enable(); + } + hide() { + if (!this.div.hidden && this.#renderingDone) { + this.highlighter?.disable(); + this.div.hidden = true; + } + } + show() { + if (this.div.hidden && this.#renderingDone) { + this.div.hidden = false; + this.highlighter?.enable(); + } + } + cancel() { + this.#textLayer?.cancel(); + this.#textLayer = null; + this.highlighter?.disable(); + this.accessibilityManager?.disable(); + TextLayerBuilder.#removeGlobalSelectionListener(this.div); + } + #bindMouse(end) { + const { + div + } = this; + div.addEventListener("mousedown", () => { + div.classList.add("selecting"); + }); + div.addEventListener("copy", event => { + if (!this.#enablePermissions) { + const selection = document.getSelection(); + event.clipboardData.setData("text/plain", removeNullCharacters(normalizeUnicode(selection.toString()))); + } + stopEvent(event); + }); + TextLayerBuilder.#textLayers.set(div, end); + TextLayerBuilder.#enableGlobalSelectionListener(); + } + static #removeGlobalSelectionListener(textLayerDiv) { + this.#textLayers.delete(textLayerDiv); + if (this.#textLayers.size === 0) { + this.#selectionChangeAbortController?.abort(); + this.#selectionChangeAbortController = null; + } + } + static #enableGlobalSelectionListener() { + if (this.#selectionChangeAbortController) { + return; + } + this.#selectionChangeAbortController = new AbortController(); + const { + signal + } = this.#selectionChangeAbortController; + const reset = (end, textLayer) => { + textLayer.append(end); + end.style.width = ""; + end.style.height = ""; + textLayer.classList.remove("selecting"); + }; + let isPointerDown = false; + document.addEventListener("pointerdown", () => { + isPointerDown = true; + }, { + signal + }); + document.addEventListener("pointerup", () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, { + signal + }); + window.addEventListener("blur", () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, { + signal + }); + document.addEventListener("keyup", () => { + if (!isPointerDown) { + this.#textLayers.forEach(reset); + } + }, { + signal + }); + var isFirefox, prevRange; + document.addEventListener("selectionchange", () => { + const selection = document.getSelection(); + if (selection.rangeCount === 0) { + this.#textLayers.forEach(reset); + return; + } + const activeTextLayers = new Set(); + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + for (const textLayerDiv of this.#textLayers.keys()) { + if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) { + activeTextLayers.add(textLayerDiv); + } + } + } + for (const [textLayerDiv, endDiv] of this.#textLayers) { + if (activeTextLayers.has(textLayerDiv)) { + textLayerDiv.classList.add("selecting"); + } else { + reset(endDiv, textLayerDiv); + } + } + isFirefox ??= getComputedStyle(this.#textLayers.values().next().value).getPropertyValue("-moz-user-select") === "none"; + if (isFirefox) { + return; + } + const range = selection.getRangeAt(0); + const modifyStart = prevRange && (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0); + let anchor = modifyStart ? range.startContainer : range.endContainer; + if (anchor.nodeType === Node.TEXT_NODE) { + anchor = anchor.parentNode; + } + const parentTextLayer = anchor.parentElement?.closest(".textLayer"); + const endDiv = this.#textLayers.get(parentTextLayer); + if (endDiv) { + endDiv.style.width = parentTextLayer.style.width; + endDiv.style.height = parentTextLayer.style.height; + anchor.parentElement.insertBefore(endDiv, modifyStart ? anchor : anchor.nextSibling); + } + prevRange = range.cloneRange(); + }, { + signal + }); + } +} + +;// ./web/xfa_layer_builder.js + +class XfaLayerBuilder { + constructor({ + pdfPage, + annotationStorage = null, + linkService, + xfaHtml = null + }) { + this.pdfPage = pdfPage; + this.annotationStorage = annotationStorage; + this.linkService = linkService; + this.xfaHtml = xfaHtml; + this.div = null; + this._cancelled = false; + } + async render(viewport, intent = "display") { + if (intent === "print") { + const parameters = { + viewport: viewport.clone({ + dontFlip: true + }), + div: this.div, + xfaHtml: this.xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent + }; + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + const xfaHtml = await this.pdfPage.getXfa(); + if (this._cancelled || !xfaHtml) { + return { + textDivs: [] + }; + } + const parameters = { + viewport: viewport.clone({ + dontFlip: true + }), + div: this.div, + xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent + }; + if (this.div) { + return XfaLayer.update(parameters); + } + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + cancel() { + this._cancelled = true; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } +} + +;// ./web/pdf_page_view.js + + + + + + + + + + + + + +const DEFAULT_LAYER_PROPERTIES = { + annotationEditorUIManager: null, + annotationStorage: null, + downloadManager: null, + enableScripting: false, + fieldObjectsPromise: null, + findController: null, + hasJSActionsPromise: null, + get linkService() { + return new SimpleLinkService(); + } +}; +const LAYERS_ORDER = new Map([["canvasWrapper", 0], ["textLayer", 1], ["annotationLayer", 2], ["annotationEditorLayer", 3], ["xfaLayer", 3]]); +class PDFPageView { + #annotationMode = AnnotationMode.ENABLE_FORMS; + #canvasWrapper = null; + #enableHWA = false; + #hasRestrictedScaling = false; + #isEditing = false; + #layerProperties = null; + #loadingId = null; + #originalViewport = null; + #previousRotation = null; + #scaleRoundX = 1; + #scaleRoundY = 1; + #renderError = null; + #renderingState = RenderingStates.INITIAL; + #textLayerMode = TextLayerMode.ENABLE; + #useThumbnailCanvas = { + directDrawing: true, + initialOptionalContent: true, + regularAnnotations: true + }; + #layers = [null, null, null, null]; + constructor(options) { + const container = options.container; + const defaultViewport = options.defaultViewport; + this.id = options.id; + this.renderingId = "page" + this.id; + this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES; + this.pdfPage = null; + this.pageLabel = null; + this.rotation = 0; + this.scale = options.scale || DEFAULT_SCALE; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this._optionalContentConfigPromise = options.optionalContentConfigPromise || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); + this.pageColors = options.pageColors || null; + this.#enableHWA = options.enableHWA || false; + this.eventBus = options.eventBus; + this.renderingQueue = options.renderingQueue; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.renderTask = null; + this.resume = null; + this._isStandalone = !this.renderingQueue?.hasViewer(); + this._container = container; + this._annotationCanvasMap = null; + this.annotationLayer = null; + this.annotationEditorLayer = null; + this.textLayer = null; + this.xfaLayer = null; + this.structTreeLayer = null; + this.drawLayer = null; + const div = document.createElement("div"); + div.className = "page"; + div.setAttribute("data-page-number", this.id); + div.setAttribute("role", "region"); + div.setAttribute("data-l10n-id", "pdfjs-page-landmark"); + div.setAttribute("data-l10n-args", JSON.stringify({ + page: this.id + })); + this.div = div; + this.#setDimensions(); + container?.append(div); + if (this._isStandalone) { + container?.style.setProperty("--scale-factor", this.scale * PixelsPerInch.PDF_TO_CSS_UNITS); + if (this.pageColors?.background) { + container?.style.setProperty("--page-bg-color", this.pageColors.background); + } + const { + optionalContentConfigPromise + } = options; + if (optionalContentConfigPromise) { + optionalContentConfigPromise.then(optionalContentConfig => { + if (optionalContentConfigPromise !== this._optionalContentConfigPromise) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility; + }); + } + if (!options.l10n) { + this.l10n.translate(this.div); + } + } + } + #addLayer(div, name) { + const pos = LAYERS_ORDER.get(name); + const oldDiv = this.#layers[pos]; + this.#layers[pos] = div; + if (oldDiv) { + oldDiv.replaceWith(div); + return; + } + for (let i = pos - 1; i >= 0; i--) { + const layer = this.#layers[i]; + if (layer) { + layer.after(div); + return; + } + } + this.div.prepend(div); + } + get renderingState() { + return this.#renderingState; + } + set renderingState(state) { + if (state === this.#renderingState) { + return; + } + this.#renderingState = state; + if (this.#loadingId) { + clearTimeout(this.#loadingId); + this.#loadingId = null; + } + switch (state) { + case RenderingStates.PAUSED: + this.div.classList.remove("loading"); + break; + case RenderingStates.RUNNING: + this.div.classList.add("loadingIcon"); + this.#loadingId = setTimeout(() => { + this.div.classList.add("loading"); + this.#loadingId = null; + }, 0); + break; + case RenderingStates.INITIAL: + case RenderingStates.FINISHED: + this.div.classList.remove("loadingIcon", "loading"); + break; + } + } + #setDimensions() { + const { + viewport + } = this; + if (this.pdfPage) { + if (this.#previousRotation === viewport.rotation) { + return; + } + this.#previousRotation = viewport.rotation; + } + setLayerDimensions(this.div, viewport, true, false); + } + setPdfPage(pdfPage) { + if (this._isStandalone && (this.pageColors?.foreground === "CanvasText" || this.pageColors?.background === "Canvas")) { + this._container?.style.setProperty("--hcm-highlight-filter", pdfPage.filterFactory.addHighlightHCMFilter("highlight", "CanvasText", "Canvas", "HighlightText", "Highlight")); + this._container?.style.setProperty("--hcm-highlight-selected-filter", pdfPage.filterFactory.addHighlightHCMFilter("highlight_selected", "CanvasText", "Canvas", "HighlightText", "Highlight")); + } + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation + }); + this.#setDimensions(); + this.reset(); + } + destroy() { + this.reset(); + this.pdfPage?.cleanup(); + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + get _textHighlighter() { + return shadow(this, "_textHighlighter", new TextHighlighter({ + pageIndex: this.id - 1, + eventBus: this.eventBus, + findController: this.#layerProperties.findController + })); + } + #dispatchLayerRendered(name, error) { + this.eventBus.dispatch(name, { + source: this, + pageNumber: this.id, + error + }); + } + async #renderAnnotationLayer() { + let error = null; + try { + await this.annotationLayer.render(this.viewport, { + structTreeLayer: this.structTreeLayer + }, "display"); + } catch (ex) { + console.error("#renderAnnotationLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationlayerrendered", error); + } + } + async #renderAnnotationEditorLayer() { + let error = null; + try { + await this.annotationEditorLayer.render(this.viewport, "display"); + } catch (ex) { + console.error("#renderAnnotationEditorLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationeditorlayerrendered", error); + } + } + async #renderDrawLayer() { + try { + await this.drawLayer.render("display"); + } catch (ex) { + console.error("#renderDrawLayer:", ex); + } + } + async #renderXfaLayer() { + let error = null; + try { + const result = await this.xfaLayer.render(this.viewport, "display"); + if (result?.textDivs && this._textHighlighter) { + this.#buildXfaTextContentItems(result.textDivs); + } + } catch (ex) { + console.error("#renderXfaLayer:", ex); + error = ex; + } finally { + if (this.xfaLayer?.div) { + this.l10n.pause(); + this.#addLayer(this.xfaLayer.div, "xfaLayer"); + this.l10n.resume(); + } + this.#dispatchLayerRendered("xfalayerrendered", error); + } + } + async #renderTextLayer() { + if (!this.textLayer) { + return; + } + let error = null; + try { + await this.textLayer.render(this.viewport); + } catch (ex) { + if (ex instanceof AbortException) { + return; + } + console.error("#renderTextLayer:", ex); + error = ex; + } + this.#dispatchLayerRendered("textlayerrendered", error); + this.#renderStructTreeLayer(); + } + async #renderStructTreeLayer() { + if (!this.textLayer) { + return; + } + const treeDom = await this.structTreeLayer?.render(); + if (treeDom) { + this.l10n.pause(); + this.structTreeLayer?.addElementsToTextLayer(); + if (this.canvas && treeDom.parentNode !== this.canvas) { + this.canvas.append(treeDom); + } + this.l10n.resume(); + } + this.structTreeLayer?.show(); + } + async #buildXfaTextContentItems(textDivs) { + const text = await this.pdfPage.getTextContent(); + const items = []; + for (const item of text.items) { + items.push(item.str); + } + this._textHighlighter.setTextMapping(textDivs, items); + this._textHighlighter.enable(); + } + #resetCanvas() { + const { + canvas + } = this; + if (!canvas) { + return; + } + canvas.remove(); + canvas.width = canvas.height = 0; + this.canvas = null; + this.#originalViewport = null; + } + reset({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + keepCanvasWrapper = false + } = {}) { + this.cancelRendering({ + keepAnnotationLayer, + keepAnnotationEditorLayer, + keepXfaLayer, + keepTextLayer + }); + this.renderingState = RenderingStates.INITIAL; + const div = this.div; + const childNodes = div.childNodes, + annotationLayerNode = keepAnnotationLayer && this.annotationLayer?.div || null, + annotationEditorLayerNode = keepAnnotationEditorLayer && this.annotationEditorLayer?.div || null, + xfaLayerNode = keepXfaLayer && this.xfaLayer?.div || null, + textLayerNode = keepTextLayer && this.textLayer?.div || null, + canvasWrapperNode = keepCanvasWrapper && this.#canvasWrapper || null; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + switch (node) { + case annotationLayerNode: + case annotationEditorLayerNode: + case xfaLayerNode: + case textLayerNode: + case canvasWrapperNode: + continue; + } + node.remove(); + const layerIndex = this.#layers.indexOf(node); + if (layerIndex >= 0) { + this.#layers[layerIndex] = null; + } + } + div.removeAttribute("data-loaded"); + if (annotationLayerNode) { + this.annotationLayer.hide(); + } + if (annotationEditorLayerNode) { + this.annotationEditorLayer.hide(); + } + if (xfaLayerNode) { + this.xfaLayer.hide(); + } + if (textLayerNode) { + this.textLayer.hide(); + } + this.structTreeLayer?.hide(); + if (!keepCanvasWrapper && this.#canvasWrapper) { + this.#canvasWrapper = null; + this.#resetCanvas(); + } + } + toggleEditingMode(isEditing) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#isEditing = isEditing; + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true + }); + } + update({ + scale = 0, + rotation = null, + optionalContentConfigPromise = null, + drawingDelay = -1 + }) { + this.scale = scale || this.scale; + if (typeof rotation === "number") { + this.rotation = rotation; + } + if (optionalContentConfigPromise instanceof Promise) { + this._optionalContentConfigPromise = optionalContentConfigPromise; + optionalContentConfigPromise.then(optionalContentConfig => { + if (optionalContentConfigPromise !== this._optionalContentConfigPromise) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility; + }); + } + this.#useThumbnailCanvas.directDrawing = true; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation + }); + this.#setDimensions(); + if (this._isStandalone) { + this._container?.style.setProperty("--scale-factor", this.viewport.scale); + } + if (this.canvas) { + let onlyCssZoom = false; + if (this.#hasRestrictedScaling) { + if (this.maxCanvasPixels === 0) { + onlyCssZoom = true; + } else if (this.maxCanvasPixels > 0) { + const { + width, + height + } = this.viewport; + const { + sx, + sy + } = this.outputScale; + onlyCssZoom = (Math.floor(width) * sx | 0) * (Math.floor(height) * sy | 0) > this.maxCanvasPixels; + } + } + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + if (postponeDrawing || onlyCssZoom) { + if (postponeDrawing && !onlyCssZoom && this.renderingState !== RenderingStates.FINISHED) { + this.cancelRendering({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + cancelExtraDelay: drawingDelay + }); + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.directDrawing = false; + } + this.cssTransform({ + redrawAnnotationLayer: true, + redrawAnnotationEditorLayer: true, + redrawXfaLayer: true, + redrawTextLayer: !postponeDrawing, + hideTextLayer: postponeDrawing + }); + if (postponeDrawing) { + return; + } + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: true, + timestamp: performance.now(), + error: this.#renderError + }); + return; + } + } + this.cssTransform({}); + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true + }); + } + cancelRendering({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + cancelExtraDelay = 0 + } = {}) { + if (this.renderTask) { + this.renderTask.cancel(cancelExtraDelay); + this.renderTask = null; + } + this.resume = null; + if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) { + this.textLayer.cancel(); + this.textLayer = null; + } + if (this.annotationLayer && (!keepAnnotationLayer || !this.annotationLayer.div)) { + this.annotationLayer.cancel(); + this.annotationLayer = null; + this._annotationCanvasMap = null; + } + if (this.structTreeLayer && !this.textLayer) { + this.structTreeLayer = null; + } + if (this.annotationEditorLayer && (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)) { + if (this.drawLayer) { + this.drawLayer.cancel(); + this.drawLayer = null; + } + this.annotationEditorLayer.cancel(); + this.annotationEditorLayer = null; + } + if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { + this.xfaLayer.cancel(); + this.xfaLayer = null; + this._textHighlighter?.disable(); + } + } + cssTransform({ + redrawAnnotationLayer = false, + redrawAnnotationEditorLayer = false, + redrawXfaLayer = false, + redrawTextLayer = false, + hideTextLayer = false + }) { + const { + canvas + } = this; + if (!canvas) { + return; + } + const originalViewport = this.#originalViewport; + if (this.viewport !== originalViewport) { + const relativeRotation = (360 + this.viewport.rotation - originalViewport.rotation) % 360; + if (relativeRotation === 90 || relativeRotation === 270) { + const { + width, + height + } = this.viewport; + const scaleX = height / width; + const scaleY = width / height; + canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`; + } else { + canvas.style.transform = relativeRotation === 0 ? "" : `rotate(${relativeRotation}deg)`; + } + } + if (redrawAnnotationLayer && this.annotationLayer) { + this.#renderAnnotationLayer(); + } + if (redrawAnnotationEditorLayer && this.annotationEditorLayer) { + if (this.drawLayer) { + this.#renderDrawLayer(); + } + this.#renderAnnotationEditorLayer(); + } + if (redrawXfaLayer && this.xfaLayer) { + this.#renderXfaLayer(); + } + if (this.textLayer) { + if (hideTextLayer) { + this.textLayer.hide(); + this.structTreeLayer?.hide(); + } else if (redrawTextLayer) { + this.#renderTextLayer(); + } + } + } + get width() { + return this.viewport.width; + } + get height() { + return this.viewport.height; + } + getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + } + async #finishRenderTask(renderTask, error = null) { + if (renderTask === this.renderTask) { + this.renderTask = null; + } + if (error instanceof RenderingCancelledException) { + this.#renderError = null; + return; + } + this.#renderError = error; + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots; + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: false, + timestamp: performance.now(), + error: this.#renderError + }); + if (error) { + throw error; + } + } + async draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error("Must be in new state before drawing"); + this.reset(); + } + const { + div, + l10n, + pageColors, + pdfPage, + viewport + } = this; + if (!pdfPage) { + this.renderingState = RenderingStates.FINISHED; + throw new Error("pdfPage is not loaded"); + } + this.renderingState = RenderingStates.RUNNING; + let canvasWrapper = this.#canvasWrapper; + if (!canvasWrapper) { + canvasWrapper = this.#canvasWrapper = document.createElement("div"); + canvasWrapper.classList.add("canvasWrapper"); + this.#addLayer(canvasWrapper, "canvasWrapper"); + } + if (!this.textLayer && this.#textLayerMode !== TextLayerMode.DISABLE && !pdfPage.isPureXfa) { + this._accessibilityManager ||= new TextAccessibilityManager(); + this.textLayer = new TextLayerBuilder({ + pdfPage, + highlighter: this._textHighlighter, + accessibilityManager: this._accessibilityManager, + enablePermissions: this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS, + onAppend: textLayerDiv => { + this.l10n.pause(); + this.#addLayer(textLayerDiv, "textLayer"); + this.l10n.resume(); + } + }); + } + if (!this.annotationLayer && this.#annotationMode !== AnnotationMode.DISABLE) { + const { + annotationStorage, + annotationEditorUIManager, + downloadManager, + enableScripting, + fieldObjectsPromise, + hasJSActionsPromise, + linkService + } = this.#layerProperties; + this._annotationCanvasMap ||= new Map(); + this.annotationLayer = new AnnotationLayerBuilder({ + pdfPage, + annotationStorage, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS, + linkService, + downloadManager, + enableScripting, + hasJSActionsPromise, + fieldObjectsPromise, + annotationCanvasMap: this._annotationCanvasMap, + accessibilityManager: this._accessibilityManager, + annotationEditorUIManager, + onAppend: annotationLayerDiv => { + this.#addLayer(annotationLayerDiv, "annotationLayer"); + } + }); + } + const renderContinueCallback = cont => { + showCanvas?.(false); + if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) { + this.renderingState = RenderingStates.PAUSED; + this.resume = () => { + this.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + const { + width, + height + } = viewport; + const canvas = document.createElement("canvas"); + canvas.setAttribute("role", "presentation"); + const hasHCM = !!(pageColors?.background && pageColors?.foreground); + const prevCanvas = this.canvas; + const updateOnFirstShow = !prevCanvas && !hasHCM; + this.canvas = canvas; + this.#originalViewport = viewport; + let showCanvas = isLastShow => { + if (updateOnFirstShow) { + canvasWrapper.prepend(canvas); + showCanvas = null; + return; + } + if (!isLastShow) { + return; + } + if (prevCanvas) { + prevCanvas.replaceWith(canvas); + prevCanvas.width = prevCanvas.height = 0; + } else { + canvasWrapper.prepend(canvas); + } + showCanvas = null; + }; + const ctx = canvas.getContext("2d", { + alpha: false, + willReadFrequently: !this.#enableHWA + }); + const outputScale = this.outputScale = new OutputScale(); + if (this.maxCanvasPixels === 0) { + const invScale = 1 / this.scale; + outputScale.sx *= invScale; + outputScale.sy *= invScale; + this.#hasRestrictedScaling = true; + } else if (this.maxCanvasPixels > 0) { + const pixelsInViewport = width * height; + const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + this.#hasRestrictedScaling = true; + } else { + this.#hasRestrictedScaling = false; + } + } + const sfx = approximateFraction(outputScale.sx); + const sfy = approximateFraction(outputScale.sy); + const canvasWidth = canvas.width = floorToDivide(calcRound(width * outputScale.sx), sfx[0]); + const canvasHeight = canvas.height = floorToDivide(calcRound(height * outputScale.sy), sfy[0]); + const pageWidth = floorToDivide(calcRound(width), sfx[1]); + const pageHeight = floorToDivide(calcRound(height), sfy[1]); + outputScale.sx = canvasWidth / pageWidth; + outputScale.sy = canvasHeight / pageHeight; + if (this.#scaleRoundX !== sfx[1]) { + div.style.setProperty("--scale-round-x", `${sfx[1]}px`); + this.#scaleRoundX = sfx[1]; + } + if (this.#scaleRoundY !== sfy[1]) { + div.style.setProperty("--scale-round-y", `${sfy[1]}px`); + this.#scaleRoundY = sfy[1]; + } + const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null; + const renderContext = { + canvasContext: ctx, + transform, + viewport, + annotationMode: this.#annotationMode, + optionalContentConfigPromise: this._optionalContentConfigPromise, + annotationCanvasMap: this._annotationCanvasMap, + pageColors, + isEditing: this.#isEditing + }; + const renderTask = this.renderTask = pdfPage.render(renderContext); + renderTask.onContinue = renderContinueCallback; + const resultPromise = renderTask.promise.then(async () => { + showCanvas?.(true); + await this.#finishRenderTask(renderTask); + this.structTreeLayer ||= new StructTreeLayerBuilder(pdfPage, viewport.rawDims); + this.#renderTextLayer(); + if (this.annotationLayer) { + await this.#renderAnnotationLayer(); + } + const { + annotationEditorUIManager + } = this.#layerProperties; + if (!annotationEditorUIManager) { + return; + } + this.drawLayer ||= new DrawLayerBuilder({ + pageIndex: this.id + }); + await this.#renderDrawLayer(); + this.drawLayer.setParent(canvasWrapper); + this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({ + uiManager: annotationEditorUIManager, + pdfPage, + l10n, + structTreeLayer: this.structTreeLayer, + accessibilityManager: this._accessibilityManager, + annotationLayer: this.annotationLayer?.annotationLayer, + textLayer: this.textLayer, + drawLayer: this.drawLayer.getDrawLayer(), + onAppend: annotationEditorLayerDiv => { + this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer"); + } + }); + this.#renderAnnotationEditorLayer(); + }, error => { + if (!(error instanceof RenderingCancelledException)) { + showCanvas?.(true); + } else { + prevCanvas?.remove(); + this.#resetCanvas(); + } + return this.#finishRenderTask(renderTask, error); + }); + if (pdfPage.isPureXfa) { + if (!this.xfaLayer) { + const { + annotationStorage, + linkService + } = this.#layerProperties; + this.xfaLayer = new XfaLayerBuilder({ + pdfPage, + annotationStorage, + linkService + }); + } + this.#renderXfaLayer(); + } + div.setAttribute("data-loaded", true); + this.eventBus.dispatch("pagerender", { + source: this, + pageNumber: this.id + }); + return resultPromise; + } + setPageLabel(label) { + this.pageLabel = typeof label === "string" ? label : null; + this.div.setAttribute("data-l10n-args", JSON.stringify({ + page: this.pageLabel ?? this.id + })); + if (this.pageLabel !== null) { + this.div.setAttribute("data-page-label", this.pageLabel); + } else { + this.div.removeAttribute("data-page-label"); + } + } + get thumbnailCanvas() { + const { + directDrawing, + initialOptionalContent, + regularAnnotations + } = this.#useThumbnailCanvas; + return directDrawing && initialOptionalContent && regularAnnotations ? this.canvas : null; + } +} + +;// ./web/generic_scripting.js + +async function docProperties(pdfDocument) { + const url = "", + baseUrl = url.split("#", 1)[0]; + let { + info, + metadata, + contentDispositionFilename, + contentLength + } = await pdfDocument.getMetadata(); + if (!contentLength) { + const { + length + } = await pdfDocument.getDownloadInfo(); + contentLength = length; + } + return { + ...info, + baseURL: baseUrl, + filesize: contentLength, + filename: contentDispositionFilename || getPdfFilenameFromUrl(url), + metadata: metadata?.getRaw(), + authors: metadata?.get("dc:creator"), + numPages: pdfDocument.numPages, + URL: url + }; +} +class GenericScripting { + constructor(sandboxBundleSrc) { + this._ready = new Promise((resolve, reject) => { + const sandbox = import(/*webpackIgnore: true*/sandboxBundleSrc); + sandbox.then(pdfjsSandbox => { + resolve(pdfjsSandbox.QuickJSSandbox()); + }).catch(reject); + }); + } + async createSandbox(data) { + const sandbox = await this._ready; + sandbox.create(data); + } + async dispatchEventInSandbox(event) { + const sandbox = await this._ready; + setTimeout(() => sandbox.dispatchEvent(event), 0); + } + async destroySandbox() { + const sandbox = await this._ready; + sandbox.nukeSandbox(); + } +} + +;// ./web/pdf_scripting_manager.js + + +class PDFScriptingManager { + #closeCapability = null; + #destroyCapability = null; + #docProperties = null; + #eventAbortController = null; + #eventBus = null; + #externalServices = null; + #pdfDocument = null; + #pdfViewer = null; + #ready = false; + #scripting = null; + #willPrintCapability = null; + constructor({ + eventBus, + externalServices = null, + docProperties = null + }) { + this.#eventBus = eventBus; + this.#externalServices = externalServices; + this.#docProperties = docProperties; + } + setViewer(pdfViewer) { + this.#pdfViewer = pdfViewer; + } + async setDocument(pdfDocument) { + if (this.#pdfDocument) { + await this.#destroyScripting(); + } + this.#pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const [objects, calculationOrder, docActions] = await Promise.all([pdfDocument.getFieldObjects(), pdfDocument.getCalculationOrderIds(), pdfDocument.getJSActions()]); + if (!objects && !docActions) { + await this.#destroyScripting(); + return; + } + if (pdfDocument !== this.#pdfDocument) { + return; + } + try { + this.#scripting = this.#initScripting(); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + const eventBus = this.#eventBus; + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + eventBus._on("updatefromsandbox", event => { + if (event?.source === window) { + this.#updateFromSandbox(event.detail); + } + }, { + signal + }); + eventBus._on("dispatcheventinsandbox", event => { + this.#scripting?.dispatchEventInSandbox(event.detail); + }, { + signal + }); + eventBus._on("pagechanging", ({ + pageNumber, + previous + }) => { + if (pageNumber === previous) { + return; + } + this.#dispatchPageClose(previous); + this.#dispatchPageOpen(pageNumber); + }, { + signal + }); + eventBus._on("pagerendered", ({ + pageNumber + }) => { + if (!this._pageOpenPending.has(pageNumber)) { + return; + } + if (pageNumber !== this.#pdfViewer.currentPageNumber) { + return; + } + this.#dispatchPageOpen(pageNumber); + }, { + signal + }); + eventBus._on("pagesdestroy", async () => { + await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber); + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillClose" + }); + this.#closeCapability?.resolve(); + }, { + signal + }); + try { + const docProperties = await this.#docProperties(pdfDocument); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting.createSandbox({ + objects, + calculationOrder, + appInfo: { + platform: navigator.platform, + language: navigator.language + }, + docInfo: { + ...docProperties, + actions: docActions + } + }); + eventBus.dispatch("sandboxcreated", { + source: this + }); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "Open" + }); + await this.#dispatchPageOpen(this.#pdfViewer.currentPageNumber, true); + Promise.resolve().then(() => { + if (pdfDocument === this.#pdfDocument) { + this.#ready = true; + } + }); + } + async dispatchWillSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillSave" + }); + } + async dispatchDidSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidSave" + }); + } + async dispatchWillPrint() { + if (!this.#scripting) { + return; + } + await this.#willPrintCapability?.promise; + this.#willPrintCapability = Promise.withResolvers(); + try { + await this.#scripting.dispatchEventInSandbox({ + id: "doc", + name: "WillPrint" + }); + } catch (ex) { + this.#willPrintCapability.resolve(); + this.#willPrintCapability = null; + throw ex; + } + await this.#willPrintCapability.promise; + } + async dispatchDidPrint() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidPrint" + }); + } + get destroyPromise() { + return this.#destroyCapability?.promise || null; + } + get ready() { + return this.#ready; + } + get _pageOpenPending() { + return shadow(this, "_pageOpenPending", new Set()); + } + get _visitedPages() { + return shadow(this, "_visitedPages", new Map()); + } + async #updateFromSandbox(detail) { + const pdfViewer = this.#pdfViewer; + const isInPresentationMode = pdfViewer.isInPresentationMode || pdfViewer.isChangingPresentationMode; + const { + id, + siblings, + command, + value + } = detail; + if (!id) { + switch (command) { + case "clear": + console.clear(); + break; + case "error": + console.error(value); + break; + case "layout": + if (!isInPresentationMode) { + const modes = apiPageLayoutToViewerModes(value); + pdfViewer.spreadMode = modes.spreadMode; + } + break; + case "page-num": + pdfViewer.currentPageNumber = value + 1; + break; + case "print": + await pdfViewer.pagesPromise; + this.#eventBus.dispatch("print", { + source: this + }); + break; + case "println": + console.log(value); + break; + case "zoom": + if (!isInPresentationMode) { + pdfViewer.currentScaleValue = value; + } + break; + case "SaveAs": + this.#eventBus.dispatch("download", { + source: this + }); + break; + case "FirstPage": + pdfViewer.currentPageNumber = 1; + break; + case "LastPage": + pdfViewer.currentPageNumber = pdfViewer.pagesCount; + break; + case "NextPage": + pdfViewer.nextPage(); + break; + case "PrevPage": + pdfViewer.previousPage(); + break; + case "ZoomViewIn": + if (!isInPresentationMode) { + pdfViewer.increaseScale(); + } + break; + case "ZoomViewOut": + if (!isInPresentationMode) { + pdfViewer.decreaseScale(); + } + break; + case "WillPrintFinished": + this.#willPrintCapability?.resolve(); + this.#willPrintCapability = null; + break; + } + return; + } + if (isInPresentationMode && detail.focus) { + return; + } + delete detail.id; + delete detail.siblings; + const ids = siblings ? [id, ...siblings] : [id]; + for (const elementId of ids) { + const element = document.querySelector(`[data-element-id="${elementId}"]`); + if (element) { + element.dispatchEvent(new CustomEvent("updatefromsandbox", { + detail + })); + } else { + this.#pdfDocument?.annotationStorage.setValue(elementId, detail); + } + } + } + async #dispatchPageOpen(pageNumber, initialize = false) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (initialize) { + this.#closeCapability = Promise.withResolvers(); + } + if (!this.#closeCapability) { + return; + } + const pageView = this.#pdfViewer.getPageView(pageNumber - 1); + if (pageView?.renderingState !== RenderingStates.FINISHED) { + this._pageOpenPending.add(pageNumber); + return; + } + this._pageOpenPending.delete(pageNumber); + const actionsPromise = (async () => { + const actions = await (!visitedPages.has(pageNumber) ? pageView.pdfPage?.getJSActions() : null); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageOpen", + pageNumber, + actions + }); + })(); + visitedPages.set(pageNumber, actionsPromise); + } + async #dispatchPageClose(pageNumber) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (!this.#closeCapability) { + return; + } + if (this._pageOpenPending.has(pageNumber)) { + return; + } + const actionsPromise = visitedPages.get(pageNumber); + if (!actionsPromise) { + return; + } + visitedPages.set(pageNumber, null); + await actionsPromise; + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageClose", + pageNumber + }); + } + #initScripting() { + this.#destroyCapability = Promise.withResolvers(); + if (this.#scripting) { + throw new Error("#initScripting: Scripting already exists."); + } + return this.#externalServices.createScripting(); + } + async #destroyScripting() { + if (!this.#scripting) { + this.#pdfDocument = null; + this.#destroyCapability?.resolve(); + return; + } + if (this.#closeCapability) { + await Promise.race([this.#closeCapability.promise, new Promise(resolve => { + setTimeout(resolve, 1000); + })]).catch(() => {}); + this.#closeCapability = null; + } + this.#pdfDocument = null; + try { + await this.#scripting.destroySandbox(); + } catch {} + this.#willPrintCapability?.reject(new Error("Scripting destroyed.")); + this.#willPrintCapability = null; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._pageOpenPending.clear(); + this._visitedPages.clear(); + this.#scripting = null; + this.#ready = false; + this.#destroyCapability?.resolve(); + } +} + +;// ./web/pdf_scripting_manager.component.js + + +class PDFScriptingManagerComponents extends PDFScriptingManager { + constructor(options) { + if (!options.externalServices) { + window.addEventListener("updatefromsandbox", event => { + options.eventBus.dispatch("updatefromsandbox", { + source: window, + detail: event.detail + }); + }); + } + options.externalServices ||= { + createScripting: () => new GenericScripting(options.sandboxBundleSrc) + }; + options.docProperties ||= pdfDocument => docProperties(pdfDocument); + super(options); + } +} + +;// ./web/pdf_rendering_queue.js + + +const CLEANUP_TIMEOUT = 30000; +class PDFRenderingQueue { + constructor() { + this.pdfViewer = null; + this.pdfThumbnailViewer = null; + this.onIdle = null; + this.highestPriorityPage = null; + this.idleTimeout = null; + this.printing = false; + this.isThumbnailViewEnabled = false; + Object.defineProperty(this, "hasViewer", { + value: () => !!this.pdfViewer + }); + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setThumbnailViewer(pdfThumbnailViewer) { + this.pdfThumbnailViewer = pdfThumbnailViewer; + } + isHighestPriority(view) { + return this.highestPriorityPage === view.renderingId; + } + renderHighestPriority(currentlyVisiblePages) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; + } + if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { + return; + } + if (this.isThumbnailViewEnabled && this.pdfThumbnailViewer?.forceRendering()) { + return; + } + if (this.printing) { + return; + } + if (this.onIdle) { + this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); + } + } + getHighestPriority(visible, views, scrolledDown, preRenderExtra = false) { + const visibleViews = visible.views, + numVisible = visibleViews.length; + if (numVisible === 0) { + return null; + } + for (let i = 0; i < numVisible; i++) { + const view = visibleViews[i].view; + if (!this.isViewFinished(view)) { + return view; + } + } + const firstId = visible.first.id, + lastId = visible.last.id; + if (lastId - firstId + 1 > numVisible) { + const visibleIds = visible.ids; + for (let i = 1, ii = lastId - firstId; i < ii; i++) { + const holeId = scrolledDown ? firstId + i : lastId - i; + if (visibleIds.has(holeId)) { + continue; + } + const holeView = views[holeId - 1]; + if (!this.isViewFinished(holeView)) { + return holeView; + } + } + } + let preRenderIndex = scrolledDown ? lastId : firstId - 2; + let preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + if (preRenderExtra) { + preRenderIndex += scrolledDown ? 1 : -1; + preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + } + return null; + } + isViewFinished(view) { + return view.renderingState === RenderingStates.FINISHED; + } + renderView(view) { + switch (view.renderingState) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + this.highestPriorityPage = view.renderingId; + view.resume(); + break; + case RenderingStates.RUNNING: + this.highestPriorityPage = view.renderingId; + break; + case RenderingStates.INITIAL: + this.highestPriorityPage = view.renderingId; + view.draw().finally(() => { + this.renderHighestPriority(); + }).catch(reason => { + if (reason instanceof RenderingCancelledException) { + return; + } + console.error("renderView:", reason); + }); + break; + } + return true; + } +} + +;// ./web/pdf_viewer.js + + + + + + +const DEFAULT_CACHE_SIZE = 10; +const PagesCountLimit = { + FORCE_SCROLL_MODE_PAGE: 10000, + FORCE_LAZY_PAGE_INIT: 5000, + PAUSE_EAGER_PAGE_INIT: 250 +}; +function isValidAnnotationEditorMode(mode) { + return Object.values(AnnotationEditorType).includes(mode) && mode !== AnnotationEditorType.DISABLE; +} +class PDFPageViewBuffer { + #buf = new Set(); + #size = 0; + constructor(size) { + this.#size = size; + } + push(view) { + const buf = this.#buf; + if (buf.has(view)) { + buf.delete(view); + } + buf.add(view); + if (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + resize(newSize, idsToKeep = null) { + this.#size = newSize; + const buf = this.#buf; + if (idsToKeep) { + const ii = buf.size; + let i = 1; + for (const view of buf) { + if (idsToKeep.has(view.id)) { + buf.delete(view); + buf.add(view); + } + if (++i > ii) { + break; + } + } + } + while (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + has(view) { + return this.#buf.has(view); + } + [Symbol.iterator]() { + return this.#buf.keys(); + } + #destroyFirstView() { + const firstView = this.#buf.keys().next().value; + firstView?.destroy(); + this.#buf.delete(firstView); + } +} +class PDFViewer { + #buffer = null; + #altTextManager = null; + #annotationEditorHighlightColors = null; + #annotationEditorMode = AnnotationEditorType.NONE; + #annotationEditorUIManager = null; + #annotationMode = AnnotationMode.ENABLE_FORMS; + #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; + #enableHighlightFloatingButton = false; + #enablePermissions = false; + #enableUpdatedAddImage = false; + #enableNewAltTextWhenAddingImage = false; + #eventAbortController = null; + #mlManager = null; + #switchAnnotationEditorModeAC = null; + #switchAnnotationEditorModeTimeoutId = null; + #getAllTextInProgress = false; + #hiddenCopyElement = null; + #interruptCopyCondition = false; + #previousContainerHeight = 0; + #resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this)); + #scrollModePageState = null; + #scaleTimeoutId = null; + #supportsPinchToZoom = true; + #textLayerMode = TextLayerMode.ENABLE; + constructor(options) { + const viewerVersion = "4.10.38"; + if (version !== viewerVersion) { + throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); + } + this.container = options.container; + this.viewer = options.viewer || options.container.firstElementChild; + if (this.container?.tagName !== "DIV" || this.viewer?.tagName !== "DIV") { + throw new Error("Invalid `container` and/or `viewer` option."); + } + if (this.container.offsetParent && getComputedStyle(this.container).position !== "absolute") { + throw new Error("The `container` must be absolutely positioned."); + } + this.#resizeObserver.observe(this.container); + this.eventBus = options.eventBus; + this.linkService = options.linkService || new SimpleLinkService(); + this.downloadManager = options.downloadManager || null; + this.findController = options.findController || null; + this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; + if (this.findController) { + this.findController.onIsPageVisible = pageNumber => this._getVisiblePages().ids.has(pageNumber); + } + this._scriptingManager = options.scriptingManager || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE; + this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null; + this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true; + this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true; + this.#enableNewAltTextWhenAddingImage = options.enableNewAltTextWhenAddingImage === true; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; + this.removePageBorders = options.removePageBorders || false; + this.maxCanvasPixels = options.maxCanvasPixels; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.#enablePermissions = options.enablePermissions || false; + this.pageColors = options.pageColors || null; + this.#mlManager = options.mlManager || null; + this.#enableHWA = options.enableHWA || false; + this.#supportsPinchToZoom = options.supportsPinchToZoom !== false; + this.defaultRenderingQueue = !options.renderingQueue; + if (this.defaultRenderingQueue) { + this.renderingQueue = new PDFRenderingQueue(); + this.renderingQueue.setViewer(this); + } else { + this.renderingQueue = options.renderingQueue; + } + const { + abortSignal + } = options; + abortSignal?.addEventListener("abort", () => { + this.#resizeObserver.disconnect(); + this.#resizeObserver = null; + }, { + once: true + }); + this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this), abortSignal); + this.presentationModeState = PresentationModeState.UNKNOWN; + this._resetView(); + if (this.removePageBorders) { + this.viewer.classList.add("removePageBorders"); + } + this.#updateContainerHeightCss(); + this.eventBus._on("thumbnailrendered", ({ + pageNumber, + pdfPage + }) => { + const pageView = this._pages[pageNumber - 1]; + if (!this.#buffer.has(pageView)) { + pdfPage?.cleanup(); + } + }); + if (!options.l10n) { + this.l10n.translate(this.container); + } + } + get pagesCount() { + return this._pages.length; + } + getPageView(index) { + return this._pages[index]; + } + getCachedPageViews() { + return new Set(this.#buffer); + } + get pageViewsReady() { + return this._pages.every(pageView => pageView?.pdfPage); + } + get renderForms() { + return this.#annotationMode === AnnotationMode.ENABLE_FORMS; + } + get enableScripting() { + return !!this._scriptingManager; + } + get currentPageNumber() { + return this._currentPageNumber; + } + set currentPageNumber(val) { + if (!Number.isInteger(val)) { + throw new Error("Invalid page number."); + } + if (!this.pdfDocument) { + return; + } + if (!this._setCurrentPageNumber(val, true)) { + console.error(`currentPageNumber: "${val}" is not a valid page.`); + } + } + _setCurrentPageNumber(val, resetCurrentPageView = false) { + if (this._currentPageNumber === val) { + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + if (!(0 < val && val <= this.pagesCount)) { + return false; + } + const previous = this._currentPageNumber; + this._currentPageNumber = val; + this.eventBus.dispatch("pagechanging", { + source: this, + pageNumber: val, + pageLabel: this._pageLabels?.[val - 1] ?? null, + previous + }); + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + get currentPageLabel() { + return this._pageLabels?.[this._currentPageNumber - 1] ?? null; + } + set currentPageLabel(val) { + if (!this.pdfDocument) { + return; + } + let page = val | 0; + if (this._pageLabels) { + const i = this._pageLabels.indexOf(val); + if (i >= 0) { + page = i + 1; + } + } + if (!this._setCurrentPageNumber(page, true)) { + console.error(`currentPageLabel: "${val}" is not a valid page.`); + } + } + get currentScale() { + return this._currentScale !== UNKNOWN_SCALE ? this._currentScale : DEFAULT_SCALE; + } + set currentScale(val) { + if (isNaN(val)) { + throw new Error("Invalid numeric scale."); + } + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false + }); + } + get currentScaleValue() { + return this._currentScaleValue; + } + set currentScaleValue(val) { + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false + }); + } + get pagesRotation() { + return this._pagesRotation; + } + set pagesRotation(rotation) { + if (!isValidRotation(rotation)) { + throw new Error("Invalid pages rotation angle."); + } + if (!this.pdfDocument) { + return; + } + rotation %= 360; + if (rotation < 0) { + rotation += 360; + } + if (this._pagesRotation === rotation) { + return; + } + this._pagesRotation = rotation; + const pageNumber = this._currentPageNumber; + this.refresh(true, { + rotation + }); + if (this._currentScaleValue) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this.eventBus.dispatch("rotationchanging", { + source: this, + pagesRotation: rotation, + pageNumber + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get firstPagePromise() { + return this.pdfDocument ? this._firstPageCapability.promise : null; + } + get onePageRendered() { + return this.pdfDocument ? this._onePageRenderedCapability.promise : null; + } + get pagesPromise() { + return this.pdfDocument ? this._pagesCapability.promise : null; + } + get _layerProperties() { + const self = this; + return shadow(this, "_layerProperties", { + get annotationEditorUIManager() { + return self.#annotationEditorUIManager; + }, + get annotationStorage() { + return self.pdfDocument?.annotationStorage; + }, + get downloadManager() { + return self.downloadManager; + }, + get enableScripting() { + return !!self._scriptingManager; + }, + get fieldObjectsPromise() { + return self.pdfDocument?.getFieldObjects(); + }, + get findController() { + return self.findController; + }, + get hasJSActionsPromise() { + return self.pdfDocument?.hasJSActions(); + }, + get linkService() { + return self.linkService; + } + }); + } + #initializePermissions(permissions) { + const params = { + annotationEditorMode: this.#annotationEditorMode, + annotationMode: this.#annotationMode, + textLayerMode: this.#textLayerMode + }; + if (!permissions) { + return params; + } + if (!permissions.includes(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { + params.textLayerMode = TextLayerMode.ENABLE_PERMISSIONS; + } + if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { + params.annotationEditorMode = AnnotationEditorType.DISABLE; + } + if (!permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { + params.annotationMode = AnnotationMode.ENABLE; + } + return params; + } + async #onePageRenderedOrForceFetch(signal) { + if (document.visibilityState === "hidden" || !this.container.offsetParent || this._getVisiblePages().views.length === 0) { + return; + } + const hiddenCapability = Promise.withResolvers(), + ac = new AbortController(); + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + hiddenCapability.resolve(); + } + }, { + signal: typeof AbortSignal.any === "function" ? AbortSignal.any([signal, ac.signal]) : signal + }); + await Promise.race([this._onePageRenderedCapability.promise, hiddenCapability.promise]); + ac.abort(); + } + async getAllText() { + const texts = []; + const buffer = []; + for (let pageNum = 1, pagesCount = this.pdfDocument.numPages; pageNum <= pagesCount; ++pageNum) { + if (this.#interruptCopyCondition) { + return null; + } + buffer.length = 0; + const page = await this.pdfDocument.getPage(pageNum); + const { + items + } = await page.getTextContent(); + for (const item of items) { + if (item.str) { + buffer.push(item.str); + } + if (item.hasEOL) { + buffer.push("\n"); + } + } + texts.push(removeNullCharacters(buffer.join(""))); + } + return texts.join("\n"); + } + #copyCallback(textLayerMode, event) { + const selection = document.getSelection(); + const { + focusNode, + anchorNode + } = selection; + if (anchorNode && focusNode && selection.containsNode(this.#hiddenCopyElement)) { + if (this.#getAllTextInProgress || textLayerMode === TextLayerMode.ENABLE_PERMISSIONS) { + stopEvent(event); + return; + } + this.#getAllTextInProgress = true; + const { + classList + } = this.viewer; + classList.add("copyAll"); + const ac = new AbortController(); + window.addEventListener("keydown", ev => this.#interruptCopyCondition = ev.key === "Escape", { + signal: ac.signal + }); + this.getAllText().then(async text => { + if (text !== null) { + await navigator.clipboard.writeText(text); + } + }).catch(reason => { + console.warn(`Something goes wrong when extracting the text: ${reason.message}`); + }).finally(() => { + this.#getAllTextInProgress = false; + this.#interruptCopyCondition = false; + ac.abort(); + classList.remove("copyAll"); + }); + stopEvent(event); + } + } + setDocument(pdfDocument) { + if (this.pdfDocument) { + this.eventBus.dispatch("pagesdestroy", { + source: this + }); + this._cancelRendering(); + this._resetView(); + this.findController?.setDocument(null); + this._scriptingManager?.setDocument(null); + this.#annotationEditorUIManager?.destroy(); + this.#annotationEditorUIManager = null; + } + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const pagesCount = pdfDocument.numPages; + const firstPagePromise = pdfDocument.getPage(1); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display" + }); + const permissionsPromise = this.#enablePermissions ? pdfDocument.getPermissions() : Promise.resolve(); + const { + eventBus, + pageColors, + viewer + } = this; + this.#eventAbortController = new AbortController(); + const { + signal + } = this.#eventAbortController; + if (pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + console.warn("Forcing PAGE-scrolling for performance reasons, given the length of the document."); + const mode = this._scrollMode = ScrollMode.PAGE; + eventBus.dispatch("scrollmodechanged", { + source: this, + mode + }); + } + this._pagesCapability.promise.then(() => { + eventBus.dispatch("pagesloaded", { + source: this, + pagesCount + }); + }, () => {}); + const onBeforeDraw = evt => { + const pageView = this._pages[evt.pageNumber - 1]; + if (!pageView) { + return; + } + this.#buffer.push(pageView); + }; + eventBus._on("pagerender", onBeforeDraw, { + signal + }); + const onAfterDraw = evt => { + if (evt.cssTransform) { + return; + } + this._onePageRenderedCapability.resolve({ + timestamp: evt.timestamp + }); + eventBus._off("pagerendered", onAfterDraw); + }; + eventBus._on("pagerendered", onAfterDraw, { + signal + }); + Promise.all([firstPagePromise, permissionsPromise]).then(([firstPdfPage, permissions]) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this._firstPageCapability.resolve(firstPdfPage); + this._optionalContentConfigPromise = optionalContentConfigPromise; + const { + annotationEditorMode, + annotationMode, + textLayerMode + } = this.#initializePermissions(permissions); + if (textLayerMode !== TextLayerMode.DISABLE) { + const element = this.#hiddenCopyElement = document.createElement("div"); + element.id = "hiddenCopyElement"; + viewer.before(element); + } + if (typeof AbortSignal.any === "function" && annotationEditorMode !== AnnotationEditorType.DISABLE) { + const mode = annotationEditorMode; + if (pdfDocument.isPureXfa) { + console.warn("Warning: XFA-editing is not implemented."); + } else if (isValidAnnotationEditorMode(mode)) { + this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, this.#mlManager, this.#editorUndoBar, this.#supportsPinchToZoom); + eventBus.dispatch("annotationeditoruimanager", { + source: this, + uiManager: this.#annotationEditorUIManager + }); + if (mode !== AnnotationEditorType.NONE) { + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + this.#annotationEditorUIManager.updateMode(mode); + } + } else { + console.error(`Invalid AnnotationEditor mode: ${mode}`); + } + } + const viewerElement = this._scrollMode === ScrollMode.PAGE ? null : viewer; + const scale = this.currentScale; + const viewport = firstPdfPage.getViewport({ + scale: scale * PixelsPerInch.PDF_TO_CSS_UNITS + }); + viewer.style.setProperty("--scale-factor", viewport.scale); + if (pageColors?.background) { + viewer.style.setProperty("--page-bg-color", pageColors.background); + } + if (pageColors?.foreground === "CanvasText" || pageColors?.background === "Canvas") { + viewer.style.setProperty("--hcm-highlight-filter", pdfDocument.filterFactory.addHighlightHCMFilter("highlight", "CanvasText", "Canvas", "HighlightText", "Highlight")); + viewer.style.setProperty("--hcm-highlight-selected-filter", pdfDocument.filterFactory.addHighlightHCMFilter("highlight_selected", "CanvasText", "Canvas", "HighlightText", "ButtonText")); + } + for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { + const pageView = new PDFPageView({ + container: viewerElement, + eventBus, + id: pageNum, + scale, + defaultViewport: viewport.clone(), + optionalContentConfigPromise, + renderingQueue: this.renderingQueue, + textLayerMode, + annotationMode, + imageResourcesPath: this.imageResourcesPath, + maxCanvasPixels: this.maxCanvasPixels, + pageColors, + l10n: this.l10n, + layerProperties: this._layerProperties, + enableHWA: this.#enableHWA + }); + this._pages.push(pageView); + } + this._pages[0]?.setPdfPage(firstPdfPage); + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._spreadMode !== SpreadMode.NONE) { + this._updateSpreadMode(); + } + this.#onePageRenderedOrForceFetch(signal).then(async () => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.findController?.setDocument(pdfDocument); + this._scriptingManager?.setDocument(pdfDocument); + if (this.#hiddenCopyElement) { + document.addEventListener("copy", this.#copyCallback.bind(this, textLayerMode), { + signal + }); + } + if (this.#annotationEditorUIManager) { + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode: this.#annotationEditorMode + }); + } + if (pdfDocument.loadingParams.disableAutoFetch || pagesCount > PagesCountLimit.FORCE_LAZY_PAGE_INIT) { + this._pagesCapability.resolve(); + return; + } + let getPagesLeft = pagesCount - 1; + if (getPagesLeft <= 0) { + this._pagesCapability.resolve(); + return; + } + for (let pageNum = 2; pageNum <= pagesCount; ++pageNum) { + const promise = pdfDocument.getPage(pageNum).then(pdfPage => { + const pageView = this._pages[pageNum - 1]; + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }, reason => { + console.error(`Unable to get page ${pageNum} to initialize viewer`, reason); + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }); + if (pageNum % PagesCountLimit.PAUSE_EAGER_PAGE_INIT === 0) { + await promise; + } + } + }); + eventBus.dispatch("pagesinit", { + source: this + }); + pdfDocument.getMetadata().then(({ + info + }) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + if (info.Language) { + viewer.lang = info.Language; + } + }); + if (this.defaultRenderingQueue) { + this.update(); + } + }).catch(reason => { + console.error("Unable to initialize viewer", reason); + this._pagesCapability.reject(reason); + }); + } + setPageLabels(labels) { + if (!this.pdfDocument) { + return; + } + if (!labels) { + this._pageLabels = null; + } else if (!(Array.isArray(labels) && this.pdfDocument.numPages === labels.length)) { + this._pageLabels = null; + console.error(`setPageLabels: Invalid page labels.`); + } else { + this._pageLabels = labels; + } + for (let i = 0, ii = this._pages.length; i < ii; i++) { + this._pages[i].setPageLabel(this._pageLabels?.[i] ?? null); + } + } + _resetView() { + this._pages = []; + this._currentPageNumber = 1; + this._currentScale = UNKNOWN_SCALE; + this._currentScaleValue = null; + this._pageLabels = null; + this.#buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); + this._location = null; + this._pagesRotation = 0; + this._optionalContentConfigPromise = null; + this._firstPageCapability = Promise.withResolvers(); + this._onePageRenderedCapability = Promise.withResolvers(); + this._pagesCapability = Promise.withResolvers(); + this._scrollMode = ScrollMode.VERTICAL; + this._previousScrollMode = ScrollMode.UNKNOWN; + this._spreadMode = SpreadMode.NONE; + this.#scrollModePageState = { + previousPageNumber: 1, + scrollDown: true, + pages: [] + }; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this.viewer.textContent = ""; + this._updateScrollMode(); + this.viewer.removeAttribute("lang"); + this.#hiddenCopyElement?.remove(); + this.#hiddenCopyElement = null; + this.#cleanupSwitchAnnotationEditorMode(); + } + #ensurePageViewVisible() { + if (this._scrollMode !== ScrollMode.PAGE) { + throw new Error("#ensurePageViewVisible: Invalid scrollMode value."); + } + const pageNumber = this._currentPageNumber, + state = this.#scrollModePageState, + viewer = this.viewer; + viewer.textContent = ""; + state.pages.length = 0; + if (this._spreadMode === SpreadMode.NONE && !this.isInPresentationMode) { + const pageView = this._pages[pageNumber - 1]; + viewer.append(pageView.div); + state.pages.push(pageView); + } else { + const pageIndexSet = new Set(), + parity = this._spreadMode - 1; + if (parity === -1) { + pageIndexSet.add(pageNumber - 1); + } else if (pageNumber % 2 !== parity) { + pageIndexSet.add(pageNumber - 1); + pageIndexSet.add(pageNumber); + } else { + pageIndexSet.add(pageNumber - 2); + pageIndexSet.add(pageNumber - 1); + } + const spread = document.createElement("div"); + spread.className = "spread"; + if (this.isInPresentationMode) { + const dummyPage = document.createElement("div"); + dummyPage.className = "dummyPage"; + spread.append(dummyPage); + } + for (const i of pageIndexSet) { + const pageView = this._pages[i]; + if (!pageView) { + continue; + } + spread.append(pageView.div); + state.pages.push(pageView); + } + viewer.append(spread); + } + state.scrollDown = pageNumber >= state.previousPageNumber; + state.previousPageNumber = pageNumber; + } + _scrollUpdate() { + if (this.pagesCount === 0) { + return; + } + this.update(); + } + #scrollIntoView(pageView, pageSpot = null) { + const { + div, + id + } = pageView; + if (this._currentPageNumber !== id) { + this._setCurrentPageNumber(id); + } + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + this.update(); + } + if (!pageSpot && !this.isInPresentationMode) { + const left = div.offsetLeft + div.clientLeft, + right = left + div.clientWidth; + const { + scrollLeft, + clientWidth + } = this.container; + if (this._scrollMode === ScrollMode.HORIZONTAL || left < scrollLeft || right > scrollLeft + clientWidth) { + pageSpot = { + left: 0, + top: 0 + }; + } + } + scrollIntoView(div, pageSpot); + if (!this._currentScaleValue && this._location) { + this._location = null; + } + } + #isSameScale(newScale) { + return newScale === this._currentScale || Math.abs(newScale - this._currentScale) < 1e-15; + } + #setScaleUpdatePages(newScale, newValue, { + noScroll = false, + preset = false, + drawingDelay = -1, + origin = null + }) { + this._currentScaleValue = newValue.toString(); + if (this.#isSameScale(newScale)) { + if (preset) { + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: newValue + }); + } + return; + } + this.viewer.style.setProperty("--scale-factor", newScale * PixelsPerInch.PDF_TO_CSS_UNITS); + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + this.refresh(true, { + scale: newScale, + drawingDelay: postponeDrawing ? drawingDelay : -1 + }); + if (postponeDrawing) { + this.#scaleTimeoutId = setTimeout(() => { + this.#scaleTimeoutId = null; + this.refresh(); + }, drawingDelay); + } + const previousScale = this._currentScale; + this._currentScale = newScale; + if (!noScroll) { + let page = this._currentPageNumber, + dest; + if (this._location && !(this.isInPresentationMode || this.isChangingPresentationMode)) { + page = this._location.pageNumber; + dest = [null, { + name: "XYZ" + }, this._location.left, this._location.top, null]; + } + this.scrollPageIntoView({ + pageNumber: page, + destArray: dest, + allowNegativeOffset: true + }); + if (Array.isArray(origin)) { + const scaleDiff = newScale / previousScale - 1; + const [top, left] = this.containerTopLeft; + this.container.scrollLeft += (origin[0] - left) * scaleDiff; + this.container.scrollTop += (origin[1] - top) * scaleDiff; + } + } + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: preset ? newValue : undefined + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get #pageWidthScaleFactor() { + if (this._spreadMode !== SpreadMode.NONE && this._scrollMode !== ScrollMode.HORIZONTAL) { + return 2; + } + return 1; + } + #setScale(value, options) { + let scale = parseFloat(value); + if (scale > 0) { + options.preset = false; + this.#setScaleUpdatePages(scale, value, options); + } else { + const currentPage = this._pages[this._currentPageNumber - 1]; + if (!currentPage) { + return; + } + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.isInPresentationMode) { + hPadding = vPadding = 4; + if (this._spreadMode !== SpreadMode.NONE) { + hPadding *= 2; + } + } else if (this.removePageBorders) { + hPadding = vPadding = 0; + } else if (this._scrollMode === ScrollMode.HORIZONTAL) { + [hPadding, vPadding] = [vPadding, hPadding]; + } + const pageWidthScale = (this.container.clientWidth - hPadding) / currentPage.width * currentPage.scale / this.#pageWidthScaleFactor; + const pageHeightScale = (this.container.clientHeight - vPadding) / currentPage.height * currentPage.scale; + switch (value) { + case "page-actual": + scale = 1; + break; + case "page-width": + scale = pageWidthScale; + break; + case "page-height": + scale = pageHeightScale; + break; + case "page-fit": + scale = Math.min(pageWidthScale, pageHeightScale); + break; + case "auto": + const horizontalScale = isPortraitOrientation(currentPage) ? pageWidthScale : Math.min(pageHeightScale, pageWidthScale); + scale = Math.min(MAX_AUTO_SCALE, horizontalScale); + break; + default: + console.error(`#setScale: "${value}" is an unknown zoom value.`); + return; + } + options.preset = true; + this.#setScaleUpdatePages(scale, value, options); + } + } + #resetCurrentPageView() { + const pageView = this._pages[this._currentPageNumber - 1]; + if (this.isInPresentationMode) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this.#scrollIntoView(pageView); + } + pageLabelToPageNumber(label) { + if (!this._pageLabels) { + return null; + } + const i = this._pageLabels.indexOf(label); + if (i < 0) { + return null; + } + return i + 1; + } + scrollPageIntoView({ + pageNumber, + destArray = null, + allowNegativeOffset = false, + ignoreDestinationZoom = false + }) { + if (!this.pdfDocument) { + return; + } + const pageView = Number.isInteger(pageNumber) && this._pages[pageNumber - 1]; + if (!pageView) { + console.error(`scrollPageIntoView: "${pageNumber}" is not a valid pageNumber parameter.`); + return; + } + if (this.isInPresentationMode || !destArray) { + this._setCurrentPageNumber(pageNumber, true); + return; + } + let x = 0, + y = 0; + let width = 0, + height = 0, + widthScale, + heightScale; + const changeOrientation = pageView.rotation % 180 !== 0; + const pageWidth = (changeOrientation ? pageView.height : pageView.width) / pageView.scale / PixelsPerInch.PDF_TO_CSS_UNITS; + const pageHeight = (changeOrientation ? pageView.width : pageView.height) / pageView.scale / PixelsPerInch.PDF_TO_CSS_UNITS; + let scale = 0; + switch (destArray[1].name) { + case "XYZ": + x = destArray[2]; + y = destArray[3]; + scale = destArray[4]; + x = x !== null ? x : 0; + y = y !== null ? y : pageHeight; + break; + case "Fit": + case "FitB": + scale = "page-fit"; + break; + case "FitH": + case "FitBH": + y = destArray[2]; + scale = "page-width"; + if (y === null && this._location) { + x = this._location.left; + y = this._location.top; + } else if (typeof y !== "number" || y < 0) { + y = pageHeight; + } + break; + case "FitV": + case "FitBV": + x = destArray[2]; + width = pageWidth; + height = pageHeight; + scale = "page-height"; + break; + case "FitR": + x = destArray[2]; + y = destArray[3]; + width = destArray[4] - x; + height = destArray[5] - y; + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.removePageBorders) { + hPadding = vPadding = 0; + } + widthScale = (this.container.clientWidth - hPadding) / width / PixelsPerInch.PDF_TO_CSS_UNITS; + heightScale = (this.container.clientHeight - vPadding) / height / PixelsPerInch.PDF_TO_CSS_UNITS; + scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); + break; + default: + console.error(`scrollPageIntoView: "${destArray[1].name}" is not a valid destination type.`); + return; + } + if (!ignoreDestinationZoom) { + if (scale && scale !== this._currentScale) { + this.currentScaleValue = scale; + } else if (this._currentScale === UNKNOWN_SCALE) { + this.currentScaleValue = DEFAULT_SCALE_VALUE; + } + } + if (scale === "page-fit" && !destArray[4]) { + this.#scrollIntoView(pageView); + return; + } + const boundingRect = [pageView.viewport.convertToViewportPoint(x, y), pageView.viewport.convertToViewportPoint(x + width, y + height)]; + let left = Math.min(boundingRect[0][0], boundingRect[1][0]); + let top = Math.min(boundingRect[0][1], boundingRect[1][1]); + if (!allowNegativeOffset) { + left = Math.max(left, 0); + top = Math.max(top, 0); + } + this.#scrollIntoView(pageView, { + left, + top + }); + } + _updateLocation(firstPage) { + const currentScale = this._currentScale; + const currentScaleValue = this._currentScaleValue; + const normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue; + const pageNumber = firstPage.id; + const currentPageView = this._pages[pageNumber - 1]; + const container = this.container; + const topLeft = currentPageView.getPagePoint(container.scrollLeft - firstPage.x, container.scrollTop - firstPage.y); + const intLeft = Math.round(topLeft[0]); + const intTop = Math.round(topLeft[1]); + let pdfOpenParams = `#page=${pageNumber}`; + if (!this.isInPresentationMode) { + pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`; + } + this._location = { + pageNumber, + scale: normalizedScaleValue, + top: intTop, + left: intLeft, + rotation: this._pagesRotation, + pdfOpenParams + }; + } + update() { + const visible = this._getVisiblePages(); + const visiblePages = visible.views, + numVisiblePages = visiblePages.length; + if (numVisiblePages === 0) { + return; + } + const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1); + this.#buffer.resize(newCacheSize, visible.ids); + this.renderingQueue.renderHighestPriority(visible); + const isSimpleLayout = this._spreadMode === SpreadMode.NONE && (this._scrollMode === ScrollMode.PAGE || this._scrollMode === ScrollMode.VERTICAL); + const currentId = this._currentPageNumber; + let stillFullyVisible = false; + for (const page of visiblePages) { + if (page.percent < 100) { + break; + } + if (page.id === currentId && isSimpleLayout) { + stillFullyVisible = true; + break; + } + } + this._setCurrentPageNumber(stillFullyVisible ? currentId : visiblePages[0].id); + this._updateLocation(visible.first); + this.eventBus.dispatch("updateviewarea", { + source: this, + location: this._location + }); + } + #switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { + ids, + views + } = visible; + for (const page of views) { + const { + view + } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + } + if (pagesToRefresh.length === 0) { + return null; + } + this.renderingQueue.renderHighestPriority({ + first: pagesToRefresh[0], + last: pagesToRefresh.at(-1), + views: pagesToRefresh, + ids + }); + return ids; + } + containsElement(element) { + return this.container.contains(element); + } + focus() { + this.container.focus(); + } + get _isContainerRtl() { + return getComputedStyle(this.container).direction === "rtl"; + } + get isInPresentationMode() { + return this.presentationModeState === PresentationModeState.FULLSCREEN; + } + get isChangingPresentationMode() { + return this.presentationModeState === PresentationModeState.CHANGING; + } + get isHorizontalScrollbarEnabled() { + return this.isInPresentationMode ? false : this.container.scrollWidth > this.container.clientWidth; + } + get isVerticalScrollbarEnabled() { + return this.isInPresentationMode ? false : this.container.scrollHeight > this.container.clientHeight; + } + _getVisiblePages() { + const views = this._scrollMode === ScrollMode.PAGE ? this.#scrollModePageState.pages : this._pages, + horizontal = this._scrollMode === ScrollMode.HORIZONTAL, + rtl = horizontal && this._isContainerRtl; + return getVisibleElements({ + scrollEl: this.container, + views, + sortByVisibility: true, + horizontal, + rtl + }); + } + cleanup() { + for (const pageView of this._pages) { + if (pageView.renderingState !== RenderingStates.FINISHED) { + pageView.reset(); + } + } + } + _cancelRendering() { + for (const pageView of this._pages) { + pageView.cancelRendering(); + } + } + async #ensurePdfPageLoaded(pageView) { + if (pageView.pdfPage) { + return pageView.pdfPage; + } + try { + const pdfPage = await this.pdfDocument.getPage(pageView.id); + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + return pdfPage; + } catch (reason) { + console.error("Unable to get page for page view", reason); + return null; + } + } + #getScrollAhead(visible) { + if (visible.first?.id === 1) { + return true; + } else if (visible.last?.id === this.pagesCount) { + return false; + } + switch (this._scrollMode) { + case ScrollMode.PAGE: + return this.#scrollModePageState.scrollDown; + case ScrollMode.HORIZONTAL: + return this.scroll.right; + } + return this.scroll.down; + } + forceRendering(currentlyVisiblePages) { + const visiblePages = currentlyVisiblePages || this._getVisiblePages(); + const scrollAhead = this.#getScrollAhead(visiblePages); + const preRenderExtra = this._spreadMode !== SpreadMode.NONE && this._scrollMode !== ScrollMode.HORIZONTAL; + const pageView = this.renderingQueue.getHighestPriority(visiblePages, this._pages, scrollAhead, preRenderExtra); + if (pageView) { + this.#ensurePdfPageLoaded(pageView).then(() => { + this.renderingQueue.renderView(pageView); + }); + return true; + } + return false; + } + get hasEqualPageSizes() { + const firstPageView = this._pages[0]; + for (let i = 1, ii = this._pages.length; i < ii; ++i) { + const pageView = this._pages[i]; + if (pageView.width !== firstPageView.width || pageView.height !== firstPageView.height) { + return false; + } + } + return true; + } + getPagesOverview() { + let initialOrientation; + return this._pages.map(pageView => { + const viewport = pageView.pdfPage.getViewport({ + scale: 1 + }); + const orientation = isPortraitOrientation(viewport); + if (initialOrientation === undefined) { + initialOrientation = orientation; + } else if (this.enablePrintAutoRotate && orientation !== initialOrientation) { + return { + width: viewport.height, + height: viewport.width, + rotation: (viewport.rotation - 90) % 360 + }; + } + return { + width: viewport.width, + height: viewport.height, + rotation: viewport.rotation + }; + }); + } + get optionalContentConfigPromise() { + if (!this.pdfDocument) { + return Promise.resolve(null); + } + if (!this._optionalContentConfigPromise) { + console.error("optionalContentConfigPromise: Not initialized yet."); + return this.pdfDocument.getOptionalContentConfig({ + intent: "display" + }); + } + return this._optionalContentConfigPromise; + } + set optionalContentConfigPromise(promise) { + if (!(promise instanceof Promise)) { + throw new Error(`Invalid optionalContentConfigPromise: ${promise}`); + } + if (!this.pdfDocument) { + return; + } + if (!this._optionalContentConfigPromise) { + return; + } + this._optionalContentConfigPromise = promise; + this.refresh(false, { + optionalContentConfigPromise: promise + }); + this.eventBus.dispatch("optionalcontentconfigchanged", { + source: this, + promise + }); + } + get scrollMode() { + return this._scrollMode; + } + set scrollMode(mode) { + if (this._scrollMode === mode) { + return; + } + if (!isValidScrollMode(mode)) { + throw new Error(`Invalid scroll mode: ${mode}`); + } + if (this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + return; + } + this._previousScrollMode = this._scrollMode; + this._scrollMode = mode; + this.eventBus.dispatch("scrollmodechanged", { + source: this, + mode + }); + this._updateScrollMode(this._currentPageNumber); + } + _updateScrollMode(pageNumber = null) { + const scrollMode = this._scrollMode, + viewer = this.viewer; + viewer.classList.toggle("scrollHorizontal", scrollMode === ScrollMode.HORIZONTAL); + viewer.classList.toggle("scrollWrapped", scrollMode === ScrollMode.WRAPPED); + if (!this.pdfDocument || !pageNumber) { + return; + } + if (scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._previousScrollMode === ScrollMode.PAGE) { + this._updateSpreadMode(); + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + get spreadMode() { + return this._spreadMode; + } + set spreadMode(mode) { + if (this._spreadMode === mode) { + return; + } + if (!isValidSpreadMode(mode)) { + throw new Error(`Invalid spread mode: ${mode}`); + } + this._spreadMode = mode; + this.eventBus.dispatch("spreadmodechanged", { + source: this, + mode + }); + this._updateSpreadMode(this._currentPageNumber); + } + _updateSpreadMode(pageNumber = null) { + if (!this.pdfDocument) { + return; + } + const viewer = this.viewer, + pages = this._pages; + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else { + viewer.textContent = ""; + if (this._spreadMode === SpreadMode.NONE) { + for (const pageView of this._pages) { + viewer.append(pageView.div); + } + } else { + const parity = this._spreadMode - 1; + let spread = null; + for (let i = 0, ii = pages.length; i < ii; ++i) { + if (spread === null) { + spread = document.createElement("div"); + spread.className = "spread"; + viewer.append(spread); + } else if (i % 2 === parity) { + spread = spread.cloneNode(false); + viewer.append(spread); + } + spread.append(pages[i].div); + } + } + } + if (!pageNumber) { + return; + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + _getPageAdvance(currentPageNumber, previous = false) { + switch (this._scrollMode) { + case ScrollMode.WRAPPED: + { + const { + views + } = this._getVisiblePages(), + pageLayout = new Map(); + for (const { + id, + y, + percent, + widthPercent + } of views) { + if (percent === 0 || widthPercent < 100) { + continue; + } + let yArray = pageLayout.get(y); + if (!yArray) { + pageLayout.set(y, yArray ||= []); + } + yArray.push(id); + } + for (const yArray of pageLayout.values()) { + const currentIndex = yArray.indexOf(currentPageNumber); + if (currentIndex === -1) { + continue; + } + const numPages = yArray.length; + if (numPages === 1) { + break; + } + if (previous) { + for (let i = currentIndex - 1, ii = 0; i >= ii; i--) { + const currentId = yArray[i], + expectedId = yArray[i + 1] - 1; + if (currentId < expectedId) { + return currentPageNumber - expectedId; + } + } + } else { + for (let i = currentIndex + 1, ii = numPages; i < ii; i++) { + const currentId = yArray[i], + expectedId = yArray[i - 1] + 1; + if (currentId > expectedId) { + return expectedId - currentPageNumber; + } + } + } + if (previous) { + const firstId = yArray[0]; + if (firstId < currentPageNumber) { + return currentPageNumber - firstId + 1; + } + } else { + const lastId = yArray[numPages - 1]; + if (lastId > currentPageNumber) { + return lastId - currentPageNumber + 1; + } + } + break; + } + break; + } + case ScrollMode.HORIZONTAL: + { + break; + } + case ScrollMode.PAGE: + case ScrollMode.VERTICAL: + { + if (this._spreadMode === SpreadMode.NONE) { + break; + } + const parity = this._spreadMode - 1; + if (previous && currentPageNumber % 2 !== parity) { + break; + } else if (!previous && currentPageNumber % 2 === parity) { + break; + } + const { + views + } = this._getVisiblePages(), + expectedId = previous ? currentPageNumber - 1 : currentPageNumber + 1; + for (const { + id, + percent, + widthPercent + } of views) { + if (id !== expectedId) { + continue; + } + if (percent > 0 && widthPercent === 100) { + return 2; + } + break; + } + break; + } + } + return 1; + } + nextPage() { + const currentPageNumber = this._currentPageNumber, + pagesCount = this.pagesCount; + if (currentPageNumber >= pagesCount) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, false) || 1; + this.currentPageNumber = Math.min(currentPageNumber + advance, pagesCount); + return true; + } + previousPage() { + const currentPageNumber = this._currentPageNumber; + if (currentPageNumber <= 1) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, true) || 1; + this.currentPageNumber = Math.max(currentPageNumber - advance, 1); + return true; + } + updateScale({ + drawingDelay, + scaleFactor = null, + steps = null, + origin + }) { + if (steps === null && scaleFactor === null) { + throw new Error("Invalid updateScale options: either `steps` or `scaleFactor` must be provided."); + } + if (!this.pdfDocument) { + return; + } + let newScale = this._currentScale; + if (scaleFactor > 0 && scaleFactor !== 1) { + newScale = Math.round(newScale * scaleFactor * 100) / 100; + } else if (steps) { + const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA; + const round = steps > 0 ? Math.ceil : Math.floor; + steps = Math.abs(steps); + do { + newScale = round((newScale * delta).toFixed(2) * 10) / 10; + } while (--steps > 0); + } + newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale)); + this.#setScale(newScale, { + noScroll: false, + drawingDelay, + origin + }); + } + increaseScale(options = {}) { + this.updateScale({ + ...options, + steps: options.steps ?? 1 + }); + } + decreaseScale(options = {}) { + this.updateScale({ + ...options, + steps: -(options.steps ?? 1) + }); + } + #updateContainerHeightCss(height = this.container.clientHeight) { + if (height !== this.#previousContainerHeight) { + this.#previousContainerHeight = height; + docStyle.setProperty("--viewer-container-height", `${height}px`); + } + } + #resizeObserverCallback(entries) { + for (const entry of entries) { + if (entry.target === this.container) { + this.#updateContainerHeightCss(Math.floor(entry.borderBoxSize[0].blockSize)); + this.#containerTopLeft = null; + break; + } + } + } + get containerTopLeft() { + return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft]; + } + #cleanupSwitchAnnotationEditorMode() { + this.#switchAnnotationEditorModeAC?.abort(); + this.#switchAnnotationEditorModeAC = null; + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + } + get annotationEditorMode() { + return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE; + } + set annotationEditorMode({ + mode, + editId = null, + isFromKeyboard = false + }) { + if (!this.#annotationEditorUIManager) { + throw new Error(`The AnnotationEditor is not enabled.`); + } + if (this.#annotationEditorMode === mode) { + return; + } + if (!isValidAnnotationEditorMode(mode)) { + throw new Error(`Invalid AnnotationEditor mode: ${mode}`); + } + if (!this.pdfDocument) { + return; + } + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + const { + eventBus + } = this; + const updater = () => { + this.#cleanupSwitchAnnotationEditorMode(); + this.#annotationEditorMode = mode; + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode + }); + }; + if (mode === AnnotationEditorType.NONE || this.#annotationEditorMode === AnnotationEditorType.NONE) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.annotationStorage.resetModifiedIds(); + } + for (const pageView of this._pages) { + pageView.toggleEditingMode(isEditing); + } + const idsToRefresh = this.#switchToEditAnnotationMode(); + if (isEditing && idsToRefresh) { + this.#cleanupSwitchAnnotationEditorMode(); + this.#switchAnnotationEditorModeAC = new AbortController(); + const signal = AbortSignal.any([this.#eventAbortController.signal, this.#switchAnnotationEditorModeAC.signal]); + eventBus._on("pagerendered", ({ + pageNumber + }) => { + idsToRefresh.delete(pageNumber); + if (idsToRefresh.size === 0) { + this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0); + } + }, { + signal + }); + return; + } + } + updater(); + } + refresh(noUpdate = false, updateArgs = Object.create(null)) { + if (!this.pdfDocument) { + return; + } + for (const pageView of this._pages) { + pageView.update(updateArgs); + } + if (this.#scaleTimeoutId !== null) { + clearTimeout(this.#scaleTimeoutId); + this.#scaleTimeoutId = null; + } + if (!noUpdate) { + this.update(); + } + } +} + +;// ./web/pdf_single_page_viewer.js + + +class PDFSinglePageViewer extends PDFViewer { + _resetView() { + super._resetView(); + this._scrollMode = ScrollMode.PAGE; + this._spreadMode = SpreadMode.NONE; + } + set scrollMode(mode) {} + _updateScrollMode() {} + set spreadMode(mode) {} + _updateSpreadMode() {} +} + +;// ./web/pdf_viewer.component.js + + + + + + + + + + + + + + + +const pdfjsVersion = "4.10.38"; +const pdfjsBuild = "f9bea397f"; + +var __webpack_exports__AnnotationLayerBuilder = __webpack_exports__.AnnotationLayerBuilder; +var __webpack_exports__DownloadManager = __webpack_exports__.DownloadManager; +var __webpack_exports__EventBus = __webpack_exports__.EventBus; +var __webpack_exports__FindState = __webpack_exports__.FindState; +var __webpack_exports__GenericL10n = __webpack_exports__.GenericL10n; +var __webpack_exports__LinkTarget = __webpack_exports__.LinkTarget; +var __webpack_exports__PDFFindController = __webpack_exports__.PDFFindController; +var __webpack_exports__PDFHistory = __webpack_exports__.PDFHistory; +var __webpack_exports__PDFLinkService = __webpack_exports__.PDFLinkService; +var __webpack_exports__PDFPageView = __webpack_exports__.PDFPageView; +var __webpack_exports__PDFScriptingManager = __webpack_exports__.PDFScriptingManager; +var __webpack_exports__PDFSinglePageViewer = __webpack_exports__.PDFSinglePageViewer; +var __webpack_exports__PDFViewer = __webpack_exports__.PDFViewer; +var __webpack_exports__ProgressBar = __webpack_exports__.ProgressBar; +var __webpack_exports__RenderingStates = __webpack_exports__.RenderingStates; +var __webpack_exports__ScrollMode = __webpack_exports__.ScrollMode; +var __webpack_exports__SimpleLinkService = __webpack_exports__.SimpleLinkService; +var __webpack_exports__SpreadMode = __webpack_exports__.SpreadMode; +var __webpack_exports__StructTreeLayerBuilder = __webpack_exports__.StructTreeLayerBuilder; +var __webpack_exports__TextLayerBuilder = __webpack_exports__.TextLayerBuilder; +var __webpack_exports__XfaLayerBuilder = __webpack_exports__.XfaLayerBuilder; +var __webpack_exports__parseQueryString = __webpack_exports__.parseQueryString; +export { __webpack_exports__AnnotationLayerBuilder as AnnotationLayerBuilder, __webpack_exports__DownloadManager as DownloadManager, __webpack_exports__EventBus as EventBus, __webpack_exports__FindState as FindState, __webpack_exports__GenericL10n as GenericL10n, __webpack_exports__LinkTarget as LinkTarget, __webpack_exports__PDFFindController as PDFFindController, __webpack_exports__PDFHistory as PDFHistory, __webpack_exports__PDFLinkService as PDFLinkService, __webpack_exports__PDFPageView as PDFPageView, __webpack_exports__PDFScriptingManager as PDFScriptingManager, __webpack_exports__PDFSinglePageViewer as PDFSinglePageViewer, __webpack_exports__PDFViewer as PDFViewer, __webpack_exports__ProgressBar as ProgressBar, __webpack_exports__RenderingStates as RenderingStates, __webpack_exports__ScrollMode as ScrollMode, __webpack_exports__SimpleLinkService as SimpleLinkService, __webpack_exports__SpreadMode as SpreadMode, __webpack_exports__StructTreeLayerBuilder as StructTreeLayerBuilder, __webpack_exports__TextLayerBuilder as TextLayerBuilder, __webpack_exports__XfaLayerBuilder as XfaLayerBuilder, __webpack_exports__parseQueryString as parseQueryString }; + +//# sourceMappingURL=pdf_viewer.mjs.map \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md new file mode 100644 index 00000000000..69fb53bf322 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/README.md @@ -0,0 +1,7 @@ +tailwindcss version 4.0.3 +https://github.com/tailwindlabs/tailwindcss +License: MIT + +This template uses only `preflight.css`, the CSS reset stylesheet from Tailwind. For simplicity, this template doesn't use a complete deployment of Tailwind. If you want to use the full Tailwind library, remove `preflight.css` and reference the full version of Tailwind. + +To update, replace the `preflight.css` file with the one in an updated build from https://www.npmjs.com/package/tailwindcss diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css new file mode 100644 index 00000000000..178c881dd23 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/wwwroot/lib/tailwindcss/dist/preflight.css @@ -0,0 +1,383 @@ +/* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. +*/ + +*, +::after, +::before, +::backdrop, +::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ +} + +/* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + 5. Use the user's configured `sans` font-feature-settings by default. + 6. Use the user's configured `sans` font-variation-settings by default. + 7. Disable tap highlights on iOS. +*/ + +html, +:host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: var( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ +} + +/* + Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + line-height: inherit; +} + +/* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* + Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* + Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* + Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; +} + +/* + Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* + 1. Use the user's configured `mono` font-family by default. + 2. Use the user's configured `mono` font-feature-settings by default. + 3. Use the user's configured `mono` font-variation-settings by default. + 4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: var( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 4 */ + font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */ + font-size: 1em; /* 4 */ +} + +/* + Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* + Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* + Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* + Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* + Make lists unstyled by default. +*/ + +ol, +ul, +menu { + list-style: none; +} + +/* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. +*/ + +button, +input, +select, +optgroup, +textarea, +::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ +} + +/* + Restore default font weight. +*/ + +:where(select:is([multiple], [size])) optgroup { + font-weight: bolder; +} + +/* + Restore indentation. +*/ + +:where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; +} + +/* + Restore space after button. +*/ + +::file-selector-button { + margin-inline-end: 4px; +} + +/* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to a semi-transparent version of the current text color. +*/ + +::placeholder { + opacity: 1; /* 1 */ + color: color-mix(in oklab, currentColor 50%, transparent); /* 2 */ +} + +/* + Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* + Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. +*/ + +::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ +} + +/* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. +*/ + +::-webkit-datetime-edit { + display: inline-flex; +} + +/* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. +*/ + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-datetime-edit, +::-webkit-datetime-edit-year-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-minute-field, +::-webkit-datetime-edit-second-field, +::-webkit-datetime-edit-millisecond-field, +::-webkit-datetime-edit-meridiem-field { + padding-block: 0; +} + +/* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* + Correct the inability to style the border radius in iOS Safari. +*/ + +button, +input:where([type='button'], [type='reset'], [type='submit']), +::file-selector-button { + appearance: button; +} + +/* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* + Make elements with the HTML hidden attribute stay hidden by default. +*/ + +[hidden]:where(:not([hidden='until-found'])) { + display: none !important; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.sln b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.sln new file mode 100644 index 00000000000..67d2a3cad3c --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.AppHost", "aichatweb.AppHost\aichatweb.AppHost.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.ServiceDefaults", "aichatweb.ServiceDefaults\aichatweb.ServiceDefaults.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Project("{00000000-0000-0000-0000-000000000000}") = "aichatweb.Web", "aichatweb.Web\aichatweb.Web.csproj", "{00000000-0000-0000-0000-000000000000}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00000000-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md index 53730a18884..3f50502fb9f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md @@ -24,7 +24,7 @@ To use Azure AI Search, you will need an Azure account and an Azure AI Search re ### 1. Create an Azure AI Search Resource Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-aichatweb-ingested` index using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. +Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-aichatweb-chunks` and `data-aichatweb-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. ### 2. Configure Azure AI Search for Keyless Authentication This template is configured to use keyless authentication (also known as Managed Identity, with Entra ID). Before continuing, you'll need to configure your Azure AI Search resource to support this. [Learn more](https://learn.microsoft.com/azure/search/keyless-connections). After creation, ensure that you have selected Role-Based Access Control (RBAC) under Settings > Keys, as this is not the default. Assign yourself the roles called out for local development. [Learn more](https://learn.microsoft.com/azure/search/keyless-connections#roles-for-local-development). From fc92e583826b28883d5c6fb9866e740bebd1685b Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Fri, 16 May 2025 15:31:58 -0400 Subject: [PATCH 118/472] Remove preview tag on Azure DevOps extension (#6456) --- .../TypeScript/azure-devops-report/vss-extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/azure-devops-report/vss-extension.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/azure-devops-report/vss-extension.json index 48b88200efe..c596e90dfa2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/azure-devops-report/vss-extension.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/azure-devops-report/vss-extension.json @@ -7,7 +7,7 @@ "description": "Display an AI Evaluation report tab in Azure DevOps build results", "public": false, "categories": ["Azure Pipelines"], - "tags": ["Preview"], + "tags": [], "targets": [ { "id": "Microsoft.VisualStudio.Services" From 19cc568c229182a7e4cdc385f80ccc7603fbd50b Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 16 May 2025 22:33:18 -0700 Subject: [PATCH 119/472] Bump PackageValidation against 9.5.0 and enable for stable MEAI packages (#6458) * Bump PackageValidation against 9.5.0 and enable for stable MEAI packages * Enable PV to MEAI.Evaluation --- eng/Versions.props | 2 +- .../Microsoft.Extensions.AI.Abstractions.csproj | 1 - .../Microsoft.Extensions.AI.Evaluation.Console.csproj | 1 - .../Microsoft.Extensions.AI.Evaluation.Quality.csproj | 1 - .../Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj | 1 - .../CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj | 1 - .../Microsoft.Extensions.AI.Evaluation.csproj | 1 - .../Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj | 1 - 8 files changed, 1 insertion(+), 8 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index e401281109f..cfd6c8b92a1 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -6,7 +6,7 @@ preview 1 $(MajorVersion).$(MinorVersion).$(PatchVersion) - 9.4.0 + 9.5.0 $(MajorVersion).$(MinorVersion).0.0 n/a n/a diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj index 36730ffc322..77ed508757c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj @@ -12,7 +12,6 @@ AIEval normal true - false n/a n/a
    diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj index 371153aa17a..8a9ef12f2bd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj @@ -19,7 +19,6 @@ AIEval normal true - false n/a n/a
    diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj index cd496877960..9e9c1eeec3d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Microsoft.Extensions.AI.Evaluation.csproj @@ -10,7 +10,6 @@ AIEval normal true - false n/a n/a
    diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 52d24800c34..6331b3c1149 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -8,7 +8,6 @@ normal - false 89 85 From 3aee3c866040f35430f587ddbc1d296a033bd534 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Fri, 16 May 2025 23:38:19 -0700 Subject: [PATCH 120/472] Fix paramref tag (#6459) Co-authored-by: Jeff Handley --- .../Buffering/GlobalLogBufferingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs index 89a0bf0aa84..060d823b35d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs @@ -36,7 +36,7 @@ public class GlobalLogBufferingOptions ///
    /// /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, - /// so the buffering will be suspended for the time. + /// so the buffering will be suspended for the time. /// [TimeSpan(MinimumAutoFlushDuration, MaximumAutoFlushDuration)] public TimeSpan AutoFlushDuration { get; set; } = _defaultAutoFlushDuration; From 8651e7c7a0cc7b030ceaeea4c007108834d0ccaa Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 05:45:14 +0000 Subject: [PATCH 121/472] Update dependencies from https://github.com/dotnet/arcade build 20250516.2 (#6462) [main] Update dependencies from dotnet/arcade --- NuGet.config | 46 +++++++++++++++++++++++++++++++++++++++++ eng/Version.Details.xml | 12 +++++------ eng/Versions.props | 2 +- global.json | 4 ++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..1b80afbe11f 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,33 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -43,10 +66,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d68ceae460d..58258276208 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - 93823d49ca01742464ad1c0b49ea940e693b1be3 + c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 - + https://github.com/dotnet/arcade - 93823d49ca01742464ad1c0b49ea940e693b1be3 + c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 - + https://github.com/dotnet/arcade - 93823d49ca01742464ad1c0b49ea940e693b1be3 + c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 diff --git a/eng/Versions.props b/eng/Versions.props index cfd6c8b92a1..bafe6b74b17 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,7 +84,7 @@ 9.0.5 - 9.0.0-beta.25263.5 + 9.0.0-beta.25266.2 diff --git a/global.json b/global.json index 0df2af37e50..694e686a6b7 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25263.5", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25263.5" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25266.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25266.2" } } From b53b505397d59ada57c93168ee88b48df24ae4c7 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Mon, 19 May 2025 19:29:07 -0700 Subject: [PATCH 122/472] Add a script to diff the contents of folders that match a specific pattern across two branches (#6453) --- scripts/DiffBranches.ps1 | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 scripts/DiffBranches.ps1 diff --git a/scripts/DiffBranches.ps1 b/scripts/DiffBranches.ps1 new file mode 100644 index 00000000000..3224f5fac17 --- /dev/null +++ b/scripts/DiffBranches.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + A script to diff the contents of folders matching a specified pattern between two specified branches. + +.DESCRIPTION + The script uses git to determine the set of files (under folders matching a specified pattern) that are different + between the specified branches. It can also optionally display the line diffs for these files. + +.PARAMETER baseline + The baseline branch against which the specified target branch is to be compared. (Defaults to 'main' if omitted.) +.PARAMETER target + The target branch which is to be compared against the specified baseline branch. +.PARAMETER folderPattern + The pattern that selects the folders that are to be compared. (Defaults to '*.AI.* if omitted.) +.PARAMETER showDiff + Determines whether or not line diffs should be displayed for the differing files. (Defaults to 'false' if omitted.) + +.EXAMPLE + PS> .\DiffBranches -target "release/9.5" -folderPattern "*.Evaluation.*" +.EXAMPLE + PS> .\DiffBranches -baseline "release/9.4" -target "release/9.5" -folderPattern "*.Evaluation.*" +.EXAMPLE + PS> .\DiffBranches -target "release/9.5" -showDiff +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$baseline = "main", + + [Parameter(Mandatory=$true)] + [string]$target, + + [Parameter(Mandatory=$false)] + [string]$folderPattern = "*.AI.*", + + [Parameter(Mandatory=$false)] + [switch]$showDiff +) + +function Invoke-GitCommand { + param( + [Parameter(Mandatory=$true)] + [string]$Command, + + [Parameter(Mandatory=$false)] + [switch]$UseCmd + ) + + if ($UseCmd) { + $Command = "cmd.exe /c $Command" + } + + Write-Host "Executing $Command" -ForegroundColor Blue + $result = Invoke-Expression $Command + return $result +} + +# Save the current directory +$originalLocation = Get-Location + +try { + # Get the root directory of the git repository + $gitRootCommand = "git rev-parse --show-toplevel" + $repoRoot = Invoke-GitCommand -Command $gitRootCommand + Write-Host "Repo root is $repoRoot" -ForegroundColor Blue + + # Change to the repository root directory + Set-Location $repoRoot + + # Get all changed files between the two branches + $gitFilesCommand = "git diff --name-only $baseline..$target" + $changedFiles = Invoke-GitCommand -Command $gitFilesCommand + + # Filter for files under folders containing the specified pattern + $matchedFiles = $changedFiles | Where-Object { + $path = $_ + $folders = $path -split '/' + $folders | Where-Object { $_ -like $folderPattern } | Select-Object -First 1 + } + + if ($matchedFiles.Count -eq 0) { + Write-Host "No changes detected." -ForegroundColor Green + } else { + Write-Host "Changes detected in following files:" -ForegroundColor Yellow + $matchedFiles | ForEach-Object { Write-Host " $_" } + + if ($showDiff) { + Write-Host "File diffs:" -ForegroundColor Yellow + + $gitDiffCommand = "git -C `"$repoRoot`" diff --color $baseline..$target -- $($matchedFiles -join ' ')" + + # Use the -UseCmd switch to run the command with cmd.exe (preserves color and paging) + Invoke-GitCommand -Command $gitDiffCommand -UseCmd + } + } +} +finally { + # Return to the original directory even if an error occurs + Set-Location $originalLocation +} From 6ae09b8eab86ac4b07870e906f6c7f4337548dbc Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 20 May 2025 15:17:19 +0300 Subject: [PATCH 123/472] Remove `title` and `description` keywords from root-level schemas in AIFunctionFactory. (#6465) --- .../Functions/AIFunctionFactory.cs | 8 ++++---- .../Utilities/AIJsonUtilitiesTests.cs | 10 ++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index d5274186645..ffd47eb08fc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -692,10 +692,10 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions JsonSerializerOptions = serializerOptions; JsonSchema = AIJsonUtilities.CreateFunctionJsonSchema( key.Method, - Name, - Description, - serializerOptions, - schemaOptions); + title: string.Empty, // Forces skipping of the title keyword + description: string.Empty, // Forces skipping of the description keyword + serializerOptions: serializerOptions, + inferenceOptions: schemaOptions); } public string Name { get; } 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 c67a6147186..20dfeb62f5c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -304,7 +304,7 @@ public static void CreateFunctionJsonSchema_ReturnsExpectedValue() Assert.NotNull(func.UnderlyingMethod); - JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.UnderlyingMethod, title: func.Name); + JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.UnderlyingMethod, title: string.Empty); AssertDeepEquals(resolvedSchema, func.JsonSchema); } @@ -333,8 +333,6 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr JsonElement expected = JsonDocument.Parse($$""" { - "title": "get_weather", - "description": "Gets the current weather for a current location", "type": "object", "properties": { "city": { @@ -369,11 +367,7 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr Assert.NotNull(func.UnderlyingMethod); AssertDeepEquals(expected, func.JsonSchema); - JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema( - func.UnderlyingMethod, - title: func.Name, - description: func.Description, - inferenceOptions: inferenceOptions); + JsonElement resolvedSchema = AIJsonUtilities.CreateFunctionJsonSchema(func.UnderlyingMethod, title: string.Empty, description: string.Empty, inferenceOptions: inferenceOptions); AssertDeepEquals(expected, resolvedSchema); } From 712d9aa99b61b85fe2c8e30a67eef6afe17d7f92 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 20 May 2025 11:17:01 -0700 Subject: [PATCH 124/472] Replace JSON vector store with SQLite (#6438) Co-authored-by: Jeff Handley --- src/ProjectTemplates/.gitignore | 1 + src/ProjectTemplates/GeneratedContent.targets | 7 +- .../Microsoft.Extensions.AI.Templates.csproj | 5 +- .../ChatWithCustomData-CSharp.Web.csproj.in | 2 + .../Program.Aspire.cs | 20 +- .../ChatWithCustomData-CSharp.Web/Program.cs | 23 +- .../Services/IngestedChunk.cs | 27 ++- .../Services/IngestedDocument.cs | 17 +- .../Services/Ingestion/DataIngestor.cs | 22 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../Services/JsonVectorStore.cs | 214 ------------------ .../Services/SemanticSearch.cs | 19 +- .../Directory.Build.props.in | 5 + .../Directory.Build.targets.in | 5 +- .../src/ChatWithCustomData/NuGet.config.in | 19 +- .../AIChatWebSnapshotTests.cs | 1 + .../aichatweb/aichatweb.Web/Program.cs | 6 +- .../aichatweb.Web/Services/IngestedChunk.cs | 15 +- .../Services/IngestedDocument.cs | 13 +- .../Services/Ingestion/DataIngestor.cs | 12 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../aichatweb.Web/Services/SemanticSearch.cs | 11 +- .../aichatweb.Web/aichatweb.Web.csproj | 4 +- .../aichatweb/Program.cs | 7 +- .../aichatweb/Services/IngestedChunk.cs | 15 +- .../aichatweb/Services/IngestedDocument.cs | 13 +- .../Services/Ingestion/DataIngestor.cs | 12 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../aichatweb/Services/JsonVectorStore.cs | 214 ------------------ .../aichatweb/Services/SemanticSearch.cs | 11 +- .../aichatweb/aichatweb.csproj | 3 +- .../aichatweb/aichatweb.Web/Program.cs | 7 +- .../aichatweb.Web/Services/IngestedChunk.cs | 15 +- .../Services/IngestedDocument.cs | 13 +- .../Services/Ingestion/DataIngestor.cs | 12 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../aichatweb.Web/Services/JsonVectorStore.cs | 214 ------------------ .../aichatweb.Web/Services/SemanticSearch.cs | 11 +- .../aichatweb.Web/aichatweb.Web.csproj | 3 +- .../aichatweb/aichatweb.Web/Program.cs | 6 +- .../aichatweb.Web/Services/IngestedChunk.cs | 15 +- .../Services/IngestedDocument.cs | 13 +- .../Services/Ingestion/DataIngestor.cs | 12 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../aichatweb.Web/Services/SemanticSearch.cs | 11 +- .../aichatweb.Web/aichatweb.Web.csproj | 4 +- .../aichatweb/Program.cs | 13 +- .../aichatweb/Services/IngestedChunk.cs | 15 +- .../aichatweb/Services/IngestedDocument.cs | 13 +- .../Services/Ingestion/DataIngestor.cs | 12 +- .../Services/Ingestion/IIngestionSource.cs | 6 +- .../Services/Ingestion/PDFDirectorySource.cs | 16 +- .../aichatweb/Services/SemanticSearch.cs | 11 +- .../aichatweb/aichatweb.csproj | 4 +- 59 files changed, 296 insertions(+), 963 deletions(-) delete mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.props.in delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs delete mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs diff --git a/src/ProjectTemplates/.gitignore b/src/ProjectTemplates/.gitignore index 9262ab010fb..ceffca4abbc 100644 --- a/src/ProjectTemplates/.gitignore +++ b/src/ProjectTemplates/.gitignore @@ -9,6 +9,7 @@ package-lock.json */src/**/*.sln */src/**/NuGet.config */src/**/Directory.Build.targets +*/src/**/Directory.Build.props */src/**/ingestioncache.* # launchSettings.json files are required for the templates. diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 41394a33dd9..5561a5214d1 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,8 +35,8 @@ 11.6.0 9.4.1-beta.277 9.2.0 - 1.50.0 - 1.50.0-preview + 1.52.1 + 1.52.1-preview 5.1.16 1.9.0 0.1.10 @@ -85,6 +85,9 @@ + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 9997515d70c..2827734c794 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -44,7 +44,7 @@ - + + **\Directory.Build.targets; + **\Directory.Build.props;" /> diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index e13c7eb63ee..46d7382d6f1 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -45,6 +45,8 @@ #elif (UseQdrant)--> + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 5ecfd95d159..adcb2452d87 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using ChatWithCustomData_CSharp.Web.Components; using ChatWithCustomData_CSharp.Web.Services; using ChatWithCustomData_CSharp.Web.Services.Ingestion; @@ -7,11 +6,6 @@ #else // IsAzureOpenAI || IsOpenAI || IsGHModels using OpenAI; #endif -#if (UseAzureAISearch) -using Microsoft.SemanticKernel.Connectors.AzureAISearch; -#elif (UseQdrant) -using Microsoft.SemanticKernel.Connectors.Qdrant; -#endif var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -41,15 +35,17 @@ #if (UseAzureAISearch) builder.AddAzureSearchClient("azureAISearch"); - -builder.Services.AddSingleton(); +builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks"); +builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents"); #elif (UseQdrant) builder.AddQdrantClient("vectordb"); - -builder.Services.AddSingleton(); +builder.Services.AddQdrantCollection("data-ChatWithCustomData-CSharp.Web-chunks"); +builder.Services.AddQdrantCollection("data-ChatWithCustomData-CSharp.Web-documents"); #else // UseLocalVectorStore -var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store")); -builder.Services.AddSingleton(vectorStore); +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-chunks", vectorStoreConnectionString); +builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-documents", vectorStoreConnectionString); #endif builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index 1cea41e02eb..f3f5740066f 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using ChatWithCustomData_CSharp.Web.Components; using ChatWithCustomData_CSharp.Web.Services; using ChatWithCustomData_CSharp.Web.Services.Ingestion; @@ -18,10 +17,6 @@ using Azure.AI.OpenAI; using System.ClientModel; #endif -#if (UseAzureAISearch) -using Azure.Search.Documents.Indexes; -using Microsoft.SemanticKernel.Connectors.AzureAISearch; -#endif var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); @@ -83,19 +78,23 @@ #if (!UseManagedIdentity) // dotnet user-secrets set AzureAISearch:Key YOUR-API-KEY #endif -var vectorStore = new AzureAISearchVectorStore( - new SearchIndexClient( - new Uri(builder.Configuration["AzureAISearch:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Endpoint. See the README for details.")), +var azureAISearchEndpoint = new Uri(builder.Configuration["AzureAISearch:Endpoint"] + ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Endpoint. See the README for details.")); #if (UseManagedIdentity) - new DefaultAzureCredential())); +var azureAISearchCredential = new DefaultAzureCredential(); #else - new AzureKeyCredential(builder.Configuration["AzureAISearch:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Key. See the README for details.")))); +var azureAISearchCredential = new AzureKeyCredential(builder.Configuration["AzureAISearch:Key"] + ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Key. See the README for details.")); #endif +builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks", azureAISearchEndpoint, azureAISearchCredential); +builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents", azureAISearchEndpoint, azureAISearchCredential); #else // UseLocalVectorStore -var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store")); +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-chunks", vectorStoreConnectionString); +builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-documents", vectorStoreConnectionString); #endif -builder.Services.AddSingleton(vectorStore); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs index 641d2adfb38..c1369e1bf65 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs @@ -4,26 +4,33 @@ namespace ChatWithCustomData_CSharp.Web.Services; public class IngestedChunk { - [VectorStoreRecordKey] +#if (IsOllama) + private const int VectorDimensions = 384; // 384 is the default vector size for the all-minilm embedding model +#else + private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model +#endif +#if (UseAzureAISearch || UseQdrant) + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; +#else + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; +#endif + + [VectorStoreKey] #if (UseQdrant) public required Guid Key { get; set; } #else public required string Key { get; set; } #endif - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } -#if (IsOllama) - [VectorStoreRecordVector(384, DistanceFunction = DistanceFunction.CosineSimilarity)] // 384 is the default vector size for the all-minilm embedding model -#else - [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model -#endif - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs index c99a2c78423..339a7479217 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs @@ -4,23 +4,30 @@ namespace ChatWithCustomData_CSharp.Web.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; +#if (UseAzureAISearch || UseQdrant) + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; +#else + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; +#endif + + [VectorStoreKey] #if (UseQdrant) public required Guid Key { get; set; } #else public required string Key { get; set; } #endif - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs index eb64fa84c1b..7eeb41c99fb 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,13 @@ namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) +#if (UseQdrant) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) +#else + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) +#endif { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,15 +22,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { -#if (UseQdrant) - var chunksCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); - var documentsCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-documents"); -#else - var chunksCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); - var documentsCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-documents"); -#endif - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -46,7 +44,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs index 051bbbfebda..ae06879b4ce 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; +namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs index f52cd86c6e6..fdbe058556e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -46,14 +45,12 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { #if (UseQdrant) Key = Guid.CreateVersion7(), @@ -61,10 +58,9 @@ public async Task> CreateChunksForDocumentAsync(IEmbe Key = Guid.CreateVersion7().ToString(), #endif DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs deleted file mode 100644 index b70af9fa033..00000000000 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs +++ /dev/null @@ -1,214 +0,0 @@ -using Microsoft.Extensions.VectorData; -using System.Linq.Expressions; -using System.Numerics.Tensors; -using System.Reflection; -using System.Text.Json; - -namespace ChatWithCustomData_CSharp.Web.Services; - -/// -/// This IVectorStore implementation is for prototyping only. Do not use this in production. -/// In production, you must replace this with a real vector store. There are many IVectorStore -/// implementations available, including ones for standalone vector databases like Qdrant or Milvus, -/// or for integrating with relational databases such as SQL Server or PostgreSQL. -/// -/// This implementation stores the vector records in large JSON files on disk. It is very inefficient -/// and is provided only for convenience when prototyping. -/// -public class JsonVectorStore(string basePath) : IVectorStore -{ - public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) - => Task.FromResult(File.Exists(FilePath(name))); - - public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) - { - File.Delete(FilePath(name)); - return Task.CompletedTask; - } - - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) - where TKey : notnull - where TRecord : notnull - => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); - - public object? GetService(Type serviceType, object? serviceKey = null) - => serviceKey is not null ? null : - serviceType.IsInstanceOfType(this) ? this : - null; - - public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); - - private string FilePath(string collectionName) - => Path.Combine(basePath, collectionName + ".json"); - - private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection - where TKey : notnull - where TRecord : notnull - { - private static readonly Func _getKey = CreateKeyReader(); - private static readonly Func> _getVector = CreateVectorReader(); - - private readonly string _name; - private readonly string _filePath; - private Dictionary? _records; - - public JsonVectorStoreRecordCollection(string name, string filePath, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - _name = name; - _filePath = filePath; - - if (File.Exists(filePath)) - { - _records = JsonSerializer.Deserialize>(File.ReadAllText(filePath)); - } - } - - public string Name => _name; - - public Task CollectionExistsAsync(CancellationToken cancellationToken = default) - => Task.FromResult(_records is not null); - - public async Task CreateCollectionAsync(CancellationToken cancellationToken = default) - { - _records = []; - await WriteToDiskAsync(cancellationToken); - } - - public async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) - { - if (_records is null) - { - await CreateCollectionAsync(cancellationToken); - } - } - - public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) - { - _records!.Remove(key); - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) - { - foreach (var key in keys) - { - _records!.Remove(key); - } - - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) - { - _records = null; - File.Delete(_filePath); - return Task.CompletedTask; - } - - public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => Task.FromResult(_records!.GetValueOrDefault(key)); - - public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - - public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) - { - var filterCompiled = filter.Compile(); - var matches = _records!.Values.Where(r => filterCompiled(r)); - - if (options?.OrderBy is { } orderBy) - { - var matchesQueryable = matches.AsQueryable(); - foreach (var sort in orderBy.Values) - { - matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); - } - matches = matchesQueryable; - } - - return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); - } - - public object? GetService(Type serviceType, object? serviceKey = null) - => null; - - public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull - { - throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); - } - - public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - { - if (vector is not ReadOnlyMemory floatVector) - { - throw new NotSupportedException($"The provided vector type {vector!.GetType().FullName} is not supported."); - } - - IEnumerable filteredRecords = _records!.Values; - if (options?.Filter is { } filter) - { - filteredRecords = filteredRecords.AsQueryable().Where(filter); - } - - var ranked = from record in filteredRecords - let candidateVector = _getVector(record) - let similarity = TensorPrimitives.CosineSimilarity(candidateVector.Span, floatVector.Span) - orderby similarity descending - select (Record: record, Similarity: similarity); - - var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); - return results.ToAsyncEnumerable(); - } - - public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - => SearchEmbeddingAsync(vector, top, options, cancellationToken); - - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) - { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } - - public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) - { - var key = _getKey(record); - _records![key] = record; - results.Add(key); - } - - await WriteToDiskAsync(cancellationToken); - return results; - } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } - - private static Func CreateKeyReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(TKey)) - .Single(); - return record => (TKey)propertyInfo.GetValue(record)!; - } - - private static Func> CreateVectorReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(ReadOnlyMemory)) - .Single(); - return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; - } - } -} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs index 2c32363c139..44cfcc18fc4 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs @@ -1,22 +1,17 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace ChatWithCustomData_CSharp.Web.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) -{ - public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) - { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); #if (UseQdrant) - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); + VectorStoreCollection vectorCollection) #else - var vectorCollection = vectorStore.GetCollection("data-ChatWithCustomData-CSharp.Web-chunks"); + VectorStoreCollection vectorCollection) #endif - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions +{ + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) + { + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.props.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.props.in new file mode 100644 index 00000000000..0eb47a5ac25 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.props.in @@ -0,0 +1,5 @@ + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in index 66ea183ef70..670604290b9 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in @@ -1,10 +1,9 @@ - - <_UsingJustBuiltPackages>${UsingJustBuiltPackages} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/NuGet.config.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/NuGet.config.in index ca3a2eae083..76020d40e7f 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/NuGet.config.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/NuGet.config.in @@ -4,17 +4,22 @@ It will not get included in the built project template. --> + + + + + + + + + + - - - - - - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs index e02ac53de0d..0f3d0951631 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs @@ -31,6 +31,7 @@ public class AIChatWebSnapshotTests "**/ingestioncache.*", "**/NuGet.config", "**/Directory.Build.targets", + "**/Directory.Build.props", ]; private readonly ILogger _log; diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 4711722548a..9a698ff1763 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; using OpenAI; -using Microsoft.SemanticKernel.Connectors.AzureAISearch; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -18,8 +16,8 @@ openai.AddEmbeddingGenerator("text-embedding-3-small"); builder.AddAzureSearchClient("azureAISearch"); - -builder.Services.AddSingleton(); +builder.Services.AddAzureAISearchCollection("data-aichatweb-chunks"); +builder.Services.AddAzureAISearchCollection("data-aichatweb-documents"); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index cff3717518e..0fd76874dfd 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -4,18 +4,21 @@ namespace aichatweb.Web.Services; public class IngestedChunk { - [VectorStoreRecordKey] + private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs index 4be3b2980d7..370aef16fd9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -4,19 +4,22 @@ namespace aichatweb.Web.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 448c2ce43b7..59732141849 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,8 @@ namespace aichatweb.Web.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,10 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -41,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs index 208b32b2fdf..a1c6b2191d1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace aichatweb.Web.Services.Ingestion; +namespace aichatweb.Web.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index 01c370d9dec..32e9f225c08 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -42,21 +41,18 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs index 47b30dd0646..84fb719f6ae 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -1,18 +1,13 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace aichatweb.Web.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection vectorCollection) { public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 4ef46e9f924..e1739390adc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs index 7e14e993839..0e97b8efffe 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using aichatweb.Components; using aichatweb.Services; using aichatweb.Services.Ingestion; @@ -23,9 +22,11 @@ var chatClient = ghModelsClient.GetChatClient("gpt-4o-mini").AsIChatClient(); var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); -var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store")); +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +builder.Services.AddSqliteCollection("data-aichatweb-chunks", vectorStoreConnectionString); +builder.Services.AddSqliteCollection("data-aichatweb-documents", vectorStoreConnectionString); -builder.Services.AddSingleton(vectorStore); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs index 9b61b7f9795..2c5a38c7912 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedChunk.cs @@ -4,18 +4,21 @@ namespace aichatweb.Services; public class IngestedChunk { - [VectorStoreRecordKey] + private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs index cc852657143..f101cfdc96a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/IngestedDocument.cs @@ -4,19 +4,22 @@ namespace aichatweb.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs index 757a92bcc14..65b520980c1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,8 @@ namespace aichatweb.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,10 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -41,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs index 89fdc81ada6..540cac117e7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace aichatweb.Services.Ingestion; +namespace aichatweb.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 39a079e76fe..0be02a9d008 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -42,21 +41,18 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs deleted file mode 100644 index 3b1d6024357..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/JsonVectorStore.cs +++ /dev/null @@ -1,214 +0,0 @@ -using Microsoft.Extensions.VectorData; -using System.Linq.Expressions; -using System.Numerics.Tensors; -using System.Reflection; -using System.Text.Json; - -namespace aichatweb.Services; - -/// -/// This IVectorStore implementation is for prototyping only. Do not use this in production. -/// In production, you must replace this with a real vector store. There are many IVectorStore -/// implementations available, including ones for standalone vector databases like Qdrant or Milvus, -/// or for integrating with relational databases such as SQL Server or PostgreSQL. -/// -/// This implementation stores the vector records in large JSON files on disk. It is very inefficient -/// and is provided only for convenience when prototyping. -/// -public class JsonVectorStore(string basePath) : IVectorStore -{ - public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) - => Task.FromResult(File.Exists(FilePath(name))); - - public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) - { - File.Delete(FilePath(name)); - return Task.CompletedTask; - } - - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) - where TKey : notnull - where TRecord : notnull - => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); - - public object? GetService(Type serviceType, object? serviceKey = null) - => serviceKey is not null ? null : - serviceType.IsInstanceOfType(this) ? this : - null; - - public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); - - private string FilePath(string collectionName) - => Path.Combine(basePath, collectionName + ".json"); - - private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection - where TKey : notnull - where TRecord : notnull - { - private static readonly Func _getKey = CreateKeyReader(); - private static readonly Func> _getVector = CreateVectorReader(); - - private readonly string _name; - private readonly string _filePath; - private Dictionary? _records; - - public JsonVectorStoreRecordCollection(string name, string filePath, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - _name = name; - _filePath = filePath; - - if (File.Exists(filePath)) - { - _records = JsonSerializer.Deserialize>(File.ReadAllText(filePath)); - } - } - - public string Name => _name; - - public Task CollectionExistsAsync(CancellationToken cancellationToken = default) - => Task.FromResult(_records is not null); - - public async Task CreateCollectionAsync(CancellationToken cancellationToken = default) - { - _records = []; - await WriteToDiskAsync(cancellationToken); - } - - public async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) - { - if (_records is null) - { - await CreateCollectionAsync(cancellationToken); - } - } - - public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) - { - _records!.Remove(key); - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) - { - foreach (var key in keys) - { - _records!.Remove(key); - } - - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) - { - _records = null; - File.Delete(_filePath); - return Task.CompletedTask; - } - - public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => Task.FromResult(_records!.GetValueOrDefault(key)); - - public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - - public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) - { - var filterCompiled = filter.Compile(); - var matches = _records!.Values.Where(r => filterCompiled(r)); - - if (options?.OrderBy is { } orderBy) - { - var matchesQueryable = matches.AsQueryable(); - foreach (var sort in orderBy.Values) - { - matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); - } - matches = matchesQueryable; - } - - return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); - } - - public object? GetService(Type serviceType, object? serviceKey = null) - => null; - - public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull - { - throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); - } - - public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - { - if (vector is not ReadOnlyMemory floatVector) - { - throw new NotSupportedException($"The provided vector type {vector!.GetType().FullName} is not supported."); - } - - IEnumerable filteredRecords = _records!.Values; - if (options?.Filter is { } filter) - { - filteredRecords = filteredRecords.AsQueryable().Where(filter); - } - - var ranked = from record in filteredRecords - let candidateVector = _getVector(record) - let similarity = TensorPrimitives.CosineSimilarity(candidateVector.Span, floatVector.Span) - orderby similarity descending - select (Record: record, Similarity: similarity); - - var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); - return results.ToAsyncEnumerable(); - } - - public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - => SearchEmbeddingAsync(vector, top, options, cancellationToken); - - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) - { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } - - public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) - { - var key = _getKey(record); - _records![key] = record; - results.Add(key); - } - - await WriteToDiskAsync(cancellationToken); - return results; - } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } - - private static Func CreateKeyReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(TKey)) - .Single(); - return record => (TKey)propertyInfo.GetValue(record)!; - } - - private static Func> CreateVectorReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(ReadOnlyMemory)) - .Single(); - return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; - } - } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs index c380a69ce25..291c6c4b4a9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/SemanticSearch.cs @@ -1,18 +1,13 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace aichatweb.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection vectorCollection) { public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 9f70601878b..4fdb7120891 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -10,9 +10,10 @@ - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs index 665d1289fef..a98905903b3 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; @@ -16,8 +15,10 @@ c.EnableSensitiveData = builder.Environment.IsDevelopment()); openai.AddEmbeddingGenerator("text-embedding-3-small"); -var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store")); -builder.Services.AddSingleton(vectorStore); +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +builder.Services.AddSqliteCollection("data-aichatweb-chunks", vectorStoreConnectionString); +builder.Services.AddSqliteCollection("data-aichatweb-documents", vectorStoreConnectionString); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index cff3717518e..92e50e61414 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -4,18 +4,21 @@ namespace aichatweb.Web.Services; public class IngestedChunk { - [VectorStoreRecordKey] + private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs index 4be3b2980d7..49a8143005e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -4,19 +4,22 @@ namespace aichatweb.Web.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; + private const string VectorDistanceFunction = DistanceFunction.CosineDistance; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 448c2ce43b7..59732141849 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,8 @@ namespace aichatweb.Web.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,10 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -41,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs index 208b32b2fdf..a1c6b2191d1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace aichatweb.Web.Services.Ingestion; +namespace aichatweb.Web.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index 01c370d9dec..32e9f225c08 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -42,21 +41,18 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs deleted file mode 100644 index 3e226c76150..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/JsonVectorStore.cs +++ /dev/null @@ -1,214 +0,0 @@ -using Microsoft.Extensions.VectorData; -using System.Linq.Expressions; -using System.Numerics.Tensors; -using System.Reflection; -using System.Text.Json; - -namespace aichatweb.Web.Services; - -/// -/// This IVectorStore implementation is for prototyping only. Do not use this in production. -/// In production, you must replace this with a real vector store. There are many IVectorStore -/// implementations available, including ones for standalone vector databases like Qdrant or Milvus, -/// or for integrating with relational databases such as SQL Server or PostgreSQL. -/// -/// This implementation stores the vector records in large JSON files on disk. It is very inefficient -/// and is provided only for convenience when prototyping. -/// -public class JsonVectorStore(string basePath) : IVectorStore -{ - public Task CollectionExistsAsync(string name, CancellationToken cancellationToken = default) - => Task.FromResult(File.Exists(FilePath(name))); - - public Task DeleteCollectionAsync(string name, CancellationToken cancellationToken = default) - { - File.Delete(FilePath(name)); - return Task.CompletedTask; - } - - public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) - where TKey : notnull - where TRecord : notnull - => new JsonVectorStoreRecordCollection(name, FilePath(name), vectorStoreRecordDefinition); - - public object? GetService(Type serviceType, object? serviceKey = null) - => serviceKey is not null ? null : - serviceType.IsInstanceOfType(this) ? this : - null; - - public IAsyncEnumerable ListCollectionNamesAsync(CancellationToken cancellationToken = default) - => Directory.EnumerateFiles(basePath, "*.json").Select(f => Path.GetFileNameWithoutExtension(f)!).ToAsyncEnumerable(); - - private string FilePath(string collectionName) - => Path.Combine(basePath, collectionName + ".json"); - - private class JsonVectorStoreRecordCollection : IVectorStoreRecordCollection - where TKey : notnull - where TRecord : notnull - { - private static readonly Func _getKey = CreateKeyReader(); - private static readonly Func> _getVector = CreateVectorReader(); - - private readonly string _name; - private readonly string _filePath; - private Dictionary? _records; - - public JsonVectorStoreRecordCollection(string name, string filePath, VectorStoreRecordDefinition? vectorStoreRecordDefinition) - { - _name = name; - _filePath = filePath; - - if (File.Exists(filePath)) - { - _records = JsonSerializer.Deserialize>(File.ReadAllText(filePath)); - } - } - - public string Name => _name; - - public Task CollectionExistsAsync(CancellationToken cancellationToken = default) - => Task.FromResult(_records is not null); - - public async Task CreateCollectionAsync(CancellationToken cancellationToken = default) - { - _records = []; - await WriteToDiskAsync(cancellationToken); - } - - public async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) - { - if (_records is null) - { - await CreateCollectionAsync(cancellationToken); - } - } - - public Task DeleteAsync(TKey key, CancellationToken cancellationToken = default) - { - _records!.Remove(key); - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteAsync(IEnumerable keys, CancellationToken cancellationToken = default) - { - foreach (var key in keys) - { - _records!.Remove(key); - } - - return WriteToDiskAsync(cancellationToken); - } - - public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) - { - _records = null; - File.Delete(_filePath); - return Task.CompletedTask; - } - - public Task GetAsync(TKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => Task.FromResult(_records!.GetValueOrDefault(key)); - - public IAsyncEnumerable GetAsync(IEnumerable keys, GetRecordOptions? options = null, CancellationToken cancellationToken = default) - => keys.Select(key => _records!.GetValueOrDefault(key)!).Where(r => r is not null).ToAsyncEnumerable(); - - public IAsyncEnumerable GetAsync(Expression> filter, int top, GetFilteredRecordOptions? options = null, CancellationToken cancellationToken = default) - { - var filterCompiled = filter.Compile(); - var matches = _records!.Values.Where(r => filterCompiled(r)); - - if (options?.OrderBy is { } orderBy) - { - var matchesQueryable = matches.AsQueryable(); - foreach (var sort in orderBy.Values) - { - matchesQueryable = sort.Ascending ? matchesQueryable.OrderBy(sort.PropertySelector) : matchesQueryable.OrderByDescending(sort.PropertySelector); - } - matches = matchesQueryable; - } - - return matches.Take(top).Skip(options?.Skip ?? 0).ToAsyncEnumerable(); - } - - public object? GetService(Type serviceType, object? serviceKey = null) - => null; - - public IAsyncEnumerable> SearchAsync(TInput value, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TInput : notnull - { - throw new NotImplementedException("The temporary JsonVectorStore type does not support generating embeddings. Use SearchEmbeddingAsync instead."); - } - - public IAsyncEnumerable> SearchEmbeddingAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - { - if (vector is not ReadOnlyMemory floatVector) - { - throw new NotSupportedException($"The provided vector type {vector!.GetType().FullName} is not supported."); - } - - IEnumerable filteredRecords = _records!.Values; - if (options?.Filter is { } filter) - { - filteredRecords = filteredRecords.AsQueryable().Where(filter); - } - - var ranked = from record in filteredRecords - let candidateVector = _getVector(record) - let similarity = TensorPrimitives.CosineSimilarity(candidateVector.Span, floatVector.Span) - orderby similarity descending - select (Record: record, Similarity: similarity); - - var results = ranked.Skip(options?.Skip ?? 0).Take(top).Select(r => new VectorSearchResult(r.Record, r.Similarity)); - return results.ToAsyncEnumerable(); - } - - public IAsyncEnumerable> VectorizedSearchAsync(TVector vector, int top, VectorSearchOptions? options = null, CancellationToken cancellationToken = default) where TVector : notnull - => SearchEmbeddingAsync(vector, top, options, cancellationToken); - - public async Task UpsertAsync(TRecord record, CancellationToken cancellationToken = default) - { - var key = _getKey(record); - _records![key] = record; - await WriteToDiskAsync(cancellationToken); - return key; - } - - public async Task> UpsertAsync(IEnumerable records, CancellationToken cancellationToken = default) - { - var results = new List(); - foreach (var record in records) - { - var key = _getKey(record); - _records![key] = record; - results.Add(key); - } - - await WriteToDiskAsync(cancellationToken); - return results; - } - - private async Task WriteToDiskAsync(CancellationToken cancellationToken = default) - { - var json = JsonSerializer.Serialize(_records); - Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await File.WriteAllTextAsync(_filePath, json, cancellationToken); - } - - private static Func CreateKeyReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(TKey)) - .Single(); - return record => (TKey)propertyInfo.GetValue(record)!; - } - - private static Func> CreateVectorReader() - { - var propertyInfo = typeof(TRecord).GetProperties() - .Where(p => p.GetCustomAttribute() is not null - && p.PropertyType == typeof(ReadOnlyMemory)) - .Single(); - return record => (ReadOnlyMemory)propertyInfo.GetValue(record)!; - } - } -} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs index 47b30dd0646..84fb719f6ae 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -1,18 +1,13 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace aichatweb.Web.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection vectorCollection) { public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index d53f16b086c..189b183a946 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,9 +11,10 @@ - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs index a38b248d45e..cdc88a082b7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs @@ -1,9 +1,7 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; -using Microsoft.SemanticKernel.Connectors.Qdrant; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -18,8 +16,8 @@ .AddEmbeddingGenerator(); builder.AddQdrantClient("vectordb"); - -builder.Services.AddSingleton(); +builder.Services.AddQdrantCollection("data-aichatweb-chunks"); +builder.Services.AddQdrantCollection("data-aichatweb-documents"); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index 018a4e5b4e5..0e161a6278b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -4,18 +4,21 @@ namespace aichatweb.Web.Services; public class IngestedChunk { - [VectorStoreRecordKey] + private const int VectorDimensions = 384; // 384 is the default vector size for the all-minilm embedding model + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required Guid Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } - [VectorStoreRecordVector(384, DistanceFunction = DistanceFunction.CosineSimilarity)] // 384 is the default vector size for the all-minilm embedding model - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs index 1cce8f3566c..8a6ec320251 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/IngestedDocument.cs @@ -4,19 +4,22 @@ namespace aichatweb.Web.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required Guid Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 0b1aaa24803..d0f7a6bc3a8 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,8 @@ namespace aichatweb.Web.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,10 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -41,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs index 208b32b2fdf..a1c6b2191d1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace aichatweb.Web.Services.Ingestion; +namespace aichatweb.Web.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs index edf5a888880..da043feb526 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -42,21 +41,18 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { Key = Guid.CreateVersion7(), DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs index b39ae8027a2..044e8378595 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs @@ -1,18 +1,13 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace aichatweb.Web.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection vectorCollection) { public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index fcd785dbe01..8fe4a7464d4 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index f7b32a97610..2b9f0790817 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; using aichatweb.Components; using aichatweb.Services; using aichatweb.Services.Ingestion; @@ -7,8 +6,6 @@ using Azure.Identity; using OpenAI; using System.ClientModel; -using Azure.Search.Documents.Indexes; -using Microsoft.SemanticKernel.Connectors.AzureAISearch; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); @@ -26,12 +23,12 @@ // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory // dotnet user-secrets set AzureAISearch:Endpoint https://YOUR-DEPLOYMENT-NAME.search.windows.net -var vectorStore = new AzureAISearchVectorStore( - new SearchIndexClient( - new Uri(builder.Configuration["AzureAISearch:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Endpoint. See the README for details.")), - new DefaultAzureCredential())); +var azureAISearchEndpoint = new Uri(builder.Configuration["AzureAISearch:Endpoint"] + ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Endpoint. See the README for details.")); +var azureAISearchCredential = new DefaultAzureCredential(); +builder.Services.AddAzureAISearchCollection("data-aichatweb-chunks", azureAISearchEndpoint, azureAISearchCredential); +builder.Services.AddAzureAISearchCollection("data-aichatweb-documents", azureAISearchEndpoint, azureAISearchCredential); -builder.Services.AddSingleton(vectorStore); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs index 9b61b7f9795..46270588cde 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedChunk.cs @@ -4,18 +4,21 @@ namespace aichatweb.Services; public class IngestedChunk { - [VectorStoreRecordKey] + private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public int PageNumber { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string Text { get; set; } - [VectorStoreRecordVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] // 1536 is the default vector size for the OpenAI text-embedding-3-small model - public ReadOnlyMemory Vector { get; set; } + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] + public string? Vector => Text; } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs index cc852657143..9b3da6058c9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/IngestedDocument.cs @@ -4,19 +4,22 @@ namespace aichatweb.Services; public class IngestedDocument { - [VectorStoreRecordKey] + private const int VectorDimensions = 2; + private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; + + [VectorStoreKey] public required string Key { get; set; } - [VectorStoreRecordData(IsIndexed = true)] + [VectorStoreData(IsIndexed = true)] public required string SourceId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentId { get; set; } - [VectorStoreRecordData] + [VectorStoreData] public required string DocumentVersion { get; set; } // The vector is not used but required for some vector databases - [VectorStoreRecordVector(2, DistanceFunction = DistanceFunction.CosineSimilarity)] + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction)] public ReadOnlyMemory Vector { get; set; } = new ReadOnlyMemory([0, 0]); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs index 757a92bcc14..65b520980c1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -5,8 +5,8 @@ namespace aichatweb.Services.Ingestion; public class DataIngestor( ILogger logger, - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection chunksCollection, + VectorStoreCollection documentsCollection) { public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source) { @@ -17,10 +17,8 @@ public static async Task IngestDataAsync(IServiceProvider services, IIngestionSo public async Task IngestDataAsync(IIngestionSource source) { - var chunksCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - var documentsCollection = vectorStore.GetCollection("data-aichatweb-documents"); - await chunksCollection.CreateCollectionIfNotExistsAsync(); - await documentsCollection.CreateCollectionIfNotExistsAsync(); + await chunksCollection.EnsureCollectionExistsAsync(); + await documentsCollection.EnsureCollectionExistsAsync(); var sourceId = source.SourceId; var documentsForSource = await documentsCollection.GetAsync(doc => doc.SourceId == sourceId, top: int.MaxValue).ToListAsync(); @@ -41,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) await documentsCollection.UpsertAsync(modifiedDocument); - var newRecords = await source.CreateChunksForDocumentAsync(embeddingGenerator, modifiedDocument); + var newRecords = await source.CreateChunksForDocumentAsync(modifiedDocument); await chunksCollection.UpsertAsync(newRecords); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs index 89fdc81ada6..540cac117e7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/IIngestionSource.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.AI; - -namespace aichatweb.Services.Ingestion; +namespace aichatweb.Services.Ingestion; public interface IIngestionSource { @@ -10,5 +8,5 @@ public interface IIngestionSource Task> GetDeletedDocumentsAsync(IReadOnlyList existingDocuments); - Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document); + Task> CreateChunksForDocumentAsync(IngestedDocument document); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs index 39a079e76fe..0be02a9d008 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/PDFDirectorySource.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.Text; +using Microsoft.SemanticKernel.Text; using UglyToad.PdfPig; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; @@ -42,21 +41,18 @@ public Task> GetDeletedDocumentsAsync(IReadOnlyLis return Task.FromResult(deletedDocuments); } - public async Task> CreateChunksForDocumentAsync(IEmbeddingGenerator> embeddingGenerator, IngestedDocument document) + public Task> CreateChunksForDocumentAsync(IngestedDocument document) { using var pdf = PdfDocument.Open(Path.Combine(sourceDirectory, document.DocumentId)); var paragraphs = pdf.GetPages().SelectMany(GetPageParagraphs).ToList(); - var embeddings = await embeddingGenerator.GenerateAsync(paragraphs.Select(c => c.Text)); - - return paragraphs.Zip(embeddings).Select(pair => new IngestedChunk + return Task.FromResult(paragraphs.Select(p => new IngestedChunk { Key = Guid.CreateVersion7().ToString(), DocumentId = document.DocumentId, - PageNumber = pair.First.PageNumber, - Text = pair.First.Text, - Vector = pair.Second.Vector, - }); + PageNumber = p.PageNumber, + Text = p.Text, + })); } private static IEnumerable<(int PageNumber, int IndexOnPage, string Text)> GetPageParagraphs(Page pdfPage) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs index c380a69ce25..291c6c4b4a9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/SemanticSearch.cs @@ -1,18 +1,13 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.VectorData; namespace aichatweb.Services; public class SemanticSearch( - IEmbeddingGenerator> embeddingGenerator, - IVectorStore vectorStore) + VectorStoreCollection vectorCollection) { public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) { - var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(text); - var vectorCollection = vectorStore.GetCollection("data-aichatweb-chunks"); - - var nearest = vectorCollection.SearchEmbeddingAsync(queryEmbedding, maxResults, new VectorSearchOptions + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions { Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 15a4bc8be9e..d20fb2cac98 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -11,11 +11,11 @@ - + - + From b598097bc8b5e71726be971a85fb73292ae370b1 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 20 May 2025 13:10:12 -0700 Subject: [PATCH 125/472] Update Microsoft.SemanticKernel to 1.53.0 (#6470) --- src/ProjectTemplates/GeneratedContent.targets | 4 ++-- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 4 ++-- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb/aichatweb.csproj | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 5561a5214d1..6c0ade38a7d 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,8 +35,8 @@ 11.6.0 9.4.1-beta.277 9.2.0 - 1.52.1 - 1.52.1-preview + 1.53.0 + 1.53.0-preview 5.1.16 1.9.0 0.1.10 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index e1739390adc..27b780d09e6 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 4fdb7120891..8af87627043 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 189b183a946..c20c1b2d991 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 8fe4a7464d4..aabee1f2ebc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index d20fb2cac98..ba3c02f2321 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -11,11 +11,11 @@ - + - + From 83a203c50ba1105de911e96dcf0e0ae29e99d807 Mon Sep 17 00:00:00 2001 From: Tim Potze Date: Tue, 20 May 2025 23:47:05 +0200 Subject: [PATCH 126/472] Fix error in json in README.md (#9415) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 71547ea9396..b767bb41e83 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -189,7 +189,7 @@ With the configuration-based endpoint provider, named endpoints can be specified ```json { "Services": { - "basket": + "basket": { "https": "https://10.2.3.4:8080", /* the https endpoint, requested via https://basket */ "dashboard": "https://10.2.3.4:9999" /* the "dashboard" endpoint, requested via https://_dashboard.basket */ } From 4d330032b0e2e4f9b4c1747a0e824df680759102 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 20 May 2025 15:43:46 -0700 Subject: [PATCH 127/472] Update template dependencies (#6471) --- src/ProjectTemplates/GeneratedContent.targets | 14 +++++++------- .../aichatweb.AppHost/aichatweb.AppHost.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 12 ++++++------ .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.AppHost/aichatweb.AppHost.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 12 ++++++------ .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 2 +- .../aichatweb.AppHost/aichatweb.AppHost.csproj | 8 ++++---- .../aichatweb.ServiceDefaults.csproj | 12 ++++++------ .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 6 +++--- 10 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 6c0ade38a7d..c3287408034 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -27,18 +27,18 @@ - 9.2.1 - 9.2.1-preview.1.25222.1 + 9.3.0 + 9.3.0-preview.1.25265.20 2.2.0-beta.4 - 1.0.0-beta.6 + 1.0.0-beta.9 1.14.0 11.6.0 - 9.4.1-beta.277 - 9.2.0 + 9.4.1-beta.291 + 9.3.0 1.53.0 1.53.0-preview - 5.1.16 - 1.9.0 + 5.1.18 + 1.12.0 0.1.10 6.0.1 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 1eb887382c9..939114cbd3d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 74656777aaf..d05035b1315 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 27b780d09e6..19ba7c1bd4a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,13 +8,13 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 1eb887382c9..939114cbd3d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 74656777aaf..d05035b1315 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index c20c1b2d991..c3b4cf6f3a9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index a9a9fc626a9..193ed5f529a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,9 +12,9 @@ - - - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 74656777aaf..d05035b1315 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index aabee1f2ebc..cfd8b0a0372 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,13 +8,13 @@ - - + + - + From 85678c05d64cb6cb35360288a50937081eadc3e1 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Wed, 21 May 2025 05:57:21 -0700 Subject: [PATCH 128/472] Exclude provider URI from cache key computation by default (#6473) Fixes #6468 --- .../Storage/AzureStorageReportingConfiguration.cs | 7 +++++++ .../CSharp/ReportingConfiguration.cs | 6 ------ .../CSharp/Storage/DiskBasedReportingConfiguration.cs | 7 +++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs index fafd8639b34..3131fc1c5d9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs @@ -59,6 +59,13 @@ public static class AzureStorageReportingConfiguration /// A that persists s to Azure Storage /// and also uses Azure Storage to cache AI responses. /// + /// + /// Note that when is set to , the cache keys used + /// for the cached responses are not guaranteed to be stable across releases of the library. In other words, when + /// you update your code to reference a newer version of the library, it is possible that old cached responses + /// (persisted to the cache using older versions of the library) will no longer be used - instead new responses + /// will be fetched from the LLM and added to the cache for use in subsequent executions. + /// #pragma warning disable S107 // Methods should not have too many parameters public static ReportingConfiguration Create( DataLakeDirectoryClient client, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs index 130586de930..2f6613bbcbc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs @@ -274,12 +274,6 @@ private static IEnumerable GetCachingKeysForChatClient(IChatClient chatC yield return providerName!; } - Uri? providerUri = metadata?.ProviderUri; - if (providerUri is not null) - { - yield return providerUri.AbsoluteUri; - } - string? modelId = metadata?.DefaultModelId; if (!string.IsNullOrWhiteSpace(modelId)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs index e967fdd1db9..10350446229 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs @@ -59,6 +59,13 @@ public static class DiskBasedReportingConfiguration /// A that persists s to disk and also uses the /// disk to cache AI responses. /// + /// + /// Note that when is set to , the cache keys used + /// for the cached responses are not guaranteed to be stable across releases of the library. In other words, when + /// you update your code to reference a newer version of the library, it is possible that old cached responses + /// (persisted to the cache using older versions of the library) will no longer be used - instead new responses + /// will be fetched from the LLM and added to the cache for use in subsequent executions. + /// #pragma warning disable S107 // Methods should not have too many parameters public static ReportingConfiguration Create( string storageRootPath, From 6f29ab505709370fc9b72bf4bb7e004a7662f69f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 21 May 2025 17:47:44 +0300 Subject: [PATCH 129/472] Make hashing stable w.r.t. indentation settings and property ordering. (#6476) * Make hashing stable w.r.t. indentation settings and property ordering. * Add indentation invariance test --- .../Utilities/AIJsonUtilities.cs | 35 +++++++++++++++++-- .../DistributedCachingChatClient.cs | 2 +- .../Utilities/AIJsonUtilitiesTests.cs | 17 +++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index 51459193923..7e28a5983ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -6,8 +6,10 @@ using System.Diagnostics; #endif using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; #if NET using System.Threading; @@ -112,7 +114,9 @@ public static string HashDataToString(ReadOnlySpan values, JsonSerializ { foreach (object? value in values) { - JsonSerializer.Serialize(stream, value, jti); + JsonNode? jsonNode = JsonSerializer.SerializeToNode(value, jti); + NormalizeJsonNode(jsonNode); + JsonSerializer.Serialize(stream, jsonNode, JsonContextNoIndentation.Default.JsonNode!); } stream.GetHashAndReset(hashData); @@ -130,7 +134,9 @@ public static string HashDataToString(ReadOnlySpan values, JsonSerializ MemoryStream stream = new(); foreach (object? value in values) { - JsonSerializer.Serialize(stream, value, jti); + JsonNode? jsonNode = JsonSerializer.SerializeToNode(value, jti); + NormalizeJsonNode(jsonNode); + JsonSerializer.Serialize(stream, jsonNode, JsonContextNoIndentation.Default.JsonNode!); } using var hashAlgorithm = SHA384.Create(); @@ -156,6 +162,31 @@ static string ConvertToHexString(ReadOnlySpan hashData) return new string(chars); } #endif + static void NormalizeJsonNode(JsonNode? node) + { + switch (node) + { + case JsonArray array: + foreach (JsonNode? item in array) + { + NormalizeJsonNode(item); + } + + break; + + case JsonObject obj: + var entries = obj.OrderBy(e => e.Key, StringComparer.Ordinal).ToArray(); + obj.Clear(); + + foreach (var entry in entries) + { + obj.Add(entry.Key, entry.Value); + NormalizeJsonNode(entry.Value); + } + + break; + } + } } private static void AddAIContentTypeCore(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs index dc8fe9db56f..9fb586f5b79 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs @@ -123,7 +123,7 @@ protected override async Task WriteCacheStreamingAsync(string key, IReadOnlyList protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) { // Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way. - const int CacheVersion = 1; + const int CacheVersion = 2; return AIJsonUtilities.HashDataToString([CacheVersion, messages, options, .. additionalValues], _jsonSerializerOptions); } 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 20dfeb62f5c..78db93e9380 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -557,13 +557,30 @@ public static void HashData_Idempotent() string key2 = AIJsonUtilities.HashDataToString(["a", 'b', 42], options); string key3 = AIJsonUtilities.HashDataToString([TimeSpan.FromSeconds(1), null, 1.23], options); string key4 = AIJsonUtilities.HashDataToString([TimeSpan.FromSeconds(1), null, 1.23], options); + string key5 = AIJsonUtilities.HashDataToString([new Dictionary { ["key1"] = 1, ["key2"] = 2 }], options); + string key6 = AIJsonUtilities.HashDataToString([new Dictionary { ["key2"] = 2, ["key1"] = 1 }], options); Assert.Equal(key1, key2); Assert.Equal(key3, key4); + Assert.Equal(key5, key6); Assert.NotEqual(key1, key3); + Assert.NotEqual(key1, key5); } } + [Fact] + public static void HashData_IndentationInvariant() + { + JsonSerializerOptions indentOptions = new(AIJsonUtilities.DefaultOptions) { WriteIndented = true }; + JsonSerializerOptions noIndentOptions = new(AIJsonUtilities.DefaultOptions) { WriteIndented = false }; + + Dictionary dict = new() { ["key1"] = 1, ["key2"] = 2 }; + string key1 = AIJsonUtilities.HashDataToString([dict], indentOptions); + string key2 = AIJsonUtilities.HashDataToString([dict], noIndentOptions); + + Assert.Equal(key1, key2); + } + [Fact] public static void CreateFunctionJsonSchema_InvokesIncludeParameterCallbackForEveryParameter() { From 496cc2fb2172ec9a9765b028fa519f566ac07883 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 05:46:14 +0000 Subject: [PATCH 130/472] Update dependencies from https://github.com/dotnet/arcade build 20250521.1 (#6481) [main] Update dependencies from dotnet/arcade --- NuGet.config | 46 ----------------------------------------- eng/Version.Details.xml | 12 +++++------ eng/Versions.props | 2 +- global.json | 4 ++-- 4 files changed, 9 insertions(+), 55 deletions(-) diff --git a/NuGet.config b/NuGet.config index 1b80afbe11f..0fedd015e82 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,33 +4,10 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -66,33 +43,10 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 58258276208..ac0208aebcd 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 + 086a1771875b63404b4a710d27250fe384dc2810 - + https://github.com/dotnet/arcade - c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 + 086a1771875b63404b4a710d27250fe384dc2810 - + https://github.com/dotnet/arcade - c62eeb5b5432f9eaa034fbd641ccd9fd0d928fb3 + 086a1771875b63404b4a710d27250fe384dc2810 diff --git a/eng/Versions.props b/eng/Versions.props index bafe6b74b17..483b6366af6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,7 +84,7 @@ 9.0.5 - 9.0.0-beta.25266.2 + 9.0.0-beta.25271.1 diff --git a/global.json b/global.json index 694e686a6b7..78b9a9c3e65 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25266.2", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25266.2" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25271.1", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25271.1" } } From 901e3668794ba3349bcbbefa8b119496db07f90f Mon Sep 17 00:00:00 2001 From: BowenYang666 <48521609+BowenYang666@users.noreply.github.com> Date: Tue, 27 May 2025 17:50:09 +0800 Subject: [PATCH 131/472] Add default value for namedArg in Microsoft.Gen.Metrics.parser (#6238) Co-authored-by: Bowen Yang (from Dev Box) --- src/Generators/Microsoft.Gen.Metrics/Parser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Generators/Microsoft.Gen.Metrics/Parser.cs b/src/Generators/Microsoft.Gen.Metrics/Parser.cs index da05c6a51ec..c0c0d6481e1 100644 --- a/src/Generators/Microsoft.Gen.Metrics/Parser.cs +++ b/src/Generators/Microsoft.Gen.Metrics/Parser.cs @@ -422,7 +422,7 @@ private void GetTagDescription( if (!methodAttribute.ConstructorArguments.IsDefaultOrEmpty && methodAttribute.ConstructorArguments[0].Kind == TypedConstantKind.Type) { - KeyValuePair namedArg; + KeyValuePair namedArg = default; var ctorArg = methodAttribute.ConstructorArguments[0]; if (!methodAttribute.NamedArguments.IsDefaultOrEmpty) From c01d681ecea4641a17832bd2003106037dd9a859 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 29 May 2025 03:09:18 -0400 Subject: [PATCH 132/472] Update OpenTelemetryChatClient to 1.34 (#6466) --- .../ChatCompletion/OpenTelemetryChatClient.cs | 7 ++++++- .../Embeddings/OpenTelemetryEmbeddingGenerator.cs | 2 +- .../Microsoft.Extensions.AI/OpenTelemetryConsts.cs | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index c22dc292c8a..98e231cf0e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.33, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.34, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed partial class OpenTelemetryChatClient : DelegatingChatClient @@ -247,6 +247,11 @@ public override async IAsyncEnumerable GetStreamingResponseA if (options is not null) { + if (options.ConversationId is string conversationId) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Conversation.Id, conversationId); + } + if (options.FrequencyPenalty is float frequencyPenalty) { _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.FrequencyPenalty, frequencyPenalty); diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 99a3ed684af..7bac1b716f5 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.33, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.34, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// /// The type of input used to produce embeddings. diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index c1a22066227..7b15f2fc05c 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -54,6 +54,11 @@ public static class TokenUsage } } + public static class Conversation + { + public const string Id = "gen_ai.conversation.id"; + } + public static class Operation { public const string Name = "gen_ai.operation.name"; From 2ab21ec6d6fa7371f19d8485215d4c0c99f9c338 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 29 May 2025 03:09:40 -0400 Subject: [PATCH 133/472] Delete M.E.AI changelog files (#6467) These were in place to help track the M.E.AI-specific changes while the abstractions were preview. Now that they're stable, we'll just use the repo-wide changelog mechanism. --- .../CHANGELOG.md | 102 ------------------ .../CHANGELOG.md | 48 --------- .../CHANGELOG.md | 40 ------- .../CHANGELOG.md | 56 ---------- .../Microsoft.Extensions.AI/CHANGELOG.md | 98 ----------------- 5 files changed, 344 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md delete mode 100644 src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md delete mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md delete mode 100644 src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md deleted file mode 100644 index b4fe9d69a66..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ /dev/null @@ -1,102 +0,0 @@ -# Release History - -## 9.4.4-preview.1.25259.16 - -- Added `AIJsonUtilities.TransformSchema` and supporting types. -- Added `BinaryEmbedding` for bit embeddings. -- Added `ChatOptions.RawRepresentationFactory` to make it easier to pass options to the underlying service. -- Added `Base64Data` property to `DataContent`. -- Moved `AIFunctionFactory` to `Microsoft.Extensions.AI.Abstractions`. -- Fixed `AIFunctionFactory` handling of default struct arguments. - -## 9.4.3-preview.1.25230.7 - -- Renamed `ChatThreadId` to `ConversationId` on `ChatResponse`, `ChatResponseUpdate`, and `ChatOptions`. -- Renamed `EmbeddingGeneratorExtensions` method `GenerateEmbeddingAsync` to `GenerateAsync` and `GenerateEmbeddingVectorAsync` to `GenerateVectorAsync`. -- Made `AIContent`'s constructor `public` instead of `protected`. -- Fixed `AIJsonUtilities.CreateJsonSchema` to tolerate `JsonSerializerOptions` instances that don't have a `TypeInfoResolver` already configured. - -## 9.4.0-preview.1.25207.5 - -- Added `ErrorContent` and `TextReasoningContent`. -- Added `MessageId` to `ChatMessage` and `ChatResponseUpdate`. -- Added `AIFunctionArguments`, changing `AIFunction.InvokeAsync` to accept one and to return a `ValueTask`. -- Updated `AIJsonUtilities`'s schema generation to not use `default` when `RequireAllProperties` is set to `true`. -- Added `ISpeechToTextClient` and supporting types. -- Fixed several issues related to Native AOT support. - -## 9.3.0-preview.1.25161.3 - -- Changed `IChatClient.GetResponseAsync` and `IChatClient.GetStreamingResponseAsync` to accept an `IEnumerable` rather than an `IList`. It is no longer mutated by implementations. -- Removed `ChatResponse.Choice` and `ChatResponseUpdate.ChoiceIndex`. -- Replaced `ChatResponse.Message` with `ChatResponse.Messages`. Responses now carry with them all messages generated as part of the operation, rather than all but the last being added to the history and the last returned. -- Added `GetRequiredService` extension method for `IChatClient`/`IEmbeddingGenerator`. -- Added non-generic `IEmbeddingGenerator` interface, which is inherited by `IEmbeddingGenerator`. The `GetService` method moves down to the non-generic interface, and the `GetService`/`GetRequiredService` extension methods are now in terms of the non-generic. -- `AIJsonUtilities.CreateFunctionJsonSchema` now special-cases `CancellationToken` to not include it in the schema. -- Improved the debugger displays for `ChatMessage` and the `AIContent` types. -- Added a static `AIJsonUtilities.HashDataToString` method. -- Split `DataContent`, which handled both in-memory data and URIs to remote data, into `DataContent` (for the former) and `UriContent` (for the latter). -- Renamed `DataContent.MediaTypeStartsWith` to `DataContent.HasTopLevelMediaType`, and changed semantics accordingly. - -## 9.3.0-preview.1.25114.11 - -- Renamed `IChatClient.Complete{Streaming}Async` to `IChatClient.Get{Streaming}ResponseAsync`. This is to avoid confusion with "Complete" being about stopping an operation, as well as to avoid tying the methods to a particular implementation detail of how responses are generated. Along with this, renamed `ChatCompletion` to `ChatResponse`, `StreamingChatCompletionUpdate` to `ChatResponseUpdate`, `CompletionId` to `ResponseId`, `ToStreamingChatCompletionUpdates` to `ToChatResponseUpdates`, and `ToChatCompletion{Async}` to `ToChatResponse{Async}`. -- Removed `IChatClient.Metadata` and `IEmbeddingGenerator.Metadata`. The `GetService` method may be used to retrieve `ChatClientMetadata` and `EmbeddingGeneratorMetadata`, respectively. -- Added overloads of `Get{Streaming}ResponseAsync` that accept a single `ChatMessage` (in addition to the other overloads that accept a `List` or a `string`). -- Added `ChatThreadId` properties to `ChatOptions`, `ChatResponse`, and `ChatResponseUpdate`. `IChatClient` can now be used in both stateful and stateless modes of operation, such as with agents that maintain server-side chat history. -- Made `ChatOptions.ToolMode` nullable and added a `None` option. -- Changed `UsageDetails`'s properties from `int?` to `long?`. -- Removed `DataContent.ContainsData`; `DataContent.Data.HasValue` may be used instead. -- Removed `ImageContent` and `AudioContent`; the base `DataContent` should now be used instead, with a new `DataContent.MediaTypeStartsWith` helper for routing based on media type. -- Removed setters on `FunctionCallContent` and `FunctionResultContent` properties where the value is supplied to the constructor. -- Removed `FunctionResultContent.Name`. -- Augmented the base `AITool` with `Name`, `Description`, and `AdditionalProperties` virtual properties. -- Added a `CodeInterpreterTool` for use with services that support server-side code execution. -- Changed `AIFunction`'s schema representation to be for the whole function rather than per parameter, and exposed corresponding methods on `AIJsonUtilities`, e.g. `CreateFunctionJsonSchema`. -- Removed `AIFunctionParameterMetadata` and `AIFunctionReturnParameterMetadata` classes and corresponding properties on `AIFunction` and `AIFunctionFactoryCreateOptions`, replacing them with a `MethodInfo?`. All relevant metadata, such as the JSON schema for the function, are moved to properties directly on `AIFunction`. -- Renamed `AIFunctionFactoryCreateOptions` to `AIFunctionFactoryOptions` and made all its properties nullable. -- Changed `AIJsonUtilities.DefaultOptions` to use relaxed JSON escaping. -- Made `IEmbeddingGenerator` contravariant on `TInput`. - -## 9.1.0-preview.1.25064.3 - -- Added `AdditionalPropertiesDictionary` and changed `UsageDetails.AdditionalProperties` to be named `AdditionalCounts` and to be of type `AdditionalPropertiesDictionary`. -- Updated `FunctionCallingChatClient` to sum all `UsageDetails` token counts from all intermediate messages. -- Fixed JSON schema generation for floating-point types. -- Added `AddAIContentType` for enabling custom `AIContent`-derived types to participate in polymorphic serialization. - -## 9.0.1-preview.1.24570.5 - -- Changed `IChatClient`/`IEmbeddingGenerator`.`GetService` to be non-generic. -- Added `ToChatCompletion` / `ToChatCompletionUpdate` extension methods for `IEnumerable` / `IAsyncEnumerable`, respectively. -- Added `ToStreamingChatCompletionUpdates` instance method to `ChatCompletion`. -- Added `IncludeTypeInEnumSchemas`, `DisallowAdditionalProperties`, `RequireAllProperties`, and `TransformSchemaNode` options to `AIJsonSchemaCreateOptions`. -- Fixed a Native AOT warning in `AIFunctionFactory.Create`. -- Fixed a bug in `AIJsonUtilities` in the handling of Boolean schemas. -- Improved the `ToString` override of `ChatMessage` and `StreamingChatCompletionUpdate` to include all `TextContent`, and of `ChatCompletion` to include all choices. -- Added `DebuggerDisplay` attributes to `DataContent` and `GeneratedEmbeddings`. -- Improved the documentation. - -## 9.0.0-preview.9.24556.5 - -- Added a strongly-typed `ChatOptions.Seed` property. -- Improved `AdditionalPropertiesDictionary` with a `TryAdd` method, a strongly-typed `Enumerator`, and debugger-related attributes for improved debuggability. -- Fixed `AIJsonUtilities` schema generation for Boolean schemas. - -## 9.0.0-preview.9.24525.1 - -- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. -- Annotated `FunctionCallContent.Exception` and `FunctionResultContent.Exception` as `[JsonIgnore]`, such that they're ignored when serializing instances with `JsonSerializer`. The corresponding constructors accepting an `Exception` were removed. -- Annotated `ChatCompletion.Message` as `[JsonIgnore]`, such that it's ignored when serializing instances with `JsonSerializer`. -- Added the `FunctionCallContent.CreateFromParsedArguments` method. -- Added the `AdditionalPropertiesDictionary.TryGetValue` method. -- Added the `StreamingChatCompletionUpdate.ModelId` property and removed the `AIContent.ModelId` property. -- Renamed the `GenerateAsync` extension method on `IEmbeddingGenerator<,>` to `GenerateEmbeddingsAsync` and updated it to return `Embedding` rather than `GeneratedEmbeddings`. -- Added `GenerateAndZipAsync` and `GenerateEmbeddingVectorAsync` extension methods for `IEmbeddingGenerator<,>`. -- Added the `EmbeddingGeneratorOptions.Dimensions` property. -- Added the `ChatOptions.TopK` property. -- Normalized `null` inputs in `TextContent` to be empty strings. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md deleted file mode 100644 index aeb023efae5..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ /dev/null @@ -1,48 +0,0 @@ -# Release History - -## 9.4.4-preview.1.25259.16 - -- Added an `AsIEmbeddingGenerator` extension method for `ImageEmbeddingsClient`. -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.3-preview.1.25230.7 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.0-preview.1.25207.5 - -- Updated to Azure.AI.Inference 1.0.0-beta.4. -- Renamed `AsChatClient`/`AsEmbeddingGenerator` extension methods to `AsIChatClient`/`AsIEmbeddingGenerator`. -- Removed the public `AzureAIInferenceChatClient`/`AzureAIInferenceEmbeddingGenerator` types. These are only created now via the extension methods. -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25161.3 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25114.11 - -- Updated to use Azure.AI.Inference 1.0.0-beta.3, adding support for structured output and audio input. - -## 9.1.0-preview.1.25064.3 - -- Fixed handling of text-only user messages. - -## 9.0.1-preview.1.24570.5 - - - Made the `ToolCallJsonSerializerOptions` property non-nullable. - -## 9.0.0-preview.9.24556.5 - -- Fixed `AzureAIInferenceEmbeddingGenerator` to respect `EmbeddingGenerationOptions.Dimensions`. - -## 9.0.0-preview.9.24525.1 - -- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. -- Updated to use Azure.AI.Inference 1.0.0-beta.2. -- Added `AzureAIInferenceEmbeddingGenerator` and corresponding `AsEmbeddingGenerator` extension method. -- Improved handling of assistant messages that include both text and function call content. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md deleted file mode 100644 index e90fed2cdba..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/CHANGELOG.md +++ /dev/null @@ -1,40 +0,0 @@ -# Release History - -## 9.4.4-preview.1.25259.16 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.3-preview.1.25230.7 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.0-preview.1.25207.5 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25161.3 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25114.11 - -- Ensures that all yielded `ChatResponseUpdates` include a `ResponseId`. -- Ensures that error HTTP status codes are correctly propagated as exceptions. - -## 9.1.0-preview.1.25064.3 - -- Added support for function calling when doing streaming operations. -- Added support for native structured output. - -## 9.0.1-preview.1.24570.5 - -- Made the `ToolCallJsonSerializerOptions` property non-nullable. - -## 9.0.0-preview.9.24525.1 - -- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. -- Added additional constructors to `OllamaChatClient` and `OllamaEmbeddingGenerator` that accept `string` endpoints, in addition to the existing ones accepting `Uri` endpoints. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md deleted file mode 100644 index ad915d06aa7..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# Release History - -## 9.4.4-preview.1.25259.16 - -- Made `IChatClient` implementation more resilient with non-OpenAI services. -- Added `ErrorContent` to represent refusals. -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.3-preview.1.25230.7 - -- Reverted previous change that enabled `strict` schemas by default. -- Updated `IChatClient` implementations to support `DataContent`s for PDFs. -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.0-preview.1.25207.5 - -- Updated to OpenAI 2.2.0-beta-4. -- Added `AsISpeechToTextClient` extension method for `AudioClient`. -- Removed `AsChatClient(OpenAIClient)`/`AsEmbeddingGenerator(OpenAIClient)` extension methods, renamed the remaining `AsChatClient` methods to `AsIChatClient`, renamed the remaining `AsEmbeddingGenerator` methods to `AsIEmbeddingGenerator`, and added an `AsIChatClient` for `OpenAIResponseClient`. -- Removed the public `OpenAIChatClient`/`OpenAIEmbeddingGenerator` types. These are only created now via the extension methods. -- Removed serialization/deserialization helpers. -- Updated to support pulling propagating image detail from `AdditionalProperties`. -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25161.3 - -- Updated to accomodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25114.11 - -- Updated to depend on OpenAI 2.2.0-beta.1, updating with support for the Developer role, audio input and output, and additional options like output prediction. It is now also compatible with NativeAOT. -- Added an `AsChatClient` extension method for OpenAI's `AssistantClient`, enabling `IChatClient` to be used with OpenAI Assistants. -- Improved the OpenAI serialization helpers, including a custom converter for the `OpenAIChatCompletionRequest` envelope type. - -## 9.1.0-preview.1.25064.3 - -- Updated to depend on OpenAI 2.1.0. -- Updated to propagate `Metadata` and `StoredOutputEnabled` from `ChatOptions.AdditionalProperties`. -- Added serialization helpers methods for deserializing OpenAI compatible JSON into the Microsoft.Extensions.AI object model, and vice versa serializing the Microsoft.Extensions.AI object model into OpenAI compatible JSON. - -## 9.0.1-preview.1.24570.5 - - - Upgraded to depend on the 2.1.0-beta.2 version of the OpenAI NuGet package. - - Added the `OpenAIRealtimeExtensions` class, with `ToConversationFunctionTool` and `HandleToolCallsAsync` extension methods for using `AIFunction` with the OpenAI Realtime API. - - Made the `ToolCallJsonSerializerOptions` property non-nullable. - -## 9.0.0-preview.9.24525.1 - -- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. -- Improved handling of system messages that include multiple content items. -- Improved handling of assistant messages that include both text and function call content. -- Fixed handling of streaming updates containing empty payloads. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md deleted file mode 100644 index 25c15aed0d2..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ /dev/null @@ -1,98 +0,0 @@ -# Release History - -## 9.4.4-preview.1.25259.16 - -- Fixed `CachingChatClient` to avoid caching when `ConversationId` is set. -- Renamed `useJsonSchema` parameter in `GetResponseAsync` to `useJsonSchemaResponseFormat`. -- Updated `OpenTelemetryChatClient` and `OpenTelemetryEmbeddingGenerator` to conform to the latest 1.33.0 draft specification of the Semantic Conventions for Generative AI systems. - -## 9.4.3-preview.1.25230.7 - -- Updated the diagnostic spans emitted by `FunctionInvokingChatClient` to include total input and output token counts. -- Updated `AIFunctionFactory` to recognize `[FromKeyedServices]` attribute on parameters in order to resolve those parameters from the `IServiceProvider`. -- Added `AIFunctionFactoryOptions.Services`, and used it with `IServiceProviderIsService` to automatically resolve `IServiceProvider`-based parameters in `AIFunction` methods. -- Added `ChatOptions.AllowMultipleToolCalls`. -- Changed `AIJsonSchemaCreateOptions.RequireAllProperties` to default to `false` instead of `true`. -- Unsealed `AIFunctionArguments`. -- Added `AIFunctionArguments` constructors accepting `IEqualityComparer` arguments. -- Unsealed `FunctionInvocationContext`. -- Added `FunctionInvocationContext.IsStreaming`. -- Added protected `FunctionInvokingChatClient.FunctionInvocationServices` property to surface the corresponding `IServiceProvider` provided at construction time. -- Changed protected virtual `FunctionInvokingChatClient.InvokeFunctionAsync` to return `ValueTask` instead of `Task`. Diagnostics are now emitted even if the method is overridden. -- Added `FunctionInvocationResult.Terminate`. - -## 9.4.0-preview.1.25207.5 - -- Updated `GetResponseAsync` to default to using JSON-schema based structured output by default. -- Updated `AIFunctionFactory` with support for customizable marshaling of function parameters and return values. -- Updated `AIFunctionFactory` with a new `Create` overload that supports creating a new receiver instance on each invocation, with either Activator.CreateInstance or ActivatorUtilities.CreateInstance. -- Updated `AIFunctionFactory` to support injecting an `IServiceProvider` as an argument, sourced from the `AIFunctionArguments` passed to `AIFunction.InvokeAsync`. -- Simplified `FunctionInvokingChatClient` error handling, removing `RetryOnError` and replacing it with `MaximumConsecutiveErrorsPerRequest`. -- `FunctionInvokingChatClient` will now ensure that it invokes `AIFunction`s in the same `SynchronizationContext` that `GetResponseAsync`/`GetStreamingResponseAsync` was called in. -- `OpenTelemetryChatClient` now considers `AdditionalProperties` to be sensitive and will only use that data as tags when `EnableSensitiveData` is set to `true`. -- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.32 draft specification of the Semantic Conventions for Generative AI systems. -- Updated the key used by `DistributedCachingChatClient` to employ SHA384 instead of SHA256. -- Lowered `System.Text.Json` 9.x dependency to 8.x when targeting `net8.0` or older. - -## 9.3.0-preview.1.25161.3 - -- Added caching to `AIFunctionFactory.Create` to improve performance of creating the same functions repeatedly. As part of this, `AIJsonSchemaCreateOptions` now implements `IEquatable`. -- Removed the public `AnonymousDelegatingChatClient`/`AnonymousDelegatingEmbeddingGenerator`. Their functionality is still available via the `Use` methods on the builders. -- Changed those `Use` methods to use `Func<...>` rather than a custom delegate type. -- `AIFunctionFactory.Create` now supports `CancellationToken` parameters, and the `AIFunctionContext` type that had served to enable that has been removed. -- Made `FunctionInvokingChatClient.CurrentContext`'s setter `protected`. -- Renamed `FunctionInvokingChatClient.DetailedErrors` to `IncludeDetailedErrors`. -- Renamed `FunctionInvokingChatClient.ConcurrentInvocation` to `AllowConcurrentInvocation`. -- Removed `FunctionInvokingChatClient.KeepFunctionCallingContent`, as it's no longer relevant now that the input messages are an `IEnumerable` rather than an `IList`. -- Renamed `FunctionStatus` to `FunctionInvocationStatus`. -- Renamed `FunctionInvocationStatus.Failed` to `FunctionInvocationStatus.Exception`. -- Moved the nested `FunctionInvocationContext` type to be a peer of `FunctionInvokingChatClient` rather than nested within it. -- Made the `serviceKey` parameters to `AddKeyedChatClient`/`AddKeyedEmbeddingGenerator` nullable. -- Improved `FunctionInvokingChatClient.GetStreamingResponseAsync` to send back to the inner client all content received until that point, and to stream back to the caller messages it generates (e.g. tool responses). -- Improved `AddEmbeddingGenerator` and `AddKeyedEmbeddingGenerator` to register for both the generic and non-generic interfaces. - -## 9.3.0-preview.1.25114.11 - -- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.30.0 draft specification of the Semantic Conventions for Generative AI systems. - -## 9.1.0-preview.1.25064.3 - -- Added `FunctionInvokingChatClient.CurrentContext` to give functions access to detailed function invocation information. -- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.29.0 draft specification of the Semantic Conventions for Generative AI systems. -- Updated `FunctionInvokingChatClient` to emit an `Activity`/span around all interactions related to a single chat operation. - -## 9.0.1-preview.1.24570.5 - -- Moved the `AddChatClient`, `AddKeyedChatClient`, `AddEmbeddingGenerator`, and `AddKeyedEmbeddingGenerator` extension methods to the `Microsoft.Extensions.DependencyInjection` namespace, changed them to register singleton instances instead of scoped instances, and changed them to support lambda-less chaining. -- Renamed `UseChatOptions`/`UseEmbeddingOptions` to `ConfigureOptions`, and changed the behavior to always invoke the delegate with a safely-mutable instance, either a new instance if the caller provided null, or a clone of the provided instance. -- Renamed the final `Use` method for building a builder to be named `Build`. The inner client instance is passed to the constructor and the `IServiceProvider` is optionally passed to the `Build` method. -- Added `AsBuilder` extension methods to `IChatClient`/`IEmbeddingGenerator` to create builders from the instances. -- Changed the `CachingChatClient`/`CachingEmbeddingGenerator`.`GetCacheKey` method to accept a `params ReadOnlySpan`, included the `ChatOptions`/`EmbeddingGeneratorOptions` as part of the caching key, and reduced memory allocation. -- Added support for anonymous delegating `IChatClient`/`IEmbeddingGenerator` implementations, with `Use` methods on `ChatClientBuilder`/`EmbeddingGeneratorBuilder` that enable the implementations of the core methods to be supplied as lambdas. -- Changed `UseLogging` to accept an `ILoggerFactory` rather than `ILogger`. -- Reversed the order of the `IChatClient`/`IEmbeddingGenerator` and `IServiceProvider` arguments to used by one of the `Use` overloads. -- Added logging capabilities to `FunctionInvokingChatClient`. `UseFunctionInvocation` now accepts an optional `ILoggerFactory`. -- Fixed the `FunctionInvokingChatClient` to include usage data for non-streaming completions in the augmented history. -- Fixed the `FunctionInvokingChatClient` streaming support to appropriately fail for multi-choice completions. -- Fixed the `FunctionInvokingChatClient` to stop yielding function calling content that was already being handled. -- Improved the documentation. - -## 9.0.0-preview.9.24556.5 - -- Added `UseEmbeddingGenerationOptions` and corresponding `ConfigureOptionsEmbeddingGenerator`. - -## 9.0.0-preview.9.24525.1 - -- Added new `AIJsonUtilities` and `AIJsonSchemaCreateOptions` classes. -- Made `AIFunctionFactory.Create` safe for use with Native AOT. -- Simplified the set of `AIFunctionFactory.Create` overloads. -- Changed the default for `FunctionInvokingChatClient.ConcurrentInvocation` from `true` to `false`. -- Improved the readability of JSON generated as part of logging. -- Fixed handling of generated JSON schema names when using arrays or generic types. -- Improved `CachingChatClient`'s coalescing of streaming updates, including reduced memory allocation and enhanced metadata propagation. -- Updated `OpenTelemetryChatClient` and `OpenTelemetryEmbeddingGenerator` to conform to the latest 1.28.0 draft specification of the Semantic Conventions for Generative AI systems. -- Improved `CompleteAsync`'s structured output support to handle primitive types, enums, and arrays. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview From ce70b6d52c062f127b7c3a28b4f8448af3ed492d Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Fri, 30 May 2025 13:20:26 -0700 Subject: [PATCH 134/472] Add comma to remarks (#6485) --- .../Logging/IHttpClientLogEnricher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/IHttpClientLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/IHttpClientLogEnricher.cs index c6ee71fa99a..f6fd2b637fc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/IHttpClientLogEnricher.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/IHttpClientLogEnricher.cs @@ -20,8 +20,8 @@ public interface IHttpClientLogEnricher /// object associated with the outgoing HTTP request. /// An optional that was thrown within the outgoing HTTP request processing. /// - /// Please be aware that depending on the result of the HTTP request - /// the and parameters may be . + /// Depending on the result of the HTTP request, + /// the and parameters might be . /// void Enrich(IEnrichmentTagCollector collector, HttpRequestMessage request, HttpResponseMessage? response, Exception? exception); } From af446a2ba95f8a551c0a574cf684f8f05cc5e205 Mon Sep 17 00:00:00 2001 From: Makazeu Date: Mon, 2 Jun 2025 15:43:15 +0800 Subject: [PATCH 135/472] Implement disk io metrics for linux (#6374) * Implement DiskStatsReader * Add a UT * Add LinuxDiskMetrics * Add DiskOperation and DiskIoTime metrics for Linux * update * Add more tests * Add UT * Update src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs Co-authored-by: Amadeusz Lechniak * Change ulong properties to uint in DiskStats and DiskStatsReader for improved memory efficiency * update * Rename `EnableDiskIoMetrics` to `EnableSystemDiskIoMetrics` * Add compatibility suppressions for EnableDiskIoMetrics * Add compatibility suppressions for EnableDiskIoMetrics --------- Co-authored-by: Amadeusz Lechniak --- .../CompatibilitySuppressions.xml | 46 ++++ .../Linux/Disk/DiskStats.cs | 36 +++ .../Linux/Disk/DiskStatsReader.cs | 111 +++++++++ .../Linux/Disk/IDiskStatsReader.cs | 18 ++ .../Linux/Disk/LinuxSystemDiskMetrics.cs | 172 ++++++++++++++ .../Linux/Log.cs | 4 + .../ResourceMonitoringOptions.Windows.cs | 6 - .../ResourceMonitoringOptions.cs | 7 + ...ceMonitoringServiceCollectionExtensions.cs | 6 +- .../Windows/Disk/WindowsDiskMetrics.cs | 8 +- .../Linux/Disk/DiskStatsReaderTests.cs | 135 +++++++++++ .../Linux/Disk/FakeDiskStatsReader.cs | 25 ++ .../Linux/Disk/LinuxSystemDiskMetricsTests.cs | 216 ++++++++++++++++++ ...iagnostics.ResourceMonitoring.Tests.csproj | 3 +- .../Windows/Disk/WindowsDiskMetricsTests.cs | 6 +- 15 files changed, 782 insertions(+), 17 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStats.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..6526176c304 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml @@ -0,0 +1,46 @@ + + + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStats.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStats.cs new file mode 100644 index 00000000000..5b1315e7a50 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStats.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; + +/// +/// Represents one line of statistics from "/proc/diskstats" +/// See https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats for details. +/// +internal sealed class DiskStats +{ + public int MajorNumber { get; set; } + public int MinorNumber { get; set; } + public string DeviceName { get; set; } = string.Empty; + public ulong ReadsCompleted { get; set; } + public ulong ReadsMerged { get; set; } + public ulong SectorsRead { get; set; } + public uint TimeReadingMs { get; set; } + public ulong WritesCompleted { get; set; } + public ulong WritesMerged { get; set; } + public ulong SectorsWritten { get; set; } + public uint TimeWritingMs { get; set; } + public uint IoInProgress { get; set; } + public uint TimeIoMs { get; set; } + public uint WeightedTimeIoMs { get; set; } + + // The following fields are available starting from kernel 4.18; if absent, remain 0 + public ulong DiscardsCompleted { get; set; } + public ulong DiscardsMerged { get; set; } + public ulong SectorsDiscarded { get; set; } + public uint TimeDiscardingMs { get; set; } + + // The following fields are available starting from kernel 5.5; if absent, remain 0 + public ulong FlushRequestsCompleted { get; set; } + public uint TimeFlushingMs { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs new file mode 100644 index 00000000000..11a6f72ccc8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs @@ -0,0 +1,111 @@ +// 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.Globalization; +using System.IO; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; + +/// +/// Handles reading and parsing of Linux procfs-diskstats file(/proc/diskstats). +/// +internal sealed class DiskStatsReader(IFileSystem fileSystem) : IDiskStatsReader +{ + private static readonly FileInfo _diskStatsFile = new("/proc/diskstats"); + private static readonly ObjectPool> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool(); + + /// + /// Reads and returns all disk statistics entries. + /// + /// List of . + public List ReadAll() + { + var diskStatsList = new List(); + + using ReturnableBufferWriter bufferWriter = new(_sharedBufferWriterPool); + using IEnumerator> enumerableLines = fileSystem.ReadAllByLines(_diskStatsFile, bufferWriter.Buffer).GetEnumerator(); + + while (enumerableLines.MoveNext()) + { + string line = enumerableLines.Current.Trim().ToString(); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + try + { + DiskStats stat = DiskStatsReader.ParseLine(line); + diskStatsList.Add(stat); + } +#pragma warning disable CA1031 + catch (Exception) +#pragma warning restore CA1031 + { + // ignore parsing errors + } + } + + return diskStatsList; + } + + /// + /// Parses one line of text into a DiskStats object. + /// + /// one line in "/proc/diskstats". + /// parsed DiskStats object. + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "These numbers represent fixed field indices in the Linux /proc/diskstats format")] + private static DiskStats ParseLine(string line) + { + // Split by any whitespace and remove empty entries +#pragma warning disable EA0009 + string[] parts = line.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); +#pragma warning restore EA0009 + + if (parts.Length < 14) + { + throw new FormatException($"Not enough fields: expected at least 14, got {parts.Length}"); + } + + // See https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats + var diskStats = new DiskStats + { + MajorNumber = int.Parse(parts[0], CultureInfo.InvariantCulture), + MinorNumber = int.Parse(parts[1], CultureInfo.InvariantCulture), + DeviceName = parts[2], + ReadsCompleted = ulong.Parse(parts[3], CultureInfo.InvariantCulture), + ReadsMerged = ulong.Parse(parts[4], CultureInfo.InvariantCulture), + SectorsRead = ulong.Parse(parts[5], CultureInfo.InvariantCulture), + TimeReadingMs = uint.Parse(parts[6], CultureInfo.InvariantCulture), + WritesCompleted = ulong.Parse(parts[7], CultureInfo.InvariantCulture), + WritesMerged = ulong.Parse(parts[8], CultureInfo.InvariantCulture), + SectorsWritten = ulong.Parse(parts[9], CultureInfo.InvariantCulture), + TimeWritingMs = uint.Parse(parts[10], CultureInfo.InvariantCulture), + IoInProgress = uint.Parse(parts[11], CultureInfo.InvariantCulture), + TimeIoMs = uint.Parse(parts[12], CultureInfo.InvariantCulture), + WeightedTimeIoMs = uint.Parse(parts[13], CultureInfo.InvariantCulture) + }; + + // Parse additional fields if present + if (parts.Length >= 18) + { + diskStats.DiscardsCompleted = ulong.Parse(parts[14], CultureInfo.InvariantCulture); + diskStats.DiscardsMerged = ulong.Parse(parts[15], CultureInfo.InvariantCulture); + diskStats.SectorsDiscarded = ulong.Parse(parts[16], CultureInfo.InvariantCulture); + diskStats.TimeDiscardingMs = uint.Parse(parts[17], CultureInfo.InvariantCulture); + } + + if (parts.Length >= 20) + { + diskStats.FlushRequestsCompleted = ulong.Parse(parts[18], CultureInfo.InvariantCulture); + diskStats.TimeFlushingMs = uint.Parse(parts[19], CultureInfo.InvariantCulture); + } + + return diskStats; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs new file mode 100644 index 00000000000..df9d0d7c020 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; + +/// +/// An interface for reading disk statistics. +/// +internal interface IDiskStatsReader +{ + /// + /// Gets all the disk statistics from the system. + /// + /// List of instances. + List ReadAll(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs new file mode 100644 index 00000000000..d70a65ed1b0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs @@ -0,0 +1,172 @@ +// 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; +using System.Diagnostics.Metrics; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Instruments; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; + +internal sealed class LinuxSystemDiskMetrics +{ + // The kernel's block layer always reports counts in 512-byte "sectors" regardless of the underlying device's real block size + // https://docs.kernel.org/block/stat.html#read-sectors-write-sectors-discard-sectors + private const int LinuxDiskSectorSize = 512; + private const int MinimumDiskStatsRefreshIntervalInSeconds = 10; + private const string DeviceKey = "system.device"; + private const string DirectionKey = "disk.io.direction"; + + private static readonly KeyValuePair _directionReadTag = new(DirectionKey, "read"); + private static readonly KeyValuePair _directionWriteTag = new(DirectionKey, "write"); + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + private readonly IDiskStatsReader _diskStatsReader; + private readonly object _lock = new(); + private readonly Dictionary _baselineDiskStatsDict = []; + private List _diskStatsSnapshot = []; + private DateTimeOffset _lastRefreshTime = DateTimeOffset.MinValue; + + public LinuxSystemDiskMetrics( + ILogger? logger, + IMeterFactory meterFactory, + IOptions options, + TimeProvider timeProvider, + IDiskStatsReader diskStatsReader) + { + _logger = logger ?? NullLogger.Instance; + _timeProvider = timeProvider; + _diskStatsReader = diskStatsReader; + if (!options.Value.EnableSystemDiskIoMetrics) + { + return; + } + + // We need to read the disk stats once to get the baseline values + _baselineDiskStatsDict = GetAllDiskStats().ToDictionary(d => d.DeviceName); + +#pragma warning disable CA2000 // Dispose objects before losing scope + // We don't dispose the meter because IMeterFactory handles that + // It's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912. + // Related documentation: https://github.com/dotnet/docs/pull/37170 + Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); +#pragma warning restore CA2000 // Dispose objects before losing scope + + // The metric is aligned with + // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio + _ = meter.CreateObservableCounter( + ResourceUtilizationInstruments.SystemDiskIo, + GetDiskIoMeasurements, + unit: "By", + description: "Disk bytes transferred"); + + // The metric is aligned with + // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskoperations + _ = meter.CreateObservableCounter( + ResourceUtilizationInstruments.SystemDiskOperations, + GetDiskOperationMeasurements, + unit: "{operation}", + description: "Disk operations"); + + // The metric is aligned with + // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time + _ = meter.CreateObservableCounter( + ResourceUtilizationInstruments.SystemDiskIoTime, + GetDiskIoTimeMeasurements, + unit: "s", + description: "Time disk spent activated"); + } + + private IEnumerable> GetDiskIoMeasurements() + { + List> measurements = []; + List diskStatsSnapshot = GetDiskStatsSnapshot(); + + foreach (DiskStats diskStats in diskStatsSnapshot) + { + _ = _baselineDiskStatsDict.TryGetValue(diskStats.DeviceName, out DiskStats? baselineDiskStats); + long readBytes = (long)(diskStats.SectorsRead - baselineDiskStats?.SectorsRead ?? 0L) * LinuxDiskSectorSize; + long writeBytes = (long)(diskStats.SectorsWritten - baselineDiskStats?.SectorsWritten ?? 0L) * LinuxDiskSectorSize; + measurements.Add(new Measurement(readBytes, new TagList { _directionReadTag, new(DeviceKey, diskStats.DeviceName) })); + measurements.Add(new Measurement(writeBytes, new TagList { _directionWriteTag, new(DeviceKey, diskStats.DeviceName) })); + } + + return measurements; + } + + private IEnumerable> GetDiskOperationMeasurements() + { + List> measurements = []; + List diskStatsSnapshot = GetDiskStatsSnapshot(); + + foreach (DiskStats diskStats in diskStatsSnapshot) + { + _ = _baselineDiskStatsDict.TryGetValue(diskStats.DeviceName, out DiskStats? baselineDiskStats); + long readCount = (long)(diskStats.ReadsCompleted - baselineDiskStats?.ReadsCompleted ?? 0L); + long writeCount = (long)(diskStats.WritesCompleted - baselineDiskStats?.WritesCompleted ?? 0L); + measurements.Add(new Measurement(readCount, new TagList { _directionReadTag, new(DeviceKey, diskStats.DeviceName) })); + measurements.Add(new Measurement(writeCount, new TagList { _directionWriteTag, new(DeviceKey, diskStats.DeviceName) })); + } + + return measurements; + } + + private IEnumerable> GetDiskIoTimeMeasurements() + { + List> measurements = []; + List diskStatsSnapshot = GetDiskStatsSnapshot(); + + foreach (DiskStats diskStats in diskStatsSnapshot) + { + _ = _baselineDiskStatsDict.TryGetValue(diskStats.DeviceName, out DiskStats? baselineDiskStats); + double ioTimeSeconds = (diskStats.TimeIoMs - baselineDiskStats?.TimeIoMs ?? 0) / 1000.0; // Convert to seconds + measurements.Add(new Measurement(ioTimeSeconds, new TagList { new(DeviceKey, diskStats.DeviceName) })); + } + + return measurements; + } + + private List GetDiskStatsSnapshot() + { + lock (_lock) + { + DateTimeOffset now = _timeProvider.GetUtcNow(); + if (_diskStatsSnapshot.Count == 0 || (now - _lastRefreshTime).TotalSeconds > MinimumDiskStatsRefreshIntervalInSeconds) + { + _diskStatsSnapshot = GetAllDiskStats(); + _lastRefreshTime = now; + } + } + + return _diskStatsSnapshot; + } + + private List GetAllDiskStats() + { + try + { + List diskStatsList = _diskStatsReader.ReadAll(); + + // We should not include ram, loop, or dm(device-mapper) devices in the disk stats, should we? + diskStatsList = diskStatsList + .Where(d => !d.DeviceName.StartsWith("ram", StringComparison.OrdinalIgnoreCase) + && !d.DeviceName.StartsWith("loop", StringComparison.OrdinalIgnoreCase) + && !d.DeviceName.StartsWith("dm-", StringComparison.OrdinalIgnoreCase)) + .ToList(); + return diskStatsList; + } +#pragma warning disable CA1031 + catch (Exception ex) +#pragma warning restore CA1031 + { + Log.HandleDiskStatsException(_logger, ex.Message); + } + + return []; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs index 918087b1b78..d2f9c8f5070 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs @@ -56,4 +56,8 @@ public static partial void CounterMessage100( public static partial void CounterMessage110( ILogger logger, long counterValue); + + [LoggerMessage(7, LogLevel.Warning, + "Error while getting disk stats: Error={errorMessage}")] + public static partial void HandleDiskStatsException(ILogger logger, string errorMessage); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs index f042d892ab1..9e8636506c7 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs @@ -10,12 +10,6 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; public partial class ResourceMonitoringOptions { - /// - /// Gets or sets a value indicating whether disk I/O metrics should be enabled. - /// - [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] - public bool EnableDiskIoMetrics { get; set; } - /// /// Gets or sets the list of source IPv4 addresses to track the connections for in telemetry. /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs index 3946add711d..420d6001f57 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs @@ -117,4 +117,11 @@ public partial class ResourceMonitoringOptions ///
    [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public bool UseDeltaNrPeriodsForCpuCalculation { get; set; } + + /// + /// Gets or sets a value indicating whether disk I/O metrics should be enabled. + /// + /// Previously EnableDiskIoMetrics. + [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] + public bool EnableSystemDiskIoMetrics { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index f018038c614..541984db78d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Diagnostics.ResourceMonitoring; #if !NETFRAMEWORK using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; #endif @@ -129,6 +130,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddSingleton(TimeProvider.System); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.PickLinuxParser(); @@ -136,7 +138,9 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild _ = builder.Services .AddActivatedSingleton() .AddActivatedSingleton() - .AddActivatedSingleton(); + .AddActivatedSingleton() + .AddActivatedSingleton() + .AddActivatedSingleton(); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs index 47706b3ce6a..2927fa657e3 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs @@ -34,7 +34,7 @@ public WindowsDiskMetrics( IOptions options) { _logger = logger ?? NullLogger.Instance; - if (!options.Value.EnableDiskIoMetrics) + if (!options.Value.EnableSystemDiskIoMetrics) { return; } @@ -73,6 +73,7 @@ public WindowsDiskMetrics( description: "Time disk spent activated"); } +#pragma warning disable CA1031 // Do not catch general exception types private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounterFactory, TimeProvider timeProvider) { const string DiskCategoryName = "LogicalDisk"; @@ -96,9 +97,7 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte ioTimePerfCounter.InitializeDiskCounters(); _diskIoTimePerfCounter = ioTimePerfCounter; } -#pragma warning disable CA1031 catch (Exception ex) -#pragma warning restore CA1031 { Log.DiskIoPerfCounterException(_logger, WindowsDiskPerfCounterNames.DiskIdleTimeCounter, ex.Message); } @@ -124,14 +123,13 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte ratePerfCounter.InitializeDiskCounters(); _diskIoRateCounters.Add(counterName, ratePerfCounter); } -#pragma warning disable CA1031 catch (Exception ex) -#pragma warning restore CA1031 { Log.DiskIoPerfCounterException(_logger, counterName, ex.Message); } } } +#pragma warning restore CA1031 // Do not catch general exception types private IEnumerable> GetDiskIoMeasurements() { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs new file mode 100644 index 00000000000..c5098b2d284 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk.Test; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] +public class DiskStatsReaderTests +{ + [Fact] + public void Test_ReadAll_Valid_DiskStats() + { + string diskStatsFileContent = + " 7 0 loop0 269334 0 12751202 147117 11604772 0 97447664 1402945 0 12193892 2255752 0 0 0 0 1206808 705690\n" + + " 7 1 loop1 965348 0 28605866 474103 73636257 0 1211288288 14086242 0 60580032 24777643 0 0 0 0 18723136 10217297\n" + + " 7 2 loop2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + + " 259 1 nvme1n1 4180498 5551 247430002 746099 96474435 12677267 2160066791 23514624 0 68786140 29777259 0 0 0 0 22111407 5516535\n" + + " 259 2 nvme1n1p1 4180387 5551 247422458 746080 96474435 12677267 2160066791 23514624 0 68786108 24260705 0 0 0 0 0 0\n" + + " 259 0 nvme0n1 6090587 689465 1120208521 1810566 19069165 8947684 406356430 3897150 0 38134844 6246643 69106 0 271818368 23139 1659742 515787\n" + + " 259 3 nvme0n1p1 378 0 26406 96 0 0 0 0 0 760 96 0 0 0 0 0 0\n" + + " 259 4 nvme0n1p2 7301 26408 116617 3628 600 47 59970 98 0 1196 3767 48 0 33106424 40 0 0\n" + + " 259 5 nvme0n1p3 6079544 663057 1119819306 1806337 19068535 8947637 406296460 3897045 0 38130316 5726482 69058 0 238711944 23098 0 0\n" + + " 252 0 dm-0 1303410 0 10434296 166616 1812455 0 14879824 1213588 0 397256 1380204 0 0 0 0 0 0\n" + + " 252 1 dm-1 712122 0 38299466 140852 18159197 0 286348832 1552768 0 14182384 1716692 69058 0 238711944 23072 0 0\n" + + " 252 5 dm-5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + + " 252 7 dm-7 6828 0 360325 2100 14438 0 1149672 1508 0 7524 3608 0 0 0 0 0 0\n" + + " 8 0 sda 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + + " 252 8 dm-8 100601 0 2990980 23940 3097278 0 32037680 1410540 0 5488608 1434496 513 0 67108872 16 0 0\n"; + + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/diskstats"), diskStatsFileContent } + }); + + var reader = new DiskStatsReader(fileSystem); + var dictionary = reader.ReadAll().ToDictionary(x => x.DeviceName); + Assert.Equal(15, dictionary.Count); + + var disk1 = dictionary["nvme0n1"]; + Assert.Equal(6_090_587u, disk1.ReadsCompleted); + Assert.Equal(689_465u, disk1.ReadsMerged); + Assert.Equal(1_120_208_521u, disk1.SectorsRead); + Assert.Equal(1_810_566u, disk1.TimeReadingMs); + Assert.Equal(19_069_165u, disk1.WritesCompleted); + Assert.Equal(8_947_684u, disk1.WritesMerged); + Assert.Equal(406_356_430u, disk1.SectorsWritten); + Assert.Equal(3_897_150u, disk1.TimeWritingMs); + Assert.Equal(0u, disk1.IoInProgress); + Assert.Equal(38_134_844u, disk1.TimeIoMs); + Assert.Equal(6_246_643u, disk1.WeightedTimeIoMs); + Assert.Equal(69_106u, disk1.DiscardsCompleted); + Assert.Equal(0u, disk1.DiscardsMerged); + Assert.Equal(271_818_368u, disk1.SectorsDiscarded); + Assert.Equal(23_139u, disk1.TimeDiscardingMs); + Assert.Equal(1_659_742u, disk1.FlushRequestsCompleted); + Assert.Equal(515_787u, disk1.TimeFlushingMs); + + var disk2 = dictionary["dm-8"]; + Assert.Equal(100_601u, disk2.ReadsCompleted); + Assert.Equal(0u, disk2.ReadsMerged); + Assert.Equal(2_990_980u, disk2.SectorsRead); + Assert.Equal(23_940u, disk2.TimeReadingMs); + Assert.Equal(3_097_278u, disk2.WritesCompleted); + Assert.Equal(0u, disk2.WritesMerged); + Assert.Equal(32_037_680u, disk2.SectorsWritten); + Assert.Equal(1_410_540u, disk2.TimeWritingMs); + Assert.Equal(0u, disk2.IoInProgress); + Assert.Equal(5_488_608u, disk2.TimeIoMs); + Assert.Equal(1_434_496u, disk2.WeightedTimeIoMs); + + var disk3 = dictionary["sda"]; + Assert.Equal(0u, disk3.ReadsCompleted); + Assert.Equal(0u, disk3.ReadsMerged); + Assert.Equal(0u, disk3.SectorsRead); + Assert.Equal(0u, disk3.TimeReadingMs); + Assert.Equal(0u, disk3.WritesCompleted); + Assert.Equal(0u, disk3.WritesMerged); + Assert.Equal(0u, disk3.SectorsWritten); + Assert.Equal(0u, disk3.TimeWritingMs); + Assert.Equal(0u, disk3.IoInProgress); + Assert.Equal(0u, disk3.TimeIoMs); + Assert.Equal(0u, disk3.WeightedTimeIoMs); + } + + [Fact] + public void Test_ReadAll_With_Invalid_Lines() + { + string diskStatsFileContent = + " 259 1 nvme1n1 4180498 5551 247430002 746099 96474435 12677267 2160066791 23514624 0 68786140 29777259 0 0 0 0 22111407 5516535\n" + + " 259 2 nvme1n1p1 4180387 5551 247422458\n" + + " 259 2 nvme1n1p1 4180387 5551 247422458 746080 96474435 12677267 2160066791 23514624 0 68786108 24260705 0 0 0 0 0 0\n" + + " 259 0 nvme0n1 6090587 689465 1120208521 1810566 19069165 8947684 406356430 3897150 0 38134844 6246643 69106 0 271818368 23139 1659742 515787\n" + + " 259 nvme0n1p1 378 0 26406 96 0 0 0 0 0 760 96 0 0 0 0 0 0\n"; + + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/diskstats"), diskStatsFileContent } + }); + + var reader = new DiskStatsReader(fileSystem); + var dictionary = reader.ReadAll().ToDictionary(x => x.DeviceName); + Assert.Equal(3, dictionary.Count); + + var disk1 = dictionary["nvme1n1"]; + Assert.Equal(4_180_498u, disk1.ReadsCompleted); + Assert.Equal(5_551u, disk1.ReadsMerged); + Assert.Equal(247_430_002u, disk1.SectorsRead); + Assert.Equal(746_099u, disk1.TimeReadingMs); + Assert.Equal(96_474_435u, disk1.WritesCompleted); + Assert.Equal(12_677_267u, disk1.WritesMerged); + Assert.Equal(2_160_066_791u, disk1.SectorsWritten); + Assert.Equal(23_514_624u, disk1.TimeWritingMs); + Assert.Equal(0u, disk1.IoInProgress); + Assert.Equal(68_786_140u, disk1.TimeIoMs); + Assert.Equal(29_777_259u, disk1.WeightedTimeIoMs); + Assert.Equal(0u, disk1.DiscardsCompleted); + Assert.Equal(0u, disk1.DiscardsMerged); + Assert.Equal(0u, disk1.SectorsDiscarded); + Assert.Equal(0u, disk1.TimeDiscardingMs); + Assert.Equal(22_111_407u, disk1.FlushRequestsCompleted); + Assert.Equal(5_516_535u, disk1.TimeFlushingMs); + + var disk2 = dictionary["nvme1n1p1"]; + Assert.NotNull(disk2); + + var disk3 = dictionary["nvme0n1"]; + Assert.NotNull(disk3); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs new file mode 100644 index 00000000000..1c69be74709 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs @@ -0,0 +1,25 @@ +// 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.Linq; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk.Test; + +internal class FakeDiskStatsReader(Dictionary> stats) : IDiskStatsReader +{ + private int _index; + + public List ReadAll() + { + if (_index >= stats.Values.First().Count) + { + throw new InvalidOperationException("No more values available."); + } + + List list = stats.Values.Select(x => x[_index]).ToList(); + _index++; + return list; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs new file mode 100644 index 00000000000..c6aba8e0b53 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs @@ -0,0 +1,216 @@ +// 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.Metrics; +using System.Linq; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Shared.Instruments; +using Microsoft.TestUtilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk.Test; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] +public class LinuxSystemDiskMetricsTests +{ + private readonly FakeLogger _fakeLogger = new(); + + [Fact] + public void Creates_Meter_With_Correct_Name() + { + using var meterFactory = new TestMeterFactory(); + var diskStatsReaderMock = new Mock(); + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; + + _ = new LinuxSystemDiskMetrics( + _fakeLogger, + meterFactory, + Options.Options.Create(options), + TimeProvider.System, + diskStatsReaderMock.Object); + + Meter meter = meterFactory.Meters.Single(); + Assert.Equal(ResourceUtilizationInstruments.MeterName, meter.Name); + } + + [Fact] + public void Test_MetricValues() + { + using var meterFactory = new TestMeterFactory(); + var fakeTimeProvider = new FakeTimeProvider(); + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; + + // Set up + var diskStatsReader = new FakeDiskStatsReader(new Dictionary> + { + { + "sda", [ + new DiskStats + { + DeviceName = "sda", + SectorsRead = 0, + SectorsWritten = 0, + ReadsCompleted = 0, + WritesCompleted = 0, + TimeIoMs = 0 + }, + new DiskStats + { + DeviceName = "sda", + SectorsRead = 500, + SectorsWritten = 1000, + ReadsCompleted = 600, + WritesCompleted = 1200, + TimeIoMs = 1234 + }, + new DiskStats + { + DeviceName = "sda", + SectorsRead = 700, + SectorsWritten = 1100, + ReadsCompleted = 800, + WritesCompleted = 1300, + TimeIoMs = 2234 + }, + new DiskStats + { + DeviceName = "sda", + SectorsRead = 1000, + SectorsWritten = 1600, + ReadsCompleted = 1300, + WritesCompleted = 1350, + TimeIoMs = 4444 + } + ] + }, + { + "sdb", [ + new DiskStats + { + DeviceName = "sdb", + SectorsRead = 200, + SectorsWritten = 300, + ReadsCompleted = 400, + WritesCompleted = 500, + TimeIoMs = 6000 + }, + new DiskStats + { + DeviceName = "sdb", + SectorsRead = 350, + SectorsWritten = 450, + ReadsCompleted = 550, + WritesCompleted = 650, + TimeIoMs = 7500 + }, + new DiskStats + { + DeviceName = "sdb", + SectorsRead = 400, + SectorsWritten = 500, + ReadsCompleted = 600, + WritesCompleted = 700, + TimeIoMs = 7500 + }, + new DiskStats + { + DeviceName = "sdb", + SectorsRead = 550, + SectorsWritten = 650, + ReadsCompleted = 750, + WritesCompleted = 850, + TimeIoMs = 9500 + } + ] + }, + }); + + _ = new LinuxSystemDiskMetrics( + _fakeLogger, + meterFactory, + Options.Options.Create(options), + fakeTimeProvider, + diskStatsReader); + Meter meter = meterFactory.Meters.Single(); + + var readTag = new KeyValuePair("disk.io.direction", "read"); + var writeTag = new KeyValuePair("disk.io.direction", "write"); + var deviceTagSda = new KeyValuePair("system.device", "sda"); + var deviceTagSdb = new KeyValuePair("system.device", "sdb"); + + using var diskIoCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskIo); + using var operationCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskOperations); + using var ioTimeCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskIoTime); + + // 1st measurement + diskIoCollector.RecordObservableInstruments(); + operationCollector.RecordObservableInstruments(); + ioTimeCollector.RecordObservableInstruments(); + + // Assert the 1st measurement + var diskIoMeasurement = diskIoCollector.GetMeasurementSnapshot(); + Assert.Equal(4, diskIoMeasurement.Count); + Assert.Equal(256_000, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // (500 - 0) * 512 = 256000 + Assert.Equal(76_800, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // (350 - 200) * 512 = 76800 + Assert.Equal(512_000, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // (1000 - 0) * 512 = 512000 + Assert.Equal(76_800, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // (450 - 300) * 512 = 76800 + var operationMeasurement = operationCollector.GetMeasurementSnapshot(); + Assert.Equal(4, operationMeasurement.Count); + Assert.Equal(600, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // 600 - 0 = 600 + Assert.Equal(150, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // 550 - 400 = 150 + Assert.Equal(1200, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // 1200 - 0 = 1200 + Assert.Equal(150, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // 650 - 500 = 150 + var ioTimeMeasurement = ioTimeCollector.GetMeasurementSnapshot(); + Assert.Equal(2, ioTimeMeasurement.Count); + Assert.Equal(1.234, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSda)).Value, 0.01); // (1234 - 0) / 1000 = 1.234 + Assert.Equal(1.5, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSdb)).Value, 0.01); // (7500 - 6000) / 1000 = 6.0 + + // 2nd measurement + fakeTimeProvider.Advance(TimeSpan.FromMinutes(1)); + diskIoCollector.RecordObservableInstruments(); + operationCollector.RecordObservableInstruments(); + ioTimeCollector.RecordObservableInstruments(); + + // Assert the 2nd measurement + diskIoMeasurement = diskIoCollector.GetMeasurementSnapshot(); + Assert.Equal(358_400, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // (700 - 0) * 512 = 358400 + Assert.Equal(102_400, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // (400 - 200) * 512 = 102400 + Assert.Equal(563_200, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // (1100 - 0) * 512 = 563200 + Assert.Equal(102_400, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // (500 - 300) * 512 = 102400 + operationMeasurement = operationCollector.GetMeasurementSnapshot(); + Assert.Equal(800, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // 800 - 0 = 800 + Assert.Equal(200, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // 600 - 400 = 200 + Assert.Equal(1300, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // 1300 - 0 = 1300 + Assert.Equal(200, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // 700 - 500 = 200 + ioTimeMeasurement = ioTimeCollector.GetMeasurementSnapshot(); + Assert.Equal(2.234, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSda)).Value, 0.01); // (2234 - 0) / 1000 = 2.234 + Assert.Equal(1.5, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSdb)).Value, 0.01); // (7500 - 6000) / 1000 = 1.5 + + // 3rd measurement + fakeTimeProvider.Advance(TimeSpan.FromMinutes(1)); + diskIoCollector.RecordObservableInstruments(); + operationCollector.RecordObservableInstruments(); + ioTimeCollector.RecordObservableInstruments(); + + // Assert the 3rd measurement + diskIoMeasurement = diskIoCollector.GetMeasurementSnapshot(); + Assert.Equal(512_000, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // (1000 - 0) * 512 = 512000 + Assert.Equal(179_200, diskIoMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // (550 - 200) * 512 = 179200 + Assert.Equal(819_200, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // (1600 - 0) * 512 = 819200 + Assert.Equal(179_200, diskIoMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // (650 - 300) * 512 = 179200 + operationMeasurement = operationCollector.GetMeasurementSnapshot(); + Assert.Equal(1300, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSda)).Value); // 1300 - 0 = 1300 + Assert.Equal(350, operationMeasurement.Last(x => x.MatchesTags(readTag, deviceTagSdb)).Value); // 750 - 400 = 350 + Assert.Equal(1350, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSda)).Value); // 1350 - 0 = 1350 + Assert.Equal(350, operationMeasurement.Last(x => x.MatchesTags(writeTag, deviceTagSdb)).Value); // 850 - 500 = 350 + ioTimeMeasurement = ioTimeCollector.GetMeasurementSnapshot(); + Assert.Equal(4.444, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSda)).Value, 0.01); // (4444 - 0) / 1000 = 4.444 + Assert.Equal(3.5, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSdb)).Value, 0.01); // (9500 - 6000) / 1000 = 3.5 + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj index b7fcf503d96..ff2cd26412f 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -6,8 +6,7 @@ - - + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs index 25587a6ad59..a592aacce19 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Disk/WindowsDiskMetricsTests.cs @@ -30,7 +30,7 @@ public void Creates_Meter_With_Correct_Name() { using var meterFactory = new TestMeterFactory(); var performanceCounterFactoryMock = new Mock(); - var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true }; + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; _ = new WindowsDiskMetrics( _fakeLogger, @@ -49,7 +49,7 @@ public void DiskOperationMetricsTest() using var meterFactory = new TestMeterFactory(); var performanceCounterFactory = new Mock(); var fakeTimeProvider = new FakeTimeProvider(); - var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true }; + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; // Set up const string ReadCounterName = WindowsDiskPerfCounterNames.DiskReadsCounter; @@ -123,7 +123,7 @@ public void DiskIoBytesMetricsTest() using var meterFactory = new TestMeterFactory(); var performanceCounterFactory = new Mock(); var fakeTimeProvider = new FakeTimeProvider(); - var options = new ResourceMonitoringOptions { EnableDiskIoMetrics = true }; + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; // Set up const string ReadCounterName = WindowsDiskPerfCounterNames.DiskReadBytesCounter; From 691f3612f0e98428882571aa766735eadd2a1320 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 3 Jun 2025 17:12:35 +0100 Subject: [PATCH 136/472] Add `AIFunction.ReturnJsonSchema` (#6447) * Add AIFunction.ReturnJsonSchema * Use `MethodInfo.ReturnParameter`. * Use null return schema for void returning functions. * Remove experimental attribute and suppress warning. * Remove suppression and list new API as stable. --------- Co-authored-by: Jeff Handley --- .../Functions/AIFunction.cs | 7 +++++ .../Functions/AIFunctionFactory.cs | 29 ++++++++++++++----- .../Microsoft.Extensions.AI.Abstractions.json | 4 +++ .../AIJsonUtilities.Schema.Create.cs | 2 +- .../Functions/AIFunctionFactoryTest.cs | 26 +++++++++++++++++ 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 448c81f61f4..3910040d0a0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -38,6 +38,13 @@ public abstract class AIFunction : AITool /// public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema; + /// Gets a JSON Schema describing the function's return value. + /// + /// A typically reflects a function that doesn't specify a return schema + /// or a function that returns , , or . + /// + public virtual JsonElement? ReturnJsonSchema => null; + /// /// Gets the underlying that this might be wrapping. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index ffd47eb08fc..320df4098a3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -540,6 +540,7 @@ private ReflectionAIFunction( public override string Description => FunctionDescriptor.Description; public override MethodInfo UnderlyingMethod => FunctionDescriptor.Method; public override JsonElement JsonSchema => FunctionDescriptor.JsonSchema; + public override JsonElement? ReturnJsonSchema => FunctionDescriptor.ReturnJsonSchema; public override JsonSerializerOptions JsonSerializerOptions => FunctionDescriptor.JsonSerializerOptions; protected override async ValueTask InvokeCoreAsync( @@ -683,13 +684,17 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i]); } - // Get a marshaling delegate for the return value. - ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions); - + ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType); Method = key.Method; Name = key.Name ?? GetFunctionName(key.Method); Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; JsonSerializerOptions = serializerOptions; + ReturnJsonSchema = returnType is null ? null : AIJsonUtilities.CreateJsonSchema( + returnType, + description: key.Method.ReturnParameter.GetCustomAttribute(inherit: true)?.Description, + serializerOptions: serializerOptions, + inferenceOptions: schemaOptions); + JsonSchema = AIJsonUtilities.CreateFunctionJsonSchema( key.Method, title: string.Empty, // Forces skipping of the title keyword @@ -703,6 +708,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions public MethodInfo Method { get; } public JsonSerializerOptions JsonSerializerOptions { get; } public JsonElement JsonSchema { get; } + public JsonElement? ReturnJsonSchema { get; } public Func[] ParameterMarshallers { get; } public Func> ReturnParameterMarshaller { get; } public ReflectionAIFunction? CachedDefaultInstance { get; set; } @@ -849,15 +855,16 @@ static void ThrowNullServices(string parameterName) => /// Gets a delegate for handling the result value of a method, converting it into the to return from the invocation. ///
    private static Func> GetReturnParameterMarshaller( - DescriptorKey key, JsonSerializerOptions serializerOptions) + DescriptorKey key, JsonSerializerOptions serializerOptions, out Type? returnType) { - Type returnType = key.Method.ReturnType; + returnType = key.Method.ReturnType; JsonTypeInfo returnTypeInfo; Func>? marshalResult = key.MarshalResult; // Void if (returnType == typeof(void)) { + returnType = null; if (marshalResult is not null) { return (result, cancellationToken) => marshalResult(null, null, cancellationToken); @@ -869,6 +876,7 @@ static void ThrowNullServices(string parameterName) => // Task if (returnType == typeof(Task)) { + returnType = null; if (marshalResult is not null) { return async (result, cancellationToken) => @@ -888,6 +896,7 @@ static void ThrowNullServices(string parameterName) => // ValueTask if (returnType == typeof(ValueTask)) { + returnType = null; if (marshalResult is not null) { return async (result, cancellationToken) => @@ -910,6 +919,8 @@ static void ThrowNullServices(string parameterName) => if (returnType.GetGenericTypeDefinition() == typeof(Task<>)) { MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition(returnType, _taskGetResult); + returnType = taskResultGetter.ReturnType; + if (marshalResult is not null) { return async (taskObj, cancellationToken) => @@ -920,7 +931,7 @@ static void ThrowNullServices(string parameterName) => }; } - returnTypeInfo = serializerOptions.GetTypeInfo(taskResultGetter.ReturnType); + returnTypeInfo = serializerOptions.GetTypeInfo(returnType); return async (taskObj, cancellationToken) => { await ((Task)ThrowIfNullResult(taskObj)).ConfigureAwait(true); @@ -934,6 +945,7 @@ static void ThrowNullServices(string parameterName) => { MethodInfo valueTaskAsTask = GetMethodFromGenericMethodDefinition(returnType, _valueTaskAsTask); MethodInfo asTaskResultGetter = GetMethodFromGenericMethodDefinition(valueTaskAsTask.ReturnType, _taskGetResult); + returnType = asTaskResultGetter.ReturnType; if (marshalResult is not null) { @@ -946,7 +958,7 @@ static void ThrowNullServices(string parameterName) => }; } - returnTypeInfo = serializerOptions.GetTypeInfo(asTaskResultGetter.ReturnType); + returnTypeInfo = serializerOptions.GetTypeInfo(returnType); return async (taskObj, cancellationToken) => { var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(taskObj), null)!; @@ -960,7 +972,8 @@ static void ThrowNullServices(string parameterName) => // For everything else, just serialize the result as-is. if (marshalResult is not null) { - return (result, cancellationToken) => marshalResult(result, returnType, cancellationToken); + Type returnTypeCopy = returnType; + return (result, cancellationToken) => marshalResult(result, returnTypeCopy, cancellationToken); } returnTypeInfo = serializerOptions.GetTypeInfo(returnType); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 25fe7307655..499ff4b4a71 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -169,6 +169,10 @@ "Member": "virtual System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.AIFunction.JsonSerializerOptions { get; }", "Stage": "Stable" }, + { + "Member": "virtual System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIFunction.ReturnJsonSchema { get; }", + "Stage": "Stable" + }, { "Member": "virtual System.Reflection.MethodInfo? Microsoft.Extensions.AI.AIFunction.UnderlyingMethod { get; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index ad4b897cef2..e182d4149bb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -227,7 +227,7 @@ private static JsonNode CreateJsonSchemaCore( (schemaObj ??= [])[DescriptionPropertyName] = description; } - return schemaObj ?? (JsonNode)true; + return schemaObj ?? new JsonObject(); } if (type == typeof(void)) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 6d448efb710..84298788e8c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -100,22 +100,27 @@ public async Task Returns_AsyncReturnTypesSupported_Async() AIFunction func; func = AIFunctionFactory.Create(Task (string a) => Task.FromResult(a + " " + a)); + Assert.Equal("""{"type":"string"}""", func.ReturnJsonSchema.ToString()); AssertExtensions.EqualFunctionCallResults("test test", await func.InvokeAsync(new() { ["a"] = "test" })); func = AIFunctionFactory.Create(ValueTask (string a, string b) => new ValueTask(b + " " + a)); + Assert.Equal("""{"type":"string"}""", func.ReturnJsonSchema.ToString()); AssertExtensions.EqualFunctionCallResults("hello world", await func.InvokeAsync(new() { ["b"] = "hello", ["a"] = "world" })); long result = 0; func = AIFunctionFactory.Create(async Task (int a, long b) => { result = a + b; await Task.Yield(); }); + Assert.Null(func.ReturnJsonSchema); AssertExtensions.EqualFunctionCallResults(null, await func.InvokeAsync(new() { ["a"] = 1, ["b"] = 2L })); Assert.Equal(3, result); result = 0; func = AIFunctionFactory.Create(async ValueTask (int a, long b) => { result = a + b; await Task.Yield(); }); + Assert.Null(func.ReturnJsonSchema); AssertExtensions.EqualFunctionCallResults(null, await func.InvokeAsync(new() { ["a"] = 1, ["b"] = 2L })); Assert.Equal(3, result); func = AIFunctionFactory.Create((int count) => SimpleIAsyncEnumerable(count), serializerOptions: JsonContext.Default.Options); + Assert.Equal("""{"type":"array","items":{"type":"integer"}}""", func.ReturnJsonSchema.ToString()); AssertExtensions.EqualFunctionCallResults(new int[] { 0, 1, 2, 3, 4 }, await func.InvokeAsync(new() { ["count"] = 5 }), JsonContext.Default.Options); static async IAsyncEnumerable SimpleIAsyncEnumerable(int count) @@ -220,6 +225,8 @@ public async Task AIFunctionFactoryOptions_SupportsSkippingParameters() Assert.DoesNotContain("firstParameter", func.JsonSchema.ToString()); Assert.Contains("secondParameter", func.JsonSchema.ToString()); + Assert.Equal("""{"type":"string"}""", func.ReturnJsonSchema.ToString()); + var result = (JsonElement?)await func.InvokeAsync(new() { ["firstParameter"] = "test", @@ -265,6 +272,8 @@ public async Task AIFunctionArguments_SatisfiesParameters() Assert.DoesNotContain("services", func.JsonSchema.ToString()); Assert.DoesNotContain("arguments", func.JsonSchema.ToString()); + Assert.Equal("""{"type":"integer"}""", func.ReturnJsonSchema.ToString()); + await Assert.ThrowsAsync("arguments.Services", () => func.InvokeAsync(arguments).AsTask()); arguments.Services = sp; @@ -430,6 +439,8 @@ public async Task FromKeyedServices_ResolvesFromServiceProvider() Assert.Contains("myInteger", f.JsonSchema.ToString()); Assert.DoesNotContain("service", f.JsonSchema.ToString()); + Assert.Equal("""{"type":"integer"}""", f.ReturnJsonSchema.ToString()); + Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); var result = await f.InvokeAsync(new() { ["myInteger"] = 1, Services = sp }); @@ -451,6 +462,8 @@ public async Task FromKeyedServices_NullKeysBindToNonKeyedServices() Assert.Contains("myInteger", f.JsonSchema.ToString()); Assert.DoesNotContain("service", f.JsonSchema.ToString()); + Assert.Equal("""{"type":"integer"}""", f.ReturnJsonSchema.ToString()); + Exception e = await Assert.ThrowsAsync("arguments.Services", () => f.InvokeAsync(new() { ["myInteger"] = 1 }).AsTask()); var result = await f.InvokeAsync(new() { ["myInteger"] = 1, Services = sp }); @@ -743,6 +756,7 @@ public async Task MarshalResult_TypeIsDeclaredTypeEvenWhenDerivedTypeReturned() Assert.Equal(cts.Token, cancellationToken); return "marshalResultInvoked"; }, + SerializerOptions = JsonContext.Default.Options, }); object? result = await f.InvokeAsync(new() { ["i"] = 42 }, cts.Token); @@ -760,6 +774,17 @@ public async Task AIFunctionFactory_DefaultDefaultParameter() Assert.Contains("00000000-0000-0000-0000-000000000000,0", result?.ToString()); } + [Fact] + public void AIFunctionFactory_ReturnTypeWithDescriptionAttribute() + { + AIFunction f = AIFunctionFactory.Create(Add, serializerOptions: JsonContext.Default.Options); + + Assert.Equal("""{"description":"The summed result","type":"integer"}""", f.ReturnJsonSchema.ToString()); + + [return: Description("The summed result")] + static int Add(int a, int b) => a + b; + } + private sealed class MyService(int value) { public int Value => value; @@ -853,5 +878,6 @@ private static AIFunctionFactoryOptions CreateKeyedServicesSupportOptions() => [JsonSerializable(typeof(string))] [JsonSerializable(typeof(Guid))] [JsonSerializable(typeof(StructWithDefaultCtor))] + [JsonSerializable(typeof(B))] private partial class JsonContext : JsonSerializerContext; } From 2f2a3fd4a48dbf36931578981cd94dd02787177e Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 5 Jun 2025 00:48:23 +0000 Subject: [PATCH 137/472] Merged PR 50646: Getting ready for the 9.6 Release Getting ready for th 9.6 release ---- #### AI description (iteration 1) #### PR Classification This PR prepares the repository for the 9.6 release by upgrading dependency versions and refining build pipeline configurations. #### PR Summary The PR updates key dependency versions and adjusts pipeline settings to support a stable 9.6 release build. - `/eng/Version.Details.xml`: Upgraded dependency versions from 9.0.5 to 9.0.6 with corresponding SHA updates. - `/eng/Versions.props`: Updated version properties for major dependencies (and LTS versions bumped to 8.0.17) while enabling package stabilization and setting DotNetFinalVersionKind to release. - `/NuGet.config`: Revised package source definitions and disabled package source mappings for internal feeds. - `/azure-pipelines.yml` & `/eng/pipelines/templates/BuildAndTest.yml`: Removed the CodeCoverage stage and added tasks to set up private feed credentials with integration tests temporarily skipped. - `/Directory.Build.props`: Suppressed the NU1507 warning for internal branch builds. --- Directory.Build.props | 5 + NuGet.config | 64 +++++--- azure-pipelines.yml | 46 ------ eng/Version.Details.xml | 188 +++++++++++------------ eng/Versions.props | 122 +++++++-------- eng/pipelines/templates/BuildAndTest.yml | 32 +++- 6 files changed, 227 insertions(+), 230 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0af806af628..0c0fcf22bfd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,11 @@ $(NetCoreTargetFrameworks) + + + $(NoWarn);NU1507 + + false latest diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..3df29b93f5a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,31 @@ + + + + + + + + + + + + + + + + + + + + + @@ -18,35 +39,34 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ec5e3d1cdb..0052dc9f706 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -239,51 +239,6 @@ extends: isWindows: false warnAsError: 0 - # ---------------------------------------------------------------- - # This stage performs quality gates enforcements - # ---------------------------------------------------------------- - - stage: codecoverage - displayName: CodeCoverage - dependsOn: - - build - condition: and(succeeded('build'), ne(variables['SkipQualityGates'], 'true')) - variables: - - template: /eng/common/templates-official/variables/pool-providers.yml@self - jobs: - - template: /eng/common/templates-official/jobs/jobs.yml@self - parameters: - enableMicrobuild: true - enableTelemetry: true - runAsPublic: ${{ variables['runAsPublic'] }} - workspace: - clean: all - - # ---------------------------------------------------------------- - # This stage downloads the code coverage reports from the build jobs, - # merges those and validates the combined test coverage. - # ---------------------------------------------------------------- - jobs: - - job: CodeCoverageReport - timeoutInMinutes: 180 - - pool: - name: NetCore1ESPool-Internal - image: 1es-mariner-2 - os: linux - - preSteps: - - checkout: self - clean: true - persistCredentials: true - fetchDepth: 1 - - steps: - - script: $(Build.SourcesDirectory)/build.sh --ci --restore - displayName: Init toolset - - - template: /eng/pipelines/templates/VerifyCoverageReport.yml - - # ---------------------------------------------------------------- # This stage only performs a build treating warnings as errors # to detect any kind of code style violations @@ -339,7 +294,6 @@ extends: parameters: validateDependsOn: - build - - codecoverage - correctness publishingInfraVersion: 3 enableSymbolValidation: false diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ac0208aebcd..427a514700a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,196 +1,196 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - e36e4d1a8f8dfb08d7e3a6041459c9791d732c01 + 3875b54e7b10b10606b105340199946d0b877754 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ed74665e773dd1ebea3289c5662d71c590305932 + 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 6765359588e8b38bab2a7974db9398432703828f + 8751e6d519fda94d5154187358765311ed4a4e84 diff --git a/eng/Versions.props b/eng/Versions.props index 483b6366af6..690e6d32f2d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,14 +11,14 @@ - false + true - + release true @@ -34,55 +34,55 @@ --> - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 - 9.0.5 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 + 9.0.6 - 9.0.5 + 9.0.6 9.0.0-beta.25271.1 @@ -108,8 +108,8 @@ 8.0.1 8.0.0 8.0.2 - 8.0.16 - 8.0.16 + 8.0.17 + 8.0.17 8.0.0 8.0.1 8.0.1 @@ -126,17 +126,17 @@ 8.0.5 8.0.0 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 - 8.0.16 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 + 8.0.17 - 8.0.16 + 8.0.17 9.0.5 - 9.0.0-beta.25271.1 + 9.0.0-beta.25302.2 diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 454fd75c7af..a8c0bd3b921 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -44,6 +44,11 @@ parameters: displayName: Publish installers and checksums type: boolean default: true + + - name: requireDefaultChannels + displayName: Fail the build if there are no default channel(s) registrations for the current build + type: boolean + default: false - name: SDLValidationParameters type: object @@ -312,5 +317,6 @@ stages: -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} -AzdoToken '$(System.AccessToken)' -WaitPublishingFinish true + -RequireDefaultChannels ${{ parameters.requireDefaultChannels }} -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' diff --git a/eng/common/post-build/publish-using-darc.ps1 b/eng/common/post-build/publish-using-darc.ps1 index 90b58e32a87..a261517ef90 100644 --- a/eng/common/post-build/publish-using-darc.ps1 +++ b/eng/common/post-build/publish-using-darc.ps1 @@ -5,7 +5,8 @@ param( [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro.dot.net', [Parameter(Mandatory=$true)][string] $WaitPublishingFinish, [Parameter(Mandatory=$false)][string] $ArtifactsPublishingAdditionalParameters, - [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters + [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters, + [Parameter(Mandatory=$false)][string] $RequireDefaultChannels ) try { @@ -33,6 +34,10 @@ try { if ("false" -eq $WaitPublishingFinish) { $optionalParams.Add("--no-wait") | Out-Null } + + if ("true" -eq $RequireDefaultChannels) { + $optionalParams.Add("--default-channels-required") | Out-Null + } & $darc add-build-to-channel ` --id $buildId ` diff --git a/global.json b/global.json index 78b9a9c3e65..84536c55f54 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25271.1", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25271.1" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25302.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25302.2" } } From 3f61adaf297d399955a631b5b7febf969903084d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 9 Jun 2025 10:44:40 -0400 Subject: [PATCH 142/472] Improve handling of RawRepresentation in OpenAI{Response}ChatClient (#6500) --- .../OpenAIChatClient.cs | 3 + .../OpenAIResponseChatClient.cs | 102 +++++++++++++----- .../OpenAIResponseClientTests.cs | 11 +- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 001f4d1a593..cd46dae53b0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -261,6 +261,9 @@ private static List ToOpenAIChatContent(IList case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"); + + case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: + return rawContentPart; } return null; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 25035e7e225..d3caee286be 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -15,6 +15,7 @@ using OpenAI.Responses; using static Microsoft.Extensions.AI.OpenAIChatClient; +#pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable S3604 // Member initializer values should not be redundant @@ -87,12 +88,13 @@ public async Task GetResponseAsync( // Convert and return the results. ChatResponse response = new() { - ResponseId = openAIResponse.Id, ConversationId = openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), Messages = [new(ChatRole.Assistant, [])], ModelId = openAIResponse.Model, + RawRepresentation = openAIResponse, + ResponseId = openAIResponse.Id, Usage = ToUsageDetails(openAIResponse), }; @@ -125,12 +127,20 @@ public async Task GetResponseAsync( case FunctionCallResponseItem functionCall: response.FinishReason ??= ChatFinishReason.ToolCalls; - message.Contents.Add( - FunctionCallContent.CreateFromParsedArguments( - functionCall.FunctionArguments.ToMemory(), - functionCall.CallId, - functionCall.FunctionName, - static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!)); + var fcc = FunctionCallContent.CreateFromParsedArguments( + functionCall.FunctionArguments.ToMemory(), + functionCall.CallId, + functionCall.FunctionName, + static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!); + fcc.RawRepresentation = outputItem; + message.Contents.Add(fcc); + break; + + default: + message.Contents.Add(new() + { + RawRepresentation = outputItem, + }); break; } } @@ -170,20 +180,21 @@ public async IAsyncEnumerable GetStreamingResponseAsync( createdAt = createdUpdate.Response.CreatedAt; responseId = createdUpdate.Response.Id; modelId = createdUpdate.Response.Model; - break; + goto default; case StreamingResponseCompletedUpdate completedUpdate: yield return new() { - Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], - CreatedAt = createdAt, - ResponseId = responseId, - ConversationId = responseId, FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? (functionCallInfos is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop), + Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], + ConversationId = responseId, + CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, Role = lastRole, }; break; @@ -200,11 +211,11 @@ public async IAsyncEnumerable GetStreamingResponseAsync( break; } - break; + goto default; case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate: _ = outputIndexToMessages.Remove(outputItemDoneUpdate.OutputIndex); - break; + goto default; case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate: _ = outputIndexToMessages.TryGetValue(outputTextDeltaUpdate.OutputIndex, out MessageResponseItem? messageItem); @@ -212,11 +223,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ToChatRole(messageItem?.Role); yield return new ChatResponseUpdate(lastRole, outputTextDeltaUpdate.Delta) { + ConversationId = responseId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, - ConversationId = responseId, }; break; @@ -227,7 +239,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( _ = (callInfo.Arguments ??= new()).Append(functionCallArgumentsDeltaUpdate.Delta); } - break; + goto default; } case StreamingResponseFunctionCallArgumentsDoneUpdate functionCallOutputDoneUpdate: @@ -246,25 +258,23 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ChatRole.Assistant; yield return new ChatResponseUpdate(lastRole, [fci]) { + ConversationId = responseId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, - ConversationId = responseId, }; + + break; } - break; + goto default; } case StreamingResponseErrorUpdate errorUpdate: yield return new ChatResponseUpdate { - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - ResponseId = responseId, - Role = lastRole, ConversationId = responseId, Contents = [ @@ -274,6 +284,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( Details = errorUpdate.Param, } ], + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, + Role = lastRole, }; break; @@ -283,12 +299,26 @@ public async IAsyncEnumerable GetStreamingResponseAsync( CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, Role = lastRole, ConversationId = responseId, Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], }; break; + + default: + yield return new ChatResponseUpdate + { + ConversationId = responseId, + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, + Role = lastRole, + }; + break; } } } @@ -487,6 +517,10 @@ private static IEnumerable ToOpenAIResponseItems( callContent.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; + + case AIContent when item.RawRepresentation is ResponseItem rawRep: + yield return rawRep; + break; } } @@ -530,11 +564,25 @@ private static List ToAIContents(IEnumerable con switch (part.Kind) { case ResponseContentPartKind.OutputText: - results.Add(new TextContent(part.Text)); + results.Add(new TextContent(part.Text) + { + RawRepresentation = part, + }); break; case ResponseContentPartKind.Refusal: - results.Add(new ErrorContent(part.Refusal) { ErrorCode = nameof(ResponseContentPartKind.Refusal) }); + results.Add(new ErrorContent(part.Refusal) + { + ErrorCode = nameof(ResponseContentPartKind.Refusal), + RawRepresentation = part, + }); + break; + + default: + results.Add(new() + { + RawRepresentation = part, + }); break; } } @@ -570,6 +618,10 @@ private static List ToOpenAIResponsesContent(IList u.Text))); var createdAt = DateTimeOffset.FromUnixTimeSeconds(1_741_892_091); - Assert.Equal(10, updates.Count); + Assert.Equal(17, updates.Count); + for (int i = 0; i < updates.Count; i++) { Assert.Equal("resp_67d329fbc87c81919f8952fe71dafc96029dabe3ee19bb77", updates[i].ResponseId); Assert.Equal("resp_67d329fbc87c81919f8952fe71dafc96029dabe3ee19bb77", updates[i].ConversationId); Assert.Equal(createdAt, updates[i].CreatedAt); Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); - Assert.Equal(ChatRole.Assistant, updates[i].Role); Assert.Null(updates[i].AdditionalProperties); - Assert.Equal(i == 10 ? 0 : 1, updates[i].Contents.Count); + Assert.Equal((i >= 4 && i <= 12) || i == 16 ? 1 : 0, updates[i].Contents.Count); Assert.Equal(i < updates.Count - 1 ? null : ChatFinishReason.Stop, updates[i].FinishReason); } + for (int i = 4; i < updates.Count; i++) + { + Assert.Equal(ChatRole.Assistant, updates[i].Role); + } + UsageContent usage = updates.SelectMany(u => u.Contents).OfType().Single(); Assert.Equal(26, usage.Details.InputTokenCount); Assert.Equal(10, usage.Details.OutputTokenCount); From 34cdd3a2ddeea9e329356448719ea0d9b896c19c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 9 Jun 2025 12:03:20 -0400 Subject: [PATCH 143/472] Bring back AsIChatClient for OpenAI AssistantClient (#6501) * Bring back AsIChatClient for OpenAI Assistantclient * Address PR feedback --- .../OpenAIAssistantChatClient.cs | 446 ++++++++++++++++++ .../OpenAIChatClient.cs | 13 +- .../OpenAIClientExtensions.cs | 14 + .../OpenAIResponseChatClient.cs | 32 +- .../ChatClientIntegrationTests.cs | 2 +- .../QuantizationEmbeddingGenerator.cs | 2 +- ...enAIAssistantChatClientIntegrationTests.cs | 83 ++++ .../OpenAIAssistantChatClientTests.cs | 77 +++ .../OpenAIResponseClientIntegrationTests.cs | 5 + 9 files changed, 648 insertions(+), 26 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs new file mode 100644 index 00000000000..c3aab83da61 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -0,0 +1,446 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using OpenAI.Assistants; + +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable SA1005 // Single line comments should begin with single space +#pragma warning disable SA1204 // Static elements should appear before instance elements +#pragma warning disable S125 // Sections of code should not be commented out +#pragma warning disable S907 // "goto" statement should not be used +#pragma warning disable S1067 // Expressions should not be too complex +#pragma warning disable S1751 // Loops with at most one iteration should be refactored +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S4456 // Parameter validation in yielding methods should be wrapped +#pragma warning disable S4457 // Parameter validation in "async"/"await" methods should be wrapped + +namespace Microsoft.Extensions.AI; + +/// Represents an for an Azure.AI.Agents.Persistent . +[Experimental("OPENAI001")] +internal sealed partial class OpenAIAssistantChatClient : IChatClient +{ + /// The underlying . + private readonly AssistantClient _client; + + /// Metadata for the client. + private readonly ChatClientMetadata _metadata; + + /// The ID of the agent to use. + private readonly string _assistantId; + + /// The thread ID to use if none is supplied in . + private readonly string? _defaultThreadId; + + /// Initializes a new instance of the class for the specified . + public OpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) + { + _client = Throw.IfNull(assistantClient); + _assistantId = Throw.IfNullOrWhitespace(assistantId); + + _defaultThreadId = defaultThreadId; + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(AssistantClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(assistantClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; + + _metadata = new("openai", providerUrl); + } + + /// + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : + serviceKey is not null ? null : + serviceType == typeof(ChatClientMetadata) ? _metadata : + serviceType == typeof(AssistantClient) ? _client : + serviceType.IsInstanceOfType(this) ? this : + null; + + /// + public Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken); + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(messages); + + // Extract necessary state from messages and options. + (RunCreationOptions runOptions, List? toolResults) = CreateRunOptions(messages, options); + + // Get the thread ID. + string? threadId = options?.ConversationId ?? _defaultThreadId; + if (threadId is null && toolResults is not null) + { + Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results."); + } + + // Get any active run ID for this thread. This is necessary in case a thread has been left with an + // active run, in which all attempts other than submitting tools will fail. We thus need to cancel + // any active run on the thread. + ThreadRun? threadRun = null; + if (threadId is not null) + { + await foreach (var run in _client.GetRunsAsync( + threadId, + new RunCollectionOptions { Order = RunCollectionOrder.Descending, PageSizeLimit = 1 }, + cancellationToken: cancellationToken).ConfigureAwait(false)) + { + if (run.Status != RunStatus.Completed && run.Status != RunStatus.Cancelled && run.Status != RunStatus.Failed && run.Status != RunStatus.Expired) + { + threadRun = run; + } + + break; + } + } + + // Submit the request. + IAsyncEnumerable updates; + if (threadRun is not null && + ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && + toolRunId == threadRun.Id) + { + // There's an active run and we have tool results to submit, so submit the results and continue streaming. + // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs, + // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare. + updates = _client.SubmitToolOutputsToRunStreamingAsync(threadRun.ThreadId, threadRun.Id, toolOutputs, cancellationToken); + } + else + { + if (threadId is null) + { + // No thread ID was provided, so create a new thread. + ThreadCreationOptions threadCreationOptions = new(); + foreach (var message in runOptions.AdditionalMessages) + { + threadCreationOptions.InitialMessages.Add(message); + } + + runOptions.AdditionalMessages.Clear(); + + var thread = await _client.CreateThreadAsync(threadCreationOptions, cancellationToken).ConfigureAwait(false); + threadId = thread.Value.Id; + } + else if (threadRun is not null) + { + // There was an active run; we need to cancel it before starting a new run. + _ = await _client.CancelRunAsync(threadId, threadRun.Id, cancellationToken).ConfigureAwait(false); + threadRun = null; + } + + // Now create a new run and stream the results. + updates = _client.CreateRunStreamingAsync( + threadId: threadId, + _assistantId, + runOptions, + cancellationToken); + } + + // Process each update. + string? responseId = null; + await foreach (var update in updates.ConfigureAwait(false)) + { + switch (update) + { + case ThreadUpdate tu: + threadId ??= tu.Value.Id; + goto default; + + case RunUpdate ru: + threadId ??= ru.Value.ThreadId; + responseId ??= ru.Value.Id; + + ChatResponseUpdate ruUpdate = new() + { + AuthorName = _assistantId, + ConversationId = threadId, + CreatedAt = ru.Value.CreatedAt, + MessageId = responseId, + ModelId = ru.Value.Model, + RawRepresentation = ru, + ResponseId = responseId, + Role = ChatRole.Assistant, + }; + + if (ru.Value.Usage is { } usage) + { + ruUpdate.Contents.Add(new UsageContent(new() + { + InputTokenCount = usage.InputTokenCount, + OutputTokenCount = usage.OutputTokenCount, + TotalTokenCount = usage.TotalTokenCount, + })); + } + + if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName) + { + ruUpdate.Contents.Add( + new FunctionCallContent( + JsonSerializer.Serialize([ru.Value.Id, toolCallId], AssistantJsonContext.Default.StringArray), + functionName, + JsonSerializer.Deserialize(rau.FunctionArguments, AssistantJsonContext.Default.IDictionaryStringObject)!)); + } + + yield return ruUpdate; + break; + + case MessageContentUpdate mcu: + yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) + { + AuthorName = _assistantId, + ConversationId = threadId, + MessageId = responseId, + RawRepresentation = mcu, + ResponseId = responseId, + }; + break; + + default: + yield return new ChatResponseUpdate + { + AuthorName = _assistantId, + ConversationId = threadId, + MessageId = responseId, + RawRepresentation = update, + ResponseId = responseId, + Role = ChatRole.Assistant, + }; + break; + } + } + } + + /// + void IDisposable.Dispose() + { + // nop + } + + /// + /// Creates the to use for the request and extracts any function result contents + /// that need to be submitted as tool results. + /// + private (RunCreationOptions RunOptions, List? ToolResults) CreateRunOptions( + IEnumerable messages, ChatOptions? options) + { + // Create the options instance to populate, either a fresh or using one the caller provides. + RunCreationOptions runOptions = + options?.RawRepresentationFactory?.Invoke(this) as RunCreationOptions ?? + new(); + + // Populate the run options from the ChatOptions, if provided. + if (options is not null) + { + runOptions.MaxOutputTokenCount ??= options.MaxOutputTokens; + runOptions.ModelOverride ??= options.ModelId; + runOptions.NucleusSamplingFactor ??= options.TopP; + runOptions.Temperature ??= options.Temperature; + runOptions.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; + + if (options.Tools is { Count: > 0 } tools) + { + // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. + foreach (AITool tool in tools) + { + switch (tool) + { + case AIFunction aiFunction: + bool? strict = aiFunction.AdditionalProperties.TryGetValue(nameof(strict), out var strictValue) && strictValue is bool strictBool ? + strictBool : + null; + runOptions.ToolsOverride.Add(new FunctionToolDefinition(aiFunction.Name) + { + Description = aiFunction.Description, + Parameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AssistantJsonContext.Default.JsonElement)), + StrictParameterSchemaEnabled = strict, + }); + break; + + case HostedCodeInterpreterTool: + runOptions.ToolsOverride.Add(new CodeInterpreterToolDefinition()); + break; + } + } + } + + // Store the tool mode, if relevant. + if (runOptions.ToolConstraint is null) + { + switch (options.ToolMode) + { + case NoneChatToolMode: + runOptions.ToolConstraint = ToolConstraint.None; + break; + + case null: + case AutoChatToolMode: + runOptions.ToolConstraint = ToolConstraint.Auto; + break; + + case RequiredChatToolMode required when required.RequiredFunctionName is { } functionName: + runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateFunction(functionName)); + break; + + case RequiredChatToolMode required: + runOptions.ToolConstraint = ToolConstraint.Required; + break; + } + } + + // Store the response format, if relevant. + if (runOptions.ResponseFormat is null) + { + switch (options.ResponseFormat) + { + case ChatResponseFormatText: + runOptions.ResponseFormat = AssistantResponseFormat.CreateTextFormat(); + break; + + case ChatResponseFormatJson jsonFormat when jsonFormat.Schema is not null: + runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat( + jsonFormat.SchemaName, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonFormat.Schema, AssistantJsonContext.Default.JsonElement)), + jsonFormat.SchemaDescription); + break; + + case ChatResponseFormatJson jsonFormat: + runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonObjectFormat(); + break; + } + } + } + + // Process ChatMessages. + StringBuilder? instructions = null; + List? functionResults = null; + foreach (var chatMessage in messages) + { + List messageContents = []; + + // Assistants doesn't support system/developer messages directly. It does support transient per-request instructions, + // so we can use the system/developer messages to build up a set of instructions that will be passed to the assistant + // as part of this request. However, in doing so, on a subsequent request that information will be lost, as there's no + // way to store per-thread instructions in the OpenAI Assistants API. We don't want to convert these to user messages, + // however, as that would then expose the system/developer messages in a way that might make the model more likely + // to include that information in its responses. System messages should ideally be instead done as instructions to + // the assistant when the assistant is created. + if (chatMessage.Role == ChatRole.System || + chatMessage.Role == OpenAIResponseChatClient.ChatRoleDeveloper) + { + instructions ??= new(); + foreach (var textContent in chatMessage.Contents.OfType()) + { + _ = instructions.Append(textContent); + } + + continue; + } + + foreach (AIContent content in chatMessage.Contents) + { + switch (content) + { + case TextContent text: + messageContents.Add(MessageContent.FromText(text.Text)); + break; + + case UriContent image when image.HasTopLevelMediaType("image"): + messageContents.Add(MessageContent.FromImageUri(image.Uri)); + break; + + // Assistants doesn't support data URIs. + //case DataContent image when image.HasTopLevelMediaType("image"): + // messageContents.Add(MessageContent.FromImageUri(new Uri(image.Uri))); + // break; + + case FunctionResultContent result: + (functionResults ??= []).Add(result); + break; + + case AIContent when content.RawRepresentation is MessageContent rawRep: + messageContents.Add(rawRep); + break; + } + } + + if (messageContents.Count > 0) + { + runOptions.AdditionalMessages.Add(new ThreadInitializationMessage( + chatMessage.Role == ChatRole.Assistant ? MessageRole.Assistant : MessageRole.User, + messageContents)); + } + } + + if (instructions is not null) + { + runOptions.AdditionalInstructions = instructions.ToString(); + } + + return (runOptions, functionResults); + } + + /// Convert instances to instances. + /// The tool results to process. + /// The generated list of tool outputs, if any could be created. + /// The run ID associated with the corresponding function call requests. + private static string? ConvertFunctionResultsToToolOutput(List? toolResults, out List? toolOutputs) + { + string? runId = null; + toolOutputs = null; + if (toolResults?.Count > 0) + { + foreach (var frc in toolResults) + { + // When creating the FunctionCallContext, we created it with a CallId == [runId, callId]. + // We need to extract the run ID and ensure that the ToolOutput we send back to Azure + // is only the call ID. + string[]? runAndCallIDs; + try + { + runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AssistantJsonContext.Default.StringArray); + } + catch + { + continue; + } + + if (runAndCallIDs is null || + runAndCallIDs.Length != 2 || + string.IsNullOrWhiteSpace(runAndCallIDs[0]) || // run ID + string.IsNullOrWhiteSpace(runAndCallIDs[1]) || // call ID + (runId is not null && runId != runAndCallIDs[0])) + { + continue; + } + + runId = runAndCallIDs[0]; + (toolOutputs ??= []).Add(new(runAndCallIDs[1], frc.Result?.ToString() ?? string.Empty)); + } + } + + return runId; + } + + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(IDictionary))] + private sealed partial class AssistantJsonContext : JsonSerializerContext; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index cd46dae53b0..f97ebd492a7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -34,9 +34,6 @@ internal sealed partial class OpenAIChatClient : IChatClient MoveDefaultKeywordToDescription = true, }); - /// Gets the default OpenAI endpoint. - private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); - /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -57,7 +54,7 @@ public OpenAIChatClient(ChatClient chatClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(ChatClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatClient) as Uri ?? DefaultOpenAIEndpoint; + ?.GetValue(chatClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; string? model = typeof(ChatClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(chatClient) as string; @@ -113,8 +110,6 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } - private static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); - /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, JsonSerializerOptions options) { @@ -125,12 +120,12 @@ void IDisposable.Dispose() { if (input.Role == ChatRole.System || input.Role == ChatRole.User || - input.Role == ChatRoleDeveloper) + input.Role == OpenAIResponseChatClient.ChatRoleDeveloper) { var parts = ToOpenAIChatContent(input.Contents); yield return input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } : - input.Role == ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : + input.Role == OpenAIResponseChatClient.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : new UserChatMessage(parts) { ParticipantName = input.AuthorName }; } else if (input.Role == ChatRole.Tool) @@ -622,7 +617,7 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => ChatMessageRole.User => ChatRole.User, ChatMessageRole.Assistant => ChatRole.Assistant, ChatMessageRole.Tool => ChatRole.Tool, - ChatMessageRole.Developer => ChatRoleDeveloper, + ChatMessageRole.Developer => OpenAIResponseChatClient.ChatRoleDeveloper, _ => new ChatRole(role.ToString()), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 81d2fe55a03..ea43b7e5e31 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using OpenAI; +using OpenAI.Assistants; using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; @@ -25,6 +26,19 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => new OpenAIResponseChatClient(responseClient); + /// Gets an for use with this . + /// The instance to be accessed as an . + /// The unique identifier of the assistant with which to interact. + /// + /// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to + /// or via the + /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request. + /// + /// An instance configured to interact with the specified agent and thread. + [Experimental("OPENAI001")] + public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => + new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); + /// Gets an for use with this . /// The client. /// An that can be used to transcribe audio via the . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index d3caee286be..34e6977e1f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -27,10 +27,10 @@ namespace Microsoft.Extensions.AI; internal sealed partial class OpenAIResponseChatClient : IChatClient { /// Gets the default OpenAI endpoint. - private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); + internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); - /// A for "developer". - private static readonly ChatRole _chatRoleDeveloper = new("developer"); + /// Gets a for "developer". + internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -88,7 +88,7 @@ public async Task GetResponseAsync( // Convert and return the results. ChatResponse response = new() { - ConversationId = openAIResponse.Id, + ConversationId = openAIOptions.StoredOutputEnabled is false ? null : openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), Messages = [new(ChatRole.Assistant, [])], @@ -167,6 +167,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( // Make the call to the OpenAIResponseClient and process the streaming results. DateTimeOffset? createdAt = null; string? responseId = null; + string? conversationId = null; string? modelId = null; string? lastMessageId = null; ChatRole? lastRole = null; @@ -179,18 +180,19 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case StreamingResponseCreatedUpdate createdUpdate: createdAt = createdUpdate.Response.CreatedAt; responseId = createdUpdate.Response.Id; + conversationId = openAIOptions.StoredOutputEnabled is false ? null : responseId; modelId = createdUpdate.Response.Model; goto default; case StreamingResponseCompletedUpdate completedUpdate: yield return new() { + Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], + ConversationId = conversationId, + CreatedAt = createdAt, FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? (functionCallInfos is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop), - Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], - ConversationId = responseId, - CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, RawRepresentation = streamingUpdate, @@ -223,7 +225,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ToChatRole(messageItem?.Role); yield return new ChatResponseUpdate(lastRole, outputTextDeltaUpdate.Delta) { - ConversationId = responseId, + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, @@ -258,7 +260,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ChatRole.Assistant; yield return new ChatResponseUpdate(lastRole, [fci]) { - ConversationId = responseId, + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, @@ -275,7 +277,6 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case StreamingResponseErrorUpdate errorUpdate: yield return new ChatResponseUpdate { - ConversationId = responseId, Contents = [ new ErrorContent(errorUpdate.Message) @@ -284,6 +285,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( Details = errorUpdate.Param, } ], + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, @@ -296,21 +298,21 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case StreamingResponseRefusalDoneUpdate refusalDone: yield return new ChatResponseUpdate { + Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, RawRepresentation = streamingUpdate, ResponseId = responseId, Role = lastRole, - ConversationId = responseId, - Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], }; break; default: yield return new ChatResponseUpdate { - ConversationId = responseId, + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, @@ -334,7 +336,7 @@ private static ChatRole ToChatRole(MessageRole? role) => role switch { MessageRole.System => ChatRole.System, - MessageRole.Developer => _chatRoleDeveloper, + MessageRole.Developer => ChatRoleDeveloper, MessageRole.User => ChatRole.User, _ => ChatRole.Assistant, }; @@ -452,7 +454,7 @@ private static IEnumerable ToOpenAIResponseItems( foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || - input.Role == _chatRoleDeveloper) + input.Role == ChatRoleDeveloper) { string text = input.Text; if (!string.IsNullOrWhiteSpace(text)) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index f28374e7d79..f34f930f3ea 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -618,9 +618,9 @@ public virtual async Task Caching_AfterFunctionInvocation_FunctionOutputUnchange // Second time, the calls to the LLM don't happen, but the function is called again var secondResponse = await chatClient.GetResponseAsync([message]); - Assert.Equal(response.Text, secondResponse.Text); Assert.Equal(2, functionCallCount); Assert.Equal(FunctionInvokingChatClientSetsConversationId ? 3 : 2, llmCallCount!.CallCount); + Assert.Equal(response.Text, secondResponse.Text); } public virtual bool FunctionInvokingChatClientSetsConversationId => false; diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs index ea87408da38..5a7bf0b246e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs @@ -52,7 +52,7 @@ private static BinaryEmbedding QuantizeToBinary(Embedding embedding) { if (vector[i] > 0) { - result[i / 8] = true; + result[i] = true; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs new file mode 100644 index 00000000000..e616d5fb87b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable CA2000 // Dispose objects before losing scope +#pragma warning disable S1135 // Track uses of "TODO" tags +#pragma warning disable xUnit1013 // Public method should be marked as test + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using OpenAI.Assistants; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenAIAssistantChatClientIntegrationTests : ChatClientIntegrationTests +{ + protected override IChatClient? CreateChatClient() + { + var openAIClient = IntegrationTestHelpers.GetOpenAIClient(); + if (openAIClient is null) + { + return null; + } + + AssistantClient ac = openAIClient.GetAssistantClient(); + var assistant = + ac.GetAssistants().FirstOrDefault() ?? + ac.CreateAssistant("gpt-4o-mini"); + + return ac.AsIChatClient(assistant.Id); + } + + public override bool FunctionInvokingChatClientSetsConversationId => true; + + // These tests aren't written in a way that works well with threads. + public override Task Caching_AfterFunctionInvocation_FunctionOutputChangedAsync() => Task.CompletedTask; + public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; + + // Assistants doesn't support data URIs. + public override Task MultiModal_DescribeImage() => Task.CompletedTask; + public override Task MultiModal_DescribePdf() => Task.CompletedTask; + + // [Fact] // uncomment and run to clear out _all_ threads in your OpenAI account + public async Task DeleteAllThreads() + { + using HttpClient client = new(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }); + + // These values need to be filled in. The bearer token needs to be sniffed from a browser + // session interacting with the dashboard (e.g. use F12 networking tools to look at request headers + // made to "https://api.openai.com/v1/threads?limit=10" after clicking on Assistants | Threads in the + // OpenAI portal dashboard). + client.DefaultRequestHeaders.Add("authorization", $"Bearer sess-ENTERYOURSESSIONTOKEN"); + client.DefaultRequestHeaders.Add("openai-organization", "org-ENTERYOURORGID"); + client.DefaultRequestHeaders.Add("openai-project", "proj_ENTERYOURPROJECTID"); + + AssistantClient ac = new AssistantClient(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); + while (true) + { + string listing = await client.GetStringAsync("https://api.openai.com/v1/threads?limit=100"); + + var matches = Regex.Matches(listing, @"thread_\w+"); + if (matches.Count == 0) + { + break; + } + + foreach (Match m in matches) + { + var dr = await ac.DeleteThreadAsync(m.Value); + Assert.True(dr.Value.Deleted); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs new file mode 100644 index 00000000000..6d3a02a08ec --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs @@ -0,0 +1,77 @@ +// 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.ClientModel; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using OpenAI; +using OpenAI.Assistants; +using Xunit; + +#pragma warning disable S103 // Lines should not be too long +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace Microsoft.Extensions.AI; + +public class OpenAIAssistantChatClientTests +{ + [Fact] + public void AsIChatClient_InvalidArgs_Throws() + { + Assert.Throws("assistantClient", () => ((AssistantClient)null!).AsIChatClient("assistantId")); + Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + { + Uri endpoint = new("http://localhost/some/endpoint"); + + var client = useAzureOpenAI ? + new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : + new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + IChatClient[] clients = + [ + client.GetAssistantClient().AsIChatClient("assistantId"), + client.GetAssistantClient().AsIChatClient("assistantId", "threadId"), + ]; + + foreach (var chatClient in clients) + { + var metadata = chatClient.GetService(); + Assert.Equal("openai", metadata?.ProviderName); + Assert.Equal(endpoint, metadata?.ProviderUri); + } + } + + [Fact] + public void GetService_AssistantClient_SuccessfullyReturnsUnderlyingClient() + { + AssistantClient assistantClient = new OpenAIClient("key").GetAssistantClient(); + IChatClient chatClient = assistantClient.AsIChatClient("assistantId"); + + Assert.Same(assistantClient, chatClient.GetService()); + + Assert.Null(chatClient.GetService()); + + using IChatClient pipeline = chatClient + .AsBuilder() + .UseFunctionInvocation() + .UseOpenTelemetry() + .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) + .Build(); + + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + + Assert.Same(assistantClient, pipeline.GetService()); + Assert.IsType(pipeline.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 2c1d6cdc80e..f8e835bdb81 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Tasks; + namespace Microsoft.Extensions.AI; public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests @@ -11,4 +13,7 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests .AsIChatClient(); public override bool FunctionInvokingChatClientSetsConversationId => true; + + // Test structure doesn't make sense with Respones. + public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; } From 6eb7ad82192803af1df30cff91fb802483eeb744 Mon Sep 17 00:00:00 2001 From: Amadeusz Lechniak Date: Tue, 10 Jun 2025 17:09:12 +0200 Subject: [PATCH 144/472] Add resiliency to Resource Monitoring in Linux (#6489) --- .../ITcpStateInfoProvider.cs | 4 +- .../Linux/Disk/DiskStatsReader.cs | 11 +- .../Linux/Disk/IDiskStatsReader.cs | 4 +- .../Linux/Disk/LinuxSystemDiskMetrics.cs | 59 ++++++--- .../Linux/LinuxUtilizationProvider.cs | 14 +-- .../Linux/Log.cs | 21 ++-- .../Linux/Network/LinuxNetworkMetrics.cs | 90 ++++++++++---- .../Linux/Network/LinuxTcpStateInfo.cs | 4 +- .../Log.cs | 11 +- .../ResourceMonitorService.cs | 6 +- .../Windows/Disk/WindowsDiskMetrics.cs | 4 +- .../Windows/Log.cs | 25 ++-- .../Windows/Network/WindowsNetworkMetrics.cs | 4 +- .../Windows/Network/WindowsTcpStateInfo.cs | 4 +- .../WindowsContainerSnapshotProvider.cs | 10 +- .../Windows/WindowsSnapshotProvider.cs | 8 +- .../Linux/Disk/DiskStatsReaderTests.cs | 73 +++++++---- .../Linux/Disk/FakeDiskStatsReader.cs | 6 +- .../Linux/Disk/LinuxSystemDiskMetricsTests.cs | 98 +++++++++++++++ .../Linux/LinuxCountersTests.cs | 4 +- .../Linux/LinuxNetworkMetricsTests.cs | 116 +++++++++++++++++- .../Windows/Tcp6TableInfoTests.cs | 13 +- .../Windows/TcpTableInfoTests.cs | 10 +- 23 files changed, 457 insertions(+), 142 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ITcpStateInfoProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ITcpStateInfoProvider.cs index 0c51e5fc45a..b62d8365d70 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ITcpStateInfoProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ITcpStateInfoProvider.cs @@ -12,11 +12,11 @@ internal interface ITcpStateInfoProvider /// Gets the last known TCP/IP v4 state of the system. ///
    /// An instance of . - TcpStateInfo GetpIpV4TcpStateInfo(); + TcpStateInfo GetIpV4TcpStateInfo(); /// /// Gets the last known TCP/IP v6 state of the system. /// /// An instance of . - TcpStateInfo GetpIpV6TcpStateInfo(); + TcpStateInfo GetIpV6TcpStateInfo(); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs index 11a6f72ccc8..660d5c4e7a1 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/DiskStatsReader.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Linq; using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Pools; @@ -23,7 +24,7 @@ internal sealed class DiskStatsReader(IFileSystem fileSystem) : IDiskStatsReader /// Reads and returns all disk statistics entries. ///
    /// List of . - public List ReadAll() + public DiskStats[] ReadAll(string[] skipDevicePrefixes) { var diskStatsList = new List(); @@ -41,7 +42,11 @@ public List ReadAll() try { DiskStats stat = DiskStatsReader.ParseLine(line); - diskStatsList.Add(stat); + if (!skipDevicePrefixes.Any(prefix => + stat.DeviceName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) + { + diskStatsList.Add(stat); + } } #pragma warning disable CA1031 catch (Exception) @@ -51,7 +56,7 @@ public List ReadAll() } } - return diskStatsList; + return diskStatsList.ToArray(); } /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs index df9d0d7c020..d4087731401 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/IDiskStatsReader.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; - namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; /// @@ -14,5 +12,5 @@ internal interface IDiskStatsReader /// Gets all the disk statistics from the system. /// /// List of instances. - List ReadAll(); + DiskStats[] ReadAll(string[] skipDevicePrefixes); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs index d70a65ed1b0..f967a1b7650 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Disk/LinuxSystemDiskMetrics.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; -using System.Linq; +using System.IO; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -22,14 +23,22 @@ internal sealed class LinuxSystemDiskMetrics private const string DeviceKey = "system.device"; private const string DirectionKey = "disk.io.direction"; + // Exclude devices with these prefixes because they represent virtual, loopback, or device-mapper disks + // that do not correspond to real physical storage. Including them would distort system disk I/O metrics. + private static readonly string[] _skipDevicePrefixes = new[] { "ram", "loop", "dm-" }; private static readonly KeyValuePair _directionReadTag = new(DirectionKey, "read"); private static readonly KeyValuePair _directionWriteTag = new(DirectionKey, "write"); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; private readonly IDiskStatsReader _diskStatsReader; private readonly object _lock = new(); - private readonly Dictionary _baselineDiskStatsDict = []; - private List _diskStatsSnapshot = []; + private readonly FrozenDictionary _baselineDiskStatsDict = FrozenDictionary.Empty; + private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(5); + + private DateTimeOffset _lastDiskStatsFailure = DateTimeOffset.MinValue; + private bool _diskStatsUnavailable; + + private DiskStats[] _diskStatsSnapshot = []; private DateTimeOffset _lastRefreshTime = DateTimeOffset.MinValue; public LinuxSystemDiskMetrics( @@ -48,7 +57,7 @@ public LinuxSystemDiskMetrics( } // We need to read the disk stats once to get the baseline values - _baselineDiskStatsDict = GetAllDiskStats().ToDictionary(d => d.DeviceName); + _baselineDiskStatsDict = GetAllDiskStats().ToFrozenDictionary(d => d.DeviceName); #pragma warning disable CA2000 // Dispose objects before losing scope // We don't dispose the meter because IMeterFactory handles that @@ -85,7 +94,7 @@ public LinuxSystemDiskMetrics( private IEnumerable> GetDiskIoMeasurements() { List> measurements = []; - List diskStatsSnapshot = GetDiskStatsSnapshot(); + DiskStats[] diskStatsSnapshot = GetDiskStatsSnapshot(); foreach (DiskStats diskStats in diskStatsSnapshot) { @@ -102,7 +111,7 @@ private IEnumerable> GetDiskIoMeasurements() private IEnumerable> GetDiskOperationMeasurements() { List> measurements = []; - List diskStatsSnapshot = GetDiskStatsSnapshot(); + DiskStats[] diskStatsSnapshot = GetDiskStatsSnapshot(); foreach (DiskStats diskStats in diskStatsSnapshot) { @@ -119,7 +128,7 @@ private IEnumerable> GetDiskOperationMeasurements() private IEnumerable> GetDiskIoTimeMeasurements() { List> measurements = []; - List diskStatsSnapshot = GetDiskStatsSnapshot(); + DiskStats[] diskStatsSnapshot = GetDiskStatsSnapshot(); foreach (DiskStats diskStats in diskStatsSnapshot) { @@ -131,12 +140,12 @@ private IEnumerable> GetDiskIoTimeMeasurements() return measurements; } - private List GetDiskStatsSnapshot() + private DiskStats[] GetDiskStatsSnapshot() { lock (_lock) { DateTimeOffset now = _timeProvider.GetUtcNow(); - if (_diskStatsSnapshot.Count == 0 || (now - _lastRefreshTime).TotalSeconds > MinimumDiskStatsRefreshIntervalInSeconds) + if (_diskStatsSnapshot.Length == 0 || (now - _lastRefreshTime).TotalSeconds > MinimumDiskStatsRefreshIntervalInSeconds) { _diskStatsSnapshot = GetAllDiskStats(); _lastRefreshTime = now; @@ -146,27 +155,37 @@ private List GetDiskStatsSnapshot() return _diskStatsSnapshot; } - private List GetAllDiskStats() + private DiskStats[] GetAllDiskStats() { + if (_diskStatsUnavailable && + _timeProvider.GetUtcNow() - _lastDiskStatsFailure < _retryInterval) + { + return Array.Empty(); + } + try { - List diskStatsList = _diskStatsReader.ReadAll(); - - // We should not include ram, loop, or dm(device-mapper) devices in the disk stats, should we? - diskStatsList = diskStatsList - .Where(d => !d.DeviceName.StartsWith("ram", StringComparison.OrdinalIgnoreCase) - && !d.DeviceName.StartsWith("loop", StringComparison.OrdinalIgnoreCase) - && !d.DeviceName.StartsWith("dm-", StringComparison.OrdinalIgnoreCase)) - .ToList(); + DiskStats[] diskStatsList = _diskStatsReader.ReadAll(_skipDevicePrefixes); + _diskStatsUnavailable = false; + return diskStatsList; } + catch (Exception ex) when ( + ex is FileNotFoundException || + ex is DirectoryNotFoundException || + ex is UnauthorizedAccessException) + { + _logger.HandleDiskStatsException(ex.Message); + _lastDiskStatsFailure = _timeProvider.GetUtcNow(); + _diskStatsUnavailable = true; + } #pragma warning disable CA1031 catch (Exception ex) #pragma warning restore CA1031 { - Log.HandleDiskStatsException(_logger, ex.Message); + _logger.HandleDiskStatsException(ex.Message); } - return []; + return Array.Empty(); } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index c6dde5c0da1..4090bbb5619 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -112,7 +112,7 @@ public LinuxUtilizationProvider(IOptions options, ILi // _memoryLimit - Resource Memory Limit (in k8s terms) // _memoryLimit - To keep the contract, this parameter will get the Host available memory Resources = new SystemResources(cpuRequest, cpuLimit, _memoryLimit, _memoryLimit); - Log.SystemResourcesInfo(_logger, cpuLimit, cpuRequest, _memoryLimit, _memoryLimit); + _logger.SystemResourcesInfo(cpuLimit, cpuRequest, _memoryLimit, _memoryLimit); } public double CpuUtilizationWithoutHostDelta() @@ -144,7 +144,7 @@ public double CpuUtilizationWithoutHostDelta() { coresUsed = deltaCgroup / (double)deltaCpuPeriodInNanoseconds; - Log.CpuUsageDataV2(_logger, cpuUsageTime, _previousCgroupCpuTime, deltaCpuPeriodInNanoseconds, coresUsed); + _logger.CpuUsageDataV2(cpuUsageTime, _previousCgroupCpuTime, deltaCpuPeriodInNanoseconds, coresUsed); _lastCpuCoresUsed = coresUsed; _refreshAfterCpu = now.Add(_cpuRefreshInterval); @@ -158,7 +158,7 @@ public double CpuUtilizationWithoutHostDelta() { coresUsed = deltaCgroup / actualElapsedNanoseconds; - Log.CpuUsageDataV2(_logger, cpuUsageTime, _previousCgroupCpuTime, actualElapsedNanoseconds, coresUsed); + _logger.CpuUsageDataV2(cpuUsageTime, _previousCgroupCpuTime, actualElapsedNanoseconds, coresUsed); _lastCpuCoresUsed = coresUsed; _refreshAfterCpu = now.Add(_cpuRefreshInterval); @@ -188,7 +188,7 @@ public double CpuUtilizationLimit(float cpuLimit) { _cpuUtilizationLimit100PercentExceededCounter?.Add(1); _cpuUtilizationLimit100PercentExceeded++; - Log.CounterMessage100(_logger, _cpuUtilizationLimit100PercentExceeded); + _logger.CounterMessage100(_cpuUtilizationLimit100PercentExceeded); } // Increment counter if utilization exceeds 110% @@ -196,7 +196,7 @@ public double CpuUtilizationLimit(float cpuLimit) { _cpuUtilizationLimit110PercentExceededCounter?.Add(1); _cpuUtilizationLimit110PercentExceeded++; - Log.CounterMessage110(_logger, _cpuUtilizationLimit110PercentExceeded); + _logger.CounterMessage110(_cpuUtilizationLimit110PercentExceeded); } return utilization; @@ -228,7 +228,7 @@ public double CpuUtilization() { double percentage = Math.Min(One, (double)deltaCgroup / deltaHost); - Log.CpuUsageData(_logger, cgroupCpuTime, hostCpuTime, _previousCgroupCpuTime, _previousHostCpuTime, percentage); + _logger.CpuUsageData(cgroupCpuTime, hostCpuTime, _previousCgroupCpuTime, _previousHostCpuTime, percentage); _cpuPercentage = percentage; _refreshAfterCpu = now.Add(_cpuRefreshInterval); @@ -266,7 +266,7 @@ public double MemoryUtilization() } } - Log.MemoryUsageData(_logger, memoryUsed, _memoryLimit, _memoryPercentage); + _logger.MemoryUsageData(memoryUsed, _memoryLimit, _memoryPercentage); return _memoryPercentage; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs index d2f9c8f5070..b78f64ddfe0 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs @@ -15,7 +15,7 @@ internal static partial class Log "Computed CPU usage with CgroupCpuTime = {cgroupCpuTime}, HostCpuTime = {hostCpuTime}, PreviousCgroupCpuTime = {previousCgroupCpuTime}, PreviousHostCpuTime = {previousHostCpuTime}, CpuPercentage = {cpuPercentage}.")] #pragma warning restore S103 // Lines should not be too long public static partial void CpuUsageData( - ILogger logger, + this ILogger logger, long cgroupCpuTime, long hostCpuTime, long previousCgroupCpuTime, @@ -25,21 +25,26 @@ public static partial void CpuUsageData( [LoggerMessage(2, LogLevel.Debug, "Computed memory usage with MemoryUsedInBytes = {memoryUsed}, MemoryLimit = {memoryLimit}, MemoryPercentage = {memoryPercentage}.")] public static partial void MemoryUsageData( - ILogger logger, + this ILogger logger, ulong memoryUsed, double memoryLimit, double memoryPercentage); [LoggerMessage(3, LogLevel.Debug, "System resources information: CpuLimit = {cpuLimit}, CpuRequest = {cpuRequest}, MemoryLimit = {memoryLimit}, MemoryRequest = {memoryRequest}.")] - public static partial void SystemResourcesInfo(ILogger logger, double cpuLimit, double cpuRequest, ulong memoryLimit, ulong memoryRequest); + public static partial void SystemResourcesInfo( + this ILogger logger, + double cpuLimit, + double cpuRequest, + ulong memoryLimit, + ulong memoryRequest); [LoggerMessage(4, LogLevel.Debug, #pragma warning disable S103 // Lines should not be too long "For CgroupV2, Computed CPU usage with CgroupCpuTime = {cgroupCpuTime}, PreviousCgroupCpuTime = {previousCgroupCpuTime}, ActualElapsedNanoseconds = {actualElapsedNanoseconds}, CpuCores = {cpuCores}.")] #pragma warning restore S103 // Lines should not be too long public static partial void CpuUsageDataV2( - ILogger logger, + this ILogger logger, long cgroupCpuTime, long previousCgroupCpuTime, double actualElapsedNanoseconds, @@ -48,16 +53,18 @@ public static partial void CpuUsageDataV2( [LoggerMessage(5, LogLevel.Debug, "CPU utilization exceeded 100%: Counter = {counterValue}")] public static partial void CounterMessage100( - ILogger logger, + this ILogger logger, long counterValue); [LoggerMessage(6, LogLevel.Debug, "CPU utilization exceeded 110%: Counter = {counterValue}")] public static partial void CounterMessage110( - ILogger logger, + this ILogger logger, long counterValue); [LoggerMessage(7, LogLevel.Warning, "Error while getting disk stats: Error={errorMessage}")] - public static partial void HandleDiskStatsException(ILogger logger, string errorMessage); + public static partial void HandleDiskStatsException( + this ILogger logger, + string errorMessage); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkMetrics.cs index a7e0c6b2303..5add8ccfe75 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkMetrics.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkMetrics.cs @@ -1,9 +1,12 @@ // 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; using System.Diagnostics.Metrics; +using System.IO; +using System.Threading; using Microsoft.Shared.Instruments; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; @@ -11,10 +14,18 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; internal sealed class LinuxNetworkMetrics { private readonly ITcpStateInfoProvider _tcpStateInfoProvider; + private readonly TimeProvider _timeProvider; - public LinuxNetworkMetrics(IMeterFactory meterFactory, ITcpStateInfoProvider tcpStateInfoProvider) + private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(5); + private DateTimeOffset _lastV4Failure = DateTimeOffset.MinValue; + private DateTimeOffset _lastV6Failure = DateTimeOffset.MinValue; + private int _v4Unavailable; + private int _v6Unavailable; + + public LinuxNetworkMetrics(IMeterFactory meterFactory, ITcpStateInfoProvider tcpStateInfoProvider, TimeProvider timeProvider) { _tcpStateInfoProvider = tcpStateInfoProvider; + _timeProvider = timeProvider; #pragma warning disable CA2000 // Dispose objects before losing scope // We don't dispose the meter because IMeterFactory handles that @@ -36,10 +47,9 @@ public LinuxNetworkMetrics(IMeterFactory meterFactory, ITcpStateInfoProvider tcp tags: commonTags); } - private IEnumerable> GetMeasurements() + public IEnumerable> GetMeasurements() { const string NetworkTypeKey = "network.type"; - const string NetworkStateKey = "system.network.state"; // These are covered in https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#attributes: KeyValuePair tcpVersionFourTag = new(NetworkTypeKey, "ipv4"); @@ -48,33 +58,59 @@ private IEnumerable> GetMeasurements() List> measurements = new(24); // IPv4: - TcpStateInfo stateV4 = _tcpStateInfoProvider.GetpIpV4TcpStateInfo(); - measurements.Add(new Measurement(stateV4.ClosedCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "close") })); - measurements.Add(new Measurement(stateV4.ListenCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "listen") })); - measurements.Add(new Measurement(stateV4.SynSentCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "syn_sent") })); - measurements.Add(new Measurement(stateV4.SynRcvdCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "syn_recv") })); - measurements.Add(new Measurement(stateV4.EstabCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "established") })); - measurements.Add(new Measurement(stateV4.FinWait1Count, new TagList { tcpVersionFourTag, new(NetworkStateKey, "fin_wait_1") })); - measurements.Add(new Measurement(stateV4.FinWait2Count, new TagList { tcpVersionFourTag, new(NetworkStateKey, "fin_wait_2") })); - measurements.Add(new Measurement(stateV4.CloseWaitCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "close_wait") })); - measurements.Add(new Measurement(stateV4.ClosingCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "closing") })); - measurements.Add(new Measurement(stateV4.LastAckCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "last_ack") })); - measurements.Add(new Measurement(stateV4.TimeWaitCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "time_wait") })); + TcpStateInfo stateV4 = GetTcpStateInfoWithRetry(_tcpStateInfoProvider.GetIpV4TcpStateInfo, ref _v4Unavailable, ref _lastV4Failure); + CreateMeasurements(tcpVersionFourTag, measurements, stateV4); // IPv6: - TcpStateInfo stateV6 = _tcpStateInfoProvider.GetpIpV6TcpStateInfo(); - measurements.Add(new Measurement(stateV6.ClosedCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "close") })); - measurements.Add(new Measurement(stateV6.ListenCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "listen") })); - measurements.Add(new Measurement(stateV6.SynSentCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "syn_sent") })); - measurements.Add(new Measurement(stateV6.SynRcvdCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "syn_recv") })); - measurements.Add(new Measurement(stateV6.EstabCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "established") })); - measurements.Add(new Measurement(stateV6.FinWait1Count, new TagList { tcpVersionSixTag, new(NetworkStateKey, "fin_wait_1") })); - measurements.Add(new Measurement(stateV6.FinWait2Count, new TagList { tcpVersionSixTag, new(NetworkStateKey, "fin_wait_2") })); - measurements.Add(new Measurement(stateV6.CloseWaitCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "close_wait") })); - measurements.Add(new Measurement(stateV6.ClosingCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "closing") })); - measurements.Add(new Measurement(stateV6.LastAckCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "last_ack") })); - measurements.Add(new Measurement(stateV6.TimeWaitCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "time_wait") })); + TcpStateInfo stateV6 = GetTcpStateInfoWithRetry(_tcpStateInfoProvider.GetIpV6TcpStateInfo, ref _v6Unavailable, ref _lastV6Failure); + CreateMeasurements(tcpVersionSixTag, measurements, stateV6); return measurements; } + + private static void CreateMeasurements(KeyValuePair tcpVersionTag, List> measurements, TcpStateInfo state) + { + const string NetworkStateKey = "system.network.state"; + + measurements.Add(new Measurement(state.ClosedCount, new TagList { tcpVersionTag, new(NetworkStateKey, "close") })); + measurements.Add(new Measurement(state.ListenCount, new TagList { tcpVersionTag, new(NetworkStateKey, "listen") })); + measurements.Add(new Measurement(state.SynSentCount, new TagList { tcpVersionTag, new(NetworkStateKey, "syn_sent") })); + measurements.Add(new Measurement(state.SynRcvdCount, new TagList { tcpVersionTag, new(NetworkStateKey, "syn_recv") })); + measurements.Add(new Measurement(state.EstabCount, new TagList { tcpVersionTag, new(NetworkStateKey, "established") })); + measurements.Add(new Measurement(state.FinWait1Count, new TagList { tcpVersionTag, new(NetworkStateKey, "fin_wait_1") })); + measurements.Add(new Measurement(state.FinWait2Count, new TagList { tcpVersionTag, new(NetworkStateKey, "fin_wait_2") })); + measurements.Add(new Measurement(state.CloseWaitCount, new TagList { tcpVersionTag, new(NetworkStateKey, "close_wait") })); + measurements.Add(new Measurement(state.ClosingCount, new TagList { tcpVersionTag, new(NetworkStateKey, "closing") })); + measurements.Add(new Measurement(state.LastAckCount, new TagList { tcpVersionTag, new(NetworkStateKey, "last_ack") })); + measurements.Add(new Measurement(state.TimeWaitCount, new TagList { tcpVersionTag, new(NetworkStateKey, "time_wait") })); + } + + private TcpStateInfo GetTcpStateInfoWithRetry( + Func getStateInfoFunc, + ref int unavailableFlag, + ref DateTimeOffset lastFailureTime) + { + if (Volatile.Read(ref unavailableFlag) == 0 || _timeProvider.GetUtcNow() - lastFailureTime > _retryInterval) + { + try + { + TcpStateInfo state = getStateInfoFunc(); + _ = Interlocked.Exchange(ref unavailableFlag, 0); + return state; + } + catch (Exception ex) when ( + ex is FileNotFoundException || + ex is DirectoryNotFoundException || + ex is UnauthorizedAccessException) + { + lastFailureTime = _timeProvider.GetUtcNow(); + _ = Interlocked.Exchange(ref unavailableFlag, 1); + return new TcpStateInfo(); + } + } + else + { + return new TcpStateInfo(); + } + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxTcpStateInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxTcpStateInfo.cs index 390bcda64ea..66bc3e1501e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxTcpStateInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxTcpStateInfo.cs @@ -23,13 +23,13 @@ public LinuxTcpStateInfo(IOptions options, LinuxNetwo _parser = parser; } - public TcpStateInfo GetpIpV4TcpStateInfo() + public TcpStateInfo GetIpV4TcpStateInfo() { RefreshSnapshotIfNeeded(); return _iPv4Snapshot; } - public TcpStateInfo GetpIpV6TcpStateInfo() + public TcpStateInfo GetIpV6TcpStateInfo() { RefreshSnapshotIfNeeded(); return _iPv6Snapshot; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Log.cs index a300450a37d..f1d2b620a71 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Log.cs @@ -12,15 +12,20 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; internal static partial class Log { [LoggerMessage(1, LogLevel.Error, "Unable to gather utilization statistics.")] - public static partial void HandledGatherStatisticsException(ILogger logger, Exception e); + public static partial void HandledGatherStatisticsException( + this ILogger logger, + Exception e); [LoggerMessage(2, LogLevel.Error, "Publisher `{Publisher}` was unable to publish utilization statistics.")] - public static partial void HandlePublishUtilizationException(ILogger logger, Exception e, string publisher); + public static partial void HandlePublishUtilizationException( + this ILogger logger, + Exception e, + string publisher); [LoggerMessage(3, LogLevel.Debug, "Snapshot received: TotalTimeSinceStart={totalTimeSinceStart}, KernelTimeSinceStart={kernelTimeSinceStart}, UserTimeSinceStart={userTimeSinceStart}, MemoryUsageInBytes={memoryUsageInBytes}.")] public static partial void SnapshotReceived( - ILogger logger, + this ILogger logger, TimeSpan totalTimeSinceStart, TimeSpan kernelTimeSinceStart, TimeSpan userTimeSinceStart, diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitorService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitorService.cs index d856cedb5ec..ab77a5dfed9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitorService.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitorService.cs @@ -115,7 +115,7 @@ internal async Task PublishUtilizationAsync(CancellationToken cancellationToken) { // By Design: Swallow the exception, as they're non-actionable in this code path. // Prioritize app reliability over error visibility - Log.HandlePublishUtilizationException(_logger, e, publisher.GetType().FullName!); + _logger.HandlePublishUtilizationException(e, publisher.GetType().FullName!); } } } @@ -133,13 +133,13 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) var snapshot = _provider.GetSnapshot(); _snapshotsStore.Add(snapshot); - Log.SnapshotReceived(_logger, snapshot.TotalTimeSinceStart, snapshot.KernelTimeSinceStart, snapshot.UserTimeSinceStart, snapshot.MemoryUsageInBytes); + _logger.SnapshotReceived(snapshot.TotalTimeSinceStart, snapshot.KernelTimeSinceStart, snapshot.UserTimeSinceStart, snapshot.MemoryUsageInBytes); } catch (Exception e) { // By Design: Swallow the exception, as they're non-actionable in this code path. // Prioritize app reliability over error visibility - Log.HandledGatherStatisticsException(_logger, e); + _logger.HandledGatherStatisticsException(e); } await PublishUtilizationAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs index 2927fa657e3..fc6250e80ba 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Disk/WindowsDiskMetrics.cs @@ -99,7 +99,7 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte } catch (Exception ex) { - Log.DiskIoPerfCounterException(_logger, WindowsDiskPerfCounterNames.DiskIdleTimeCounter, ex.Message); + _logger.DiskIoPerfCounterException(WindowsDiskPerfCounterNames.DiskIdleTimeCounter, ex.Message); } // Initialize disk performance counters for "system.disk.io" and "system.disk.operations" metrics @@ -125,7 +125,7 @@ private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounte } catch (Exception ex) { - Log.DiskIoPerfCounterException(_logger, counterName, ex.Message); + _logger.DiskIoPerfCounterException(counterName, ex.Message); } } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs index 3d23f87dff9..fdc0d17fe44 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs @@ -10,14 +10,15 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; internal static partial class Log { [LoggerMessage(1, LogLevel.Information, "Resource Monitoring is running inside a Job Object. For more information about Job Objects see https://aka.ms/job-objects")] - public static partial void RunningInsideJobObject(ILogger logger); + public static partial void RunningInsideJobObject(this ILogger logger); [LoggerMessage(2, LogLevel.Information, "Resource Monitoring is running outside of Job Object. For more information about Job Objects see https://aka.ms/job-objects")] - public static partial void RunningOutsideJobObject(ILogger logger); + public static partial void RunningOutsideJobObject(this ILogger logger); [LoggerMessage(3, LogLevel.Debug, "Computed CPU usage with CpuUsageTicks = {cpuUsageTicks}, OldCpuUsageTicks = {oldCpuUsageTicks}, TimeTickDelta = {timeTickDelta}, CpuUnits = {cpuUnits}, CpuPercentage = {cpuPercentage}.")] - public static partial void CpuUsageData(ILogger logger, + public static partial void CpuUsageData( + this ILogger logger, long cpuUsageTicks, long oldCpuUsageTicks, double timeTickDelta, @@ -26,7 +27,8 @@ public static partial void CpuUsageData(ILogger logger, [LoggerMessage(4, LogLevel.Debug, "Computed memory usage with CurrentMemoryUsage = {currentMemoryUsage}, TotalMemory = {totalMemory}, MemoryPercentage = {memoryPercentage}.")] - public static partial void MemoryUsageData(ILogger logger, + public static partial void MemoryUsageData( + this ILogger logger, ulong currentMemoryUsage, double totalMemory, double memoryPercentage); @@ -34,7 +36,8 @@ public static partial void MemoryUsageData(ILogger logger, #pragma warning disable S103 // Lines should not be too long [LoggerMessage(5, LogLevel.Debug, "Computed CPU usage with CpuUsageKernelTicks = {cpuUsageKernelTicks}, CpuUsageUserTicks = {cpuUsageUserTicks}, OldCpuUsageTicks = {oldCpuUsageTicks}, TimeTickDelta = {timeTickDelta}, CpuUnits = {cpuUnits}, CpuPercentage = {cpuPercentage}.")] #pragma warning restore S103 // Lines should not be too long - public static partial void CpuContainerUsageData(ILogger logger, + public static partial void CpuContainerUsageData( + this ILogger logger, long cpuUsageKernelTicks, long cpuUsageUserTicks, long oldCpuUsageTicks, @@ -44,9 +47,17 @@ public static partial void CpuContainerUsageData(ILogger logger, [LoggerMessage(6, LogLevel.Debug, "System resources information: CpuLimit = {cpuLimit}, CpuRequest = {cpuRequest}, MemoryLimit = {memoryLimit}, MemoryRequest = {memoryRequest}.")] - public static partial void SystemResourcesInfo(ILogger logger, double cpuLimit, double cpuRequest, ulong memoryLimit, ulong memoryRequest); + public static partial void SystemResourcesInfo( + this ILogger logger, + double cpuLimit, + double cpuRequest, + ulong memoryLimit, + ulong memoryRequest); [LoggerMessage(7, LogLevel.Warning, "Error initializing disk io perf counter: PerfCounter={counterName}, Error={errorMessage}")] - public static partial void DiskIoPerfCounterException(ILogger logger, string counterName, string errorMessage); + public static partial void DiskIoPerfCounterException( + this ILogger logger, + string counterName, + string errorMessage); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs index 1acc8d02edd..be3b4e6983f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsNetworkMetrics.cs @@ -48,7 +48,7 @@ private IEnumerable> GetMeasurements() List> measurements = new(24); // IPv4: - TcpStateInfo stateV4 = _tcpStateInfoProvider.GetpIpV4TcpStateInfo(); + TcpStateInfo stateV4 = _tcpStateInfoProvider.GetIpV4TcpStateInfo(); measurements.Add(new Measurement(stateV4.ClosedCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "close") })); measurements.Add(new Measurement(stateV4.ListenCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "listen") })); measurements.Add(new Measurement(stateV4.SynSentCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "syn_sent") })); @@ -63,7 +63,7 @@ private IEnumerable> GetMeasurements() measurements.Add(new Measurement(stateV4.DeleteTcbCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "delete") })); // IPv6: - TcpStateInfo stateV6 = _tcpStateInfoProvider.GetpIpV6TcpStateInfo(); + TcpStateInfo stateV6 = _tcpStateInfoProvider.GetIpV6TcpStateInfo(); measurements.Add(new Measurement(stateV6.ClosedCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "close") })); measurements.Add(new Measurement(stateV6.ListenCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "listen") })); measurements.Add(new Measurement(stateV6.SynSentCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "syn_sent") })); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsTcpStateInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsTcpStateInfo.cs index 732c522cda5..cc0310fd4c8 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsTcpStateInfo.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Network/WindowsTcpStateInfo.cs @@ -42,13 +42,13 @@ public WindowsTcpStateInfo(IOptions options) _refreshAfter = default; } - public TcpStateInfo GetpIpV4TcpStateInfo() + public TcpStateInfo GetIpV4TcpStateInfo() { RefreshSnapshotIfNeeded(); return _iPv4Snapshot; } - public TcpStateInfo GetpIpV6TcpStateInfo() + public TcpStateInfo GetIpV6TcpStateInfo() { RefreshSnapshotIfNeeded(); return _iPv6Snapshot; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 56d8bc2e578..27156ea874e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -73,7 +73,7 @@ internal WindowsContainerSnapshotProvider( ResourceMonitoringOptions options) { _logger = logger ?? NullLogger.Instance; - Log.RunningInsideJobObject(_logger); + _logger.RunningInsideJobObject(); _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; @@ -96,7 +96,7 @@ internal WindowsContainerSnapshotProvider( var cpuRequest = _cpuLimit; var memoryRequest = memoryLimitLong; Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); - Log.SystemResourcesInfo(_logger, _cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); + _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; @@ -205,7 +205,7 @@ private double MemoryPercentage(Func getMemoryUsage) _refreshAfterMemory = now.Add(_memoryRefreshInterval); } - Log.MemoryUsageData(_logger, memoryUsage, _memoryLimit, _memoryPercentage); + _logger.MemoryUsageData(memoryUsage, _memoryLimit, _memoryPercentage); return _memoryPercentage; } @@ -238,8 +238,8 @@ private double CpuPercentage() // Don't change calculation order, otherwise precision is lost: _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / timeTickDelta * _metricValueMultiplier); - Log.CpuContainerUsageData( - _logger, basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, _cpuLimit, _cpuPercentage); + _logger.CpuContainerUsageData( + basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, _cpuLimit, _cpuPercentage); _oldCpuUsageTicks = currentCpuTicks; _oldCpuTimeTicks = now.Ticks; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index 3a20412424c..837cd0f9a06 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -57,7 +57,7 @@ internal WindowsSnapshotProvider( { _logger = logger ?? NullLogger.Instance; - Log.RunningOutsideJobObject(_logger); + _logger.RunningOutsideJobObject(); _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; @@ -68,7 +68,7 @@ internal WindowsSnapshotProvider( // any resource requests or resource limits, therefore using physical values // such as number of CPUs and physical memory and using it for both requests and limits (aka 'guaranteed' and 'max'): Resources = new SystemResources(_cpuUnits, _cpuUnits, totalMemory, totalMemory); - Log.SystemResourcesInfo(_logger, _cpuUnits, _cpuUnits, totalMemory, totalMemory); + _logger.SystemResourcesInfo(_cpuUnits, _cpuUnits, totalMemory, totalMemory); _timeProvider = timeProvider; _getCpuTicksFunc = getCpuTicksFunc; @@ -144,7 +144,7 @@ private double MemoryPercentage() _refreshAfterMemory = now.Add(_memoryRefreshInterval); } - Log.MemoryUsageData(_logger, (ulong)currentMemoryUsage, _totalMemory, _memoryPercentage); + _logger.MemoryUsageData((ulong)currentMemoryUsage, _totalMemory, _memoryPercentage); return _memoryPercentage; } @@ -175,7 +175,7 @@ private double CpuPercentage() // Don't change calculation order, otherwise we loose some precision: _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / (double)timeTickDelta * _metricValueMultiplier); - Log.CpuUsageData(_logger, currentCpuTicks, _oldCpuUsageTicks, timeTickDelta, _cpuUnits, _cpuPercentage); + _logger.CpuUsageData(currentCpuTicks, _oldCpuUsageTicks, timeTickDelta, _cpuUnits, _cpuPercentage); _oldCpuUsageTicks = currentCpuTicks; _oldCpuTimeTicks = now.Ticks; diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs index c5098b2d284..1f7738bb030 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/DiskStatsReaderTests.cs @@ -13,6 +13,8 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public class DiskStatsReaderTests { + private static readonly string[] _skipDevicePrefixes = new[] { "ram", "loop", "dm-" }; + [Fact] public void Test_ReadAll_Valid_DiskStats() { @@ -39,8 +41,17 @@ public void Test_ReadAll_Valid_DiskStats() }); var reader = new DiskStatsReader(fileSystem); - var dictionary = reader.ReadAll().ToDictionary(x => x.DeviceName); - Assert.Equal(15, dictionary.Count); + var dictionary = reader.ReadAll(_skipDevicePrefixes).ToDictionary(x => x.DeviceName); + + var expectedDevices = new[] + { + "nvme1n1", "nvme1n1p1", "nvme0n1", "nvme0n1p1", "nvme0n1p2", "nvme0n1p3", "sda" + }; + Assert.Equal(expectedDevices.Length, dictionary.Count); + foreach (var device in expectedDevices) + { + Assert.True(dictionary.ContainsKey(device), $"Expected device {device} to be present."); + } var disk1 = dictionary["nvme0n1"]; Assert.Equal(6_090_587u, disk1.ReadsCompleted); @@ -61,31 +72,18 @@ public void Test_ReadAll_Valid_DiskStats() Assert.Equal(1_659_742u, disk1.FlushRequestsCompleted); Assert.Equal(515_787u, disk1.TimeFlushingMs); - var disk2 = dictionary["dm-8"]; - Assert.Equal(100_601u, disk2.ReadsCompleted); + var disk2 = dictionary["sda"]; + Assert.Equal(0u, disk2.ReadsCompleted); Assert.Equal(0u, disk2.ReadsMerged); - Assert.Equal(2_990_980u, disk2.SectorsRead); - Assert.Equal(23_940u, disk2.TimeReadingMs); - Assert.Equal(3_097_278u, disk2.WritesCompleted); + Assert.Equal(0u, disk2.SectorsRead); + Assert.Equal(0u, disk2.TimeReadingMs); + Assert.Equal(0u, disk2.WritesCompleted); Assert.Equal(0u, disk2.WritesMerged); - Assert.Equal(32_037_680u, disk2.SectorsWritten); - Assert.Equal(1_410_540u, disk2.TimeWritingMs); + Assert.Equal(0u, disk2.SectorsWritten); + Assert.Equal(0u, disk2.TimeWritingMs); Assert.Equal(0u, disk2.IoInProgress); - Assert.Equal(5_488_608u, disk2.TimeIoMs); - Assert.Equal(1_434_496u, disk2.WeightedTimeIoMs); - - var disk3 = dictionary["sda"]; - Assert.Equal(0u, disk3.ReadsCompleted); - Assert.Equal(0u, disk3.ReadsMerged); - Assert.Equal(0u, disk3.SectorsRead); - Assert.Equal(0u, disk3.TimeReadingMs); - Assert.Equal(0u, disk3.WritesCompleted); - Assert.Equal(0u, disk3.WritesMerged); - Assert.Equal(0u, disk3.SectorsWritten); - Assert.Equal(0u, disk3.TimeWritingMs); - Assert.Equal(0u, disk3.IoInProgress); - Assert.Equal(0u, disk3.TimeIoMs); - Assert.Equal(0u, disk3.WeightedTimeIoMs); + Assert.Equal(0u, disk2.TimeIoMs); + Assert.Equal(0u, disk2.WeightedTimeIoMs); } [Fact] @@ -104,7 +102,7 @@ public void Test_ReadAll_With_Invalid_Lines() }); var reader = new DiskStatsReader(fileSystem); - var dictionary = reader.ReadAll().ToDictionary(x => x.DeviceName); + var dictionary = reader.ReadAll(_skipDevicePrefixes).ToDictionary(x => x.DeviceName); Assert.Equal(3, dictionary.Count); var disk1 = dictionary["nvme1n1"]; @@ -132,4 +130,29 @@ public void Test_ReadAll_With_Invalid_Lines() var disk3 = dictionary["nvme0n1"]; Assert.NotNull(disk3); } + + [Fact] + public void Test_ReadAll_Skips_Prefixes() + { + string diskStatsFileContent = + " 7 0 loop0 100 0 1000 10 1000 0 10000 100 0 1000 100 0 0 0 0 100 100\n" + + " 1 0 ram0 200 0 2000 20 2000 0 20000 200 0 2000 200 0 0 0 0 200 200\n" + + " 259 0 nvme0n1 300 0 3000 30 3000 0 30000 300 0 3000 300 0 0 0 0 300 300\n" + + " 252 0 dm-0 400 0 4000 40 4000 0 40000 400 0 4000 400 0 0 0 0 400 400\n" + + " 8 0 sda 500 0 5000 50 5000 0 50000 500 0 5000 500 0 0 0 0 500 500\n"; + + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/diskstats"), diskStatsFileContent } + }); + + var reader = new DiskStatsReader(fileSystem); + var dictionary = reader.ReadAll(_skipDevicePrefixes).ToDictionary(x => x.DeviceName); + + Assert.DoesNotContain("loop0", dictionary.Keys); + Assert.DoesNotContain("ram0", dictionary.Keys); + Assert.DoesNotContain("dm-0", dictionary.Keys); + Assert.Contains("nvme0n1", dictionary.Keys); + Assert.Contains("sda", dictionary.Keys); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs index 1c69be74709..4b8186c8fb7 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/FakeDiskStatsReader.cs @@ -11,15 +11,15 @@ internal class FakeDiskStatsReader(Dictionary> stats) : { private int _index; - public List ReadAll() + public DiskStats[] ReadAll(string[] skipDevicePrefixes) { if (_index >= stats.Values.First().Count) { throw new InvalidOperationException("No more values available."); } - List list = stats.Values.Select(x => x[_index]).ToList(); + DiskStats[] result = stats.Values.Select(x => x[_index]).ToArray(); _index++; - return list; + return result; } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs index c6aba8e0b53..80ebc818894 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Disk/LinuxSystemDiskMetricsTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Metrics; +using System.IO; using System.Linq; using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; @@ -19,6 +20,7 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public class LinuxSystemDiskMetricsTests { + private static readonly string[] _skipDevicePrefixes = new[] { "ram", "loop", "dm-" }; private readonly FakeLogger _fakeLogger = new(); [Fact] @@ -213,4 +215,100 @@ public void Test_MetricValues() Assert.Equal(4.444, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSda)).Value, 0.01); // (4444 - 0) / 1000 = 4.444 Assert.Equal(3.5, ioTimeMeasurement.Last(x => x.MatchesTags(deviceTagSdb)).Value, 0.01); // (9500 - 6000) / 1000 = 3.5 } + + [Fact] + public void GetAllDiskStats_RetriesAfterFailureInterval() + { + using var meterFactory = new TestMeterFactory(); + var fakeTimeProvider = new FakeTimeProvider(); + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; + + var diskStats = new DiskStats + { + DeviceName = "sda", + SectorsRead = 100, + SectorsWritten = 200, + ReadsCompleted = 10, + WritesCompleted = 20, + TimeIoMs = 1000 + }; + + var diskStatsReaderMock = new Mock(); + diskStatsReaderMock.Setup(r => r.ReadAll(_skipDevicePrefixes)).Throws(); + + var metrics = new LinuxSystemDiskMetrics( + _fakeLogger, + meterFactory, + Options.Options.Create(options), + fakeTimeProvider, + diskStatsReaderMock.Object); + + using var ioCollector = new MetricCollector(meterFactory.Meters.Single(), ResourceUtilizationInstruments.SystemDiskIo); + + ioCollector.RecordObservableInstruments(); + Assert.Empty(ioCollector.GetMeasurementSnapshot()); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Once); + + ioCollector.RecordObservableInstruments(); + Assert.Empty(ioCollector.GetMeasurementSnapshot()); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Once); + + fakeTimeProvider.Advance(TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(1))); + + ioCollector.RecordObservableInstruments(); + Assert.Empty(ioCollector.GetMeasurementSnapshot()); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Exactly(2)); + + diskStatsReaderMock.Reset(); + diskStatsReaderMock.Setup(r => r.ReadAll(_skipDevicePrefixes)).Returns(new DiskStats[] { diskStats }); + + fakeTimeProvider.Advance(TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(1))); + + ioCollector.RecordObservableInstruments(); + var measurements = ioCollector.GetMeasurementSnapshot(); + Assert.NotEmpty(measurements); + Assert.Contains(measurements, m => m.Tags.Any(t => t.Value?.ToString() == "sda")); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Once); + + fakeTimeProvider.Advance(TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(1))); + + ioCollector.RecordObservableInstruments(); + measurements = ioCollector.GetMeasurementSnapshot(); + Assert.NotEmpty(measurements); + Assert.Contains(measurements, m => m.Tags.Any(t => t.Value?.ToString() == "sda")); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Exactly(2)); + } + + [Fact] + public void Metrics_Are_Not_Created_When_ReadAll_Throws_FileNotFoundException() + { + using var meterFactory = new TestMeterFactory(); + var options = new ResourceMonitoringOptions { EnableSystemDiskIoMetrics = true }; + var diskStatsReaderMock = new Mock(); + diskStatsReaderMock.Setup(r => r.ReadAll(_skipDevicePrefixes)).Throws(); + + var fakeTimeProvider = new FakeTimeProvider(); + + _ = new LinuxSystemDiskMetrics( + _fakeLogger, + meterFactory, + Options.Options.Create(options), + fakeTimeProvider, + diskStatsReaderMock.Object); + + Meter meter = meterFactory.Meters.Single(); + + using var diskIoCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskIo); + using var operationCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskOperations); + using var ioTimeCollector = new MetricCollector(meter, ResourceUtilizationInstruments.SystemDiskIoTime); + + diskIoCollector.RecordObservableInstruments(); + operationCollector.RecordObservableInstruments(); + ioTimeCollector.RecordObservableInstruments(); + + Assert.Empty(diskIoCollector.GetMeasurementSnapshot()); + Assert.Empty(operationCollector.GetMeasurementSnapshot()); + Assert.Empty(ioTimeCollector.GetMeasurementSnapshot()); + diskStatsReaderMock.Verify(r => r.ReadAll(_skipDevicePrefixes), Times.Once); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs index e0ff3880075..4f8dbf9547f 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTests.cs @@ -1,12 +1,14 @@ // 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.Metrics; using System.IO; using System.Threading; using FluentAssertions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; +using Microsoft.Extensions.Time.Testing; using Microsoft.Shared.Instruments; using Moq; using Xunit; @@ -85,7 +87,7 @@ public void LinuxNetworkCounters_Registers_Instruments() .Returns(meter); var tcpStateInfo = new LinuxTcpStateInfo(options, parser); - var lnm = new LinuxNetworkMetrics(meterFactoryMock.Object, tcpStateInfo); + var lnm = new LinuxNetworkMetrics(meterFactoryMock.Object, tcpStateInfo, new FakeTimeProvider(DateTimeOffset.UtcNow)); using var listener = new MeterListener { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxNetworkMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxNetworkMetricsTests.cs index 6668ebe811c..40536a3245c 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxNetworkMetricsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxNetworkMetricsTests.cs @@ -1,10 +1,14 @@ // 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.Metrics; +using System.IO; using System.Linq; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; +using Microsoft.Extensions.Time.Testing; using Microsoft.Shared.Instruments; using Microsoft.TestUtilities; using Moq; @@ -15,14 +19,120 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] public class LinuxNetworkMetricsTests { + private readonly Mock _tcpStateInfoProvider = new(); + private readonly DateTimeOffset _startTime = DateTimeOffset.UtcNow; + private FakeTimeProvider _timeProvider; + + public LinuxNetworkMetricsTests() + { + _timeProvider = new FakeTimeProvider(_startTime); + + _tcpStateInfoProvider.Setup(p => p.GetIpV4TcpStateInfo()).Returns(new TcpStateInfo()); + _tcpStateInfoProvider.Setup(p => p.GetIpV6TcpStateInfo()).Returns(new TcpStateInfo()); + } + [Fact] - public void Creates_Meter_With_Correct_Name() + public void CreatesMeter_WithCorrectName() { using var meterFactory = new TestMeterFactory(); - var tcpStateInfoProviderMock = new Mock(); - _ = new LinuxNetworkMetrics(meterFactory, tcpStateInfoProviderMock.Object); + _ = new LinuxNetworkMetrics( + meterFactory, + _tcpStateInfoProvider.Object, + _timeProvider); Meter meter = meterFactory.Meters.Single(); Assert.Equal(ResourceUtilizationInstruments.MeterName, meter.Name); } + + [Fact] + public void GetTcpStateInfoWithRetry_SuccessfulCall_ReturnsState() + { + var expectedV4 = new TcpStateInfo { ClosedCount = 42 }; + var expectedV6 = new TcpStateInfo { EstabCount = 24 }; + _tcpStateInfoProvider.Setup(p => p.GetIpV4TcpStateInfo()).Returns(expectedV4); + _tcpStateInfoProvider.Setup(p => p.GetIpV6TcpStateInfo()).Returns(expectedV6); + + LinuxNetworkMetrics metrics = CreateMetrics(); + List> measurements = metrics.GetMeasurements().ToList(); + + Assert.Contains(measurements, m => HasTagWithValue(m, "network.type", "ipv4", 42)); + Assert.Contains(measurements, m => HasTagWithValue(m, "system.network.state", "close", 42)); + Assert.Contains(measurements, m => HasTagWithValue(m, "network.type", "ipv6", 24)); + Assert.Contains(measurements, m => HasTagWithValue(m, "system.network.state", "established", 24)); + } + + [Theory] + [InlineData(typeof(FileNotFoundException))] + [InlineData(typeof(DirectoryNotFoundException))] + [InlineData(typeof(UnauthorizedAccessException))] + public void GetTcpStateInfoWithRetry_Failure_SetsUnavailableAndReturnsDefault(Type exceptionType) + { + _tcpStateInfoProvider.Setup(p => p.GetIpV4TcpStateInfo()).Throws((Exception)Activator.CreateInstance(exceptionType)!); + + LinuxNetworkMetrics metrics = CreateMetrics(); + List> measurements = metrics.GetMeasurements().ToList(); + + Assert.All(measurements.Take(11), m => Assert.Equal(0, m.Value)); + } + + [Fact] + public void GetTcpStateInfoWithRetry_DuringRetryInterval_ReturnsDefault() + { + _tcpStateInfoProvider.SetupSequence(p => p.GetIpV4TcpStateInfo()) + .Throws(new FileNotFoundException()) + .Returns(new TcpStateInfo { ClosedCount = 123 }); + + LinuxNetworkMetrics metrics = CreateMetrics(); + List> first = metrics.GetMeasurements().ToList(); + + _timeProvider.Advance(TimeSpan.FromMinutes(2)); + List> second = metrics.GetMeasurements().ToList(); + + Assert.All(first.Take(11), m => Assert.Equal(0, m.Value)); + Assert.All(second.Take(11), m => Assert.Equal(0, m.Value)); + _tcpStateInfoProvider.Verify(p => p.GetIpV4TcpStateInfo(), Times.Once); + } + + [Fact] + public void GetTcpStateInfoWithRetry_AfterRetryInterval_ResetsUnavailableOnSuccess() + { + _tcpStateInfoProvider.SetupSequence(p => p.GetIpV4TcpStateInfo()) + .Throws(new FileNotFoundException()) + .Returns(new TcpStateInfo { ClosedCount = 99 }); + + LinuxNetworkMetrics metrics = CreateMetrics(); + List> first = metrics.GetMeasurements().ToList(); + + _timeProvider.Advance(TimeSpan.FromMinutes(6)); + List> second = metrics.GetMeasurements().ToList(); + + Assert.All(first.Take(11), m => Assert.Equal(0, m.Value)); + Assert.Equal(99, second[0].Value); + Assert.Contains(second, m => HasTagWithValue(m, "network.type", "ipv4", 99)); + Assert.Contains(second, m => HasTagWithValue(m, "system.network.state", "close", 99)); + + _tcpStateInfoProvider.Verify(p => p.GetIpV4TcpStateInfo(), Times.Exactly(2)); + } + + private static bool HasTagWithValue(Measurement measurement, string tagKey, string tagValue, long expectedValue) + { + foreach (KeyValuePair tag in measurement.Tags) + { + if (tag.Key == tagKey && string.Equals(tag.Value as string, tagValue, StringComparison.Ordinal)) + { + return measurement.Value == expectedValue; + } + } + + return false; + } + + private LinuxNetworkMetrics CreateMetrics() + { + using var meterFactory = new TestMeterFactory(); + return new LinuxNetworkMetrics( + meterFactory, + _tcpStateInfoProvider.Object, + _timeProvider); + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Tcp6TableInfoTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Tcp6TableInfoTests.cs index 3ef240e1274..a5dfdb5c170 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Tcp6TableInfoTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Tcp6TableInfoTests.cs @@ -234,11 +234,12 @@ public void Test_Tcp6TableInfo_Get_UnsuccessfulStatus_All_The_Time() SourceIpAddresses = new HashSet { "[::1]" }, SamplingInterval = DefaultTimeSpan }; - WindowsTcpStateInfo tcp6TableInfo = new WindowsTcpStateInfo(Options.Options.Create(options)); + + var tcp6TableInfo = new WindowsTcpStateInfo(Options.Options.Create(options)); tcp6TableInfo.SetGetTcp6TableDelegate(FakeGetTcp6TableWithUnsuccessfulStatusAllTheTime); Assert.Throws(() => { - var tcpStateInfo = tcp6TableInfo.GetpIpV6TcpStateInfo(); + TcpStateInfo tcpStateInfo = tcp6TableInfo.GetIpV6TcpStateInfo(); }); } @@ -254,7 +255,7 @@ public void Test_Tcp6TableInfo_Get_InsufficientBuffer_Then_Get_InvalidParameter( tcp6TableInfo.SetGetTcp6TableDelegate(FakeGetTcp6TableWithInsufficientBufferAndInvalidParameter); Assert.Throws(() => { - var tcpStateInfo = tcp6TableInfo.GetpIpV6TcpStateInfo(); + TcpStateInfo tcpStateInfo = tcp6TableInfo.GetIpV6TcpStateInfo(); }); } @@ -270,7 +271,7 @@ public void Test_Tcp6TableInfo_Get_Correct_Information() }; WindowsTcpStateInfo tcp6TableInfo = new WindowsTcpStateInfo(Options.Options.Create(options)); tcp6TableInfo.SetGetTcp6TableDelegate(FakeGetTcp6TableWithFakeInformation); - var tcpStateInfo = tcp6TableInfo.GetpIpV6TcpStateInfo(); + TcpStateInfo tcpStateInfo = tcp6TableInfo.GetIpV6TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(1, tcpStateInfo.ClosedCount); Assert.Equal(1, tcpStateInfo.ListenCount); @@ -286,7 +287,7 @@ public void Test_Tcp6TableInfo_Get_Correct_Information() Assert.Equal(1, tcpStateInfo.DeleteTcbCount); // Second calling in a small interval. - tcpStateInfo = tcp6TableInfo.GetpIpV6TcpStateInfo(); + tcpStateInfo = tcp6TableInfo.GetIpV6TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(1, tcpStateInfo.ClosedCount); Assert.Equal(1, tcpStateInfo.ListenCount); @@ -303,7 +304,7 @@ public void Test_Tcp6TableInfo_Get_Correct_Information() // Third calling in a long interval. Thread.Sleep(6000); - tcpStateInfo = tcp6TableInfo.GetpIpV6TcpStateInfo(); + tcpStateInfo = tcp6TableInfo.GetIpV6TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(2, tcpStateInfo.ClosedCount); Assert.Equal(2, tcpStateInfo.ListenCount); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTests.cs index fb79d0cb839..8c88fc123dd 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTests.cs @@ -181,7 +181,7 @@ public void Test_TcpTableInfo_Get_UnsuccessfulStatus_All_The_Time() tcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithUnsuccessfulStatusAllTheTime); Assert.Throws(() => { - var tcpStateInfo = tcpTableInfo.GetpIpV4TcpStateInfo(); + var tcpStateInfo = tcpTableInfo.GetIpV4TcpStateInfo(); }); } @@ -197,7 +197,7 @@ public void Test_TcpTableInfo_Get_InsufficientBuffer_Then_Get_InvalidParameter() tcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithInsufficientBufferAndInvalidParameter); Assert.Throws(() => { - var tcpStateInfo = tcpTableInfo.GetpIpV4TcpStateInfo(); + var tcpStateInfo = tcpTableInfo.GetIpV4TcpStateInfo(); }); } @@ -213,7 +213,7 @@ public void Test_TcpTableInfo_Get_Correct_Information() }; WindowsTcpStateInfo tcpTableInfo = new WindowsTcpStateInfo(Options.Options.Create(options)); tcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithFakeInformation); - var tcpStateInfo = tcpTableInfo.GetpIpV4TcpStateInfo(); + var tcpStateInfo = tcpTableInfo.GetIpV4TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(1, tcpStateInfo.ClosedCount); Assert.Equal(1, tcpStateInfo.ListenCount); @@ -229,7 +229,7 @@ public void Test_TcpTableInfo_Get_Correct_Information() Assert.Equal(1, tcpStateInfo.DeleteTcbCount); // Second calling in a small interval. - tcpStateInfo = tcpTableInfo.GetpIpV4TcpStateInfo(); + tcpStateInfo = tcpTableInfo.GetIpV4TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(1, tcpStateInfo.ClosedCount); Assert.Equal(1, tcpStateInfo.ListenCount); @@ -246,7 +246,7 @@ public void Test_TcpTableInfo_Get_Correct_Information() // Third calling in a long interval. Thread.Sleep(6000); - tcpStateInfo = tcpTableInfo.GetpIpV4TcpStateInfo(); + tcpStateInfo = tcpTableInfo.GetIpV4TcpStateInfo(); Assert.NotNull(tcpStateInfo); Assert.Equal(2, tcpStateInfo.ClosedCount); Assert.Equal(2, tcpStateInfo.ListenCount); From 0629e19447972896877f703e39b901420e2c4179 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 10 Jun 2025 17:39:25 +0000 Subject: [PATCH 145/472] Merged PR 50714: Port 6500 and 6501 to internal/release/9.6 Automated cherry-picks ---- #### AI description (iteration 1) #### PR Classification This PR implements a new OpenAIAssistantChatClient along with updated streaming response handling and enhanced integration tests. #### PR Summary This update introduces a new chat client and refines conversation management and error handling for OpenAI responses while expanding test coverage. Key changes include: - **`src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs`**: Added a new experimental chat client that integrates with AssistantClient, supports streaming responses, tool result processing, and uses reflection to access internal endpoints. - **`src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs`**: Updated response streaming logic to correctly propagate conversation IDs, attach raw representations, and improve error handling and role mapping. - **`src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs`**: Introduced an extension method to expose AssistantClient as an IChatClient, enabling direct integration with the new chat client. - **Test files under `test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests` and related integration tests**: Added and modified tests to validate the new client functionality and updated response behavior. --- .../OpenAIAssistantChatClient.cs | 446 ++++++++++++++++++ .../OpenAIChatClient.cs | 16 +- .../OpenAIClientExtensions.cs | 14 + .../OpenAIResponseChatClient.cs | 118 +++-- .../ChatClientIntegrationTests.cs | 2 +- .../QuantizationEmbeddingGenerator.cs | 2 +- ...enAIAssistantChatClientIntegrationTests.cs | 83 ++++ .../OpenAIAssistantChatClientTests.cs | 77 +++ .../OpenAIResponseClientIntegrationTests.cs | 5 + .../OpenAIResponseClientTests.cs | 11 +- 10 files changed, 728 insertions(+), 46 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs new file mode 100644 index 00000000000..c3aab83da61 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -0,0 +1,446 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using OpenAI.Assistants; + +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable SA1005 // Single line comments should begin with single space +#pragma warning disable SA1204 // Static elements should appear before instance elements +#pragma warning disable S125 // Sections of code should not be commented out +#pragma warning disable S907 // "goto" statement should not be used +#pragma warning disable S1067 // Expressions should not be too complex +#pragma warning disable S1751 // Loops with at most one iteration should be refactored +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S4456 // Parameter validation in yielding methods should be wrapped +#pragma warning disable S4457 // Parameter validation in "async"/"await" methods should be wrapped + +namespace Microsoft.Extensions.AI; + +/// Represents an for an Azure.AI.Agents.Persistent . +[Experimental("OPENAI001")] +internal sealed partial class OpenAIAssistantChatClient : IChatClient +{ + /// The underlying . + private readonly AssistantClient _client; + + /// Metadata for the client. + private readonly ChatClientMetadata _metadata; + + /// The ID of the agent to use. + private readonly string _assistantId; + + /// The thread ID to use if none is supplied in . + private readonly string? _defaultThreadId; + + /// Initializes a new instance of the class for the specified . + public OpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) + { + _client = Throw.IfNull(assistantClient); + _assistantId = Throw.IfNullOrWhitespace(assistantId); + + _defaultThreadId = defaultThreadId; + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(AssistantClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(assistantClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; + + _metadata = new("openai", providerUrl); + } + + /// + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : + serviceKey is not null ? null : + serviceType == typeof(ChatClientMetadata) ? _metadata : + serviceType == typeof(AssistantClient) ? _client : + serviceType.IsInstanceOfType(this) ? this : + null; + + /// + public Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken); + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(messages); + + // Extract necessary state from messages and options. + (RunCreationOptions runOptions, List? toolResults) = CreateRunOptions(messages, options); + + // Get the thread ID. + string? threadId = options?.ConversationId ?? _defaultThreadId; + if (threadId is null && toolResults is not null) + { + Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results."); + } + + // Get any active run ID for this thread. This is necessary in case a thread has been left with an + // active run, in which all attempts other than submitting tools will fail. We thus need to cancel + // any active run on the thread. + ThreadRun? threadRun = null; + if (threadId is not null) + { + await foreach (var run in _client.GetRunsAsync( + threadId, + new RunCollectionOptions { Order = RunCollectionOrder.Descending, PageSizeLimit = 1 }, + cancellationToken: cancellationToken).ConfigureAwait(false)) + { + if (run.Status != RunStatus.Completed && run.Status != RunStatus.Cancelled && run.Status != RunStatus.Failed && run.Status != RunStatus.Expired) + { + threadRun = run; + } + + break; + } + } + + // Submit the request. + IAsyncEnumerable updates; + if (threadRun is not null && + ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && + toolRunId == threadRun.Id) + { + // There's an active run and we have tool results to submit, so submit the results and continue streaming. + // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs, + // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare. + updates = _client.SubmitToolOutputsToRunStreamingAsync(threadRun.ThreadId, threadRun.Id, toolOutputs, cancellationToken); + } + else + { + if (threadId is null) + { + // No thread ID was provided, so create a new thread. + ThreadCreationOptions threadCreationOptions = new(); + foreach (var message in runOptions.AdditionalMessages) + { + threadCreationOptions.InitialMessages.Add(message); + } + + runOptions.AdditionalMessages.Clear(); + + var thread = await _client.CreateThreadAsync(threadCreationOptions, cancellationToken).ConfigureAwait(false); + threadId = thread.Value.Id; + } + else if (threadRun is not null) + { + // There was an active run; we need to cancel it before starting a new run. + _ = await _client.CancelRunAsync(threadId, threadRun.Id, cancellationToken).ConfigureAwait(false); + threadRun = null; + } + + // Now create a new run and stream the results. + updates = _client.CreateRunStreamingAsync( + threadId: threadId, + _assistantId, + runOptions, + cancellationToken); + } + + // Process each update. + string? responseId = null; + await foreach (var update in updates.ConfigureAwait(false)) + { + switch (update) + { + case ThreadUpdate tu: + threadId ??= tu.Value.Id; + goto default; + + case RunUpdate ru: + threadId ??= ru.Value.ThreadId; + responseId ??= ru.Value.Id; + + ChatResponseUpdate ruUpdate = new() + { + AuthorName = _assistantId, + ConversationId = threadId, + CreatedAt = ru.Value.CreatedAt, + MessageId = responseId, + ModelId = ru.Value.Model, + RawRepresentation = ru, + ResponseId = responseId, + Role = ChatRole.Assistant, + }; + + if (ru.Value.Usage is { } usage) + { + ruUpdate.Contents.Add(new UsageContent(new() + { + InputTokenCount = usage.InputTokenCount, + OutputTokenCount = usage.OutputTokenCount, + TotalTokenCount = usage.TotalTokenCount, + })); + } + + if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName) + { + ruUpdate.Contents.Add( + new FunctionCallContent( + JsonSerializer.Serialize([ru.Value.Id, toolCallId], AssistantJsonContext.Default.StringArray), + functionName, + JsonSerializer.Deserialize(rau.FunctionArguments, AssistantJsonContext.Default.IDictionaryStringObject)!)); + } + + yield return ruUpdate; + break; + + case MessageContentUpdate mcu: + yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) + { + AuthorName = _assistantId, + ConversationId = threadId, + MessageId = responseId, + RawRepresentation = mcu, + ResponseId = responseId, + }; + break; + + default: + yield return new ChatResponseUpdate + { + AuthorName = _assistantId, + ConversationId = threadId, + MessageId = responseId, + RawRepresentation = update, + ResponseId = responseId, + Role = ChatRole.Assistant, + }; + break; + } + } + } + + /// + void IDisposable.Dispose() + { + // nop + } + + /// + /// Creates the to use for the request and extracts any function result contents + /// that need to be submitted as tool results. + /// + private (RunCreationOptions RunOptions, List? ToolResults) CreateRunOptions( + IEnumerable messages, ChatOptions? options) + { + // Create the options instance to populate, either a fresh or using one the caller provides. + RunCreationOptions runOptions = + options?.RawRepresentationFactory?.Invoke(this) as RunCreationOptions ?? + new(); + + // Populate the run options from the ChatOptions, if provided. + if (options is not null) + { + runOptions.MaxOutputTokenCount ??= options.MaxOutputTokens; + runOptions.ModelOverride ??= options.ModelId; + runOptions.NucleusSamplingFactor ??= options.TopP; + runOptions.Temperature ??= options.Temperature; + runOptions.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; + + if (options.Tools is { Count: > 0 } tools) + { + // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. + foreach (AITool tool in tools) + { + switch (tool) + { + case AIFunction aiFunction: + bool? strict = aiFunction.AdditionalProperties.TryGetValue(nameof(strict), out var strictValue) && strictValue is bool strictBool ? + strictBool : + null; + runOptions.ToolsOverride.Add(new FunctionToolDefinition(aiFunction.Name) + { + Description = aiFunction.Description, + Parameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AssistantJsonContext.Default.JsonElement)), + StrictParameterSchemaEnabled = strict, + }); + break; + + case HostedCodeInterpreterTool: + runOptions.ToolsOverride.Add(new CodeInterpreterToolDefinition()); + break; + } + } + } + + // Store the tool mode, if relevant. + if (runOptions.ToolConstraint is null) + { + switch (options.ToolMode) + { + case NoneChatToolMode: + runOptions.ToolConstraint = ToolConstraint.None; + break; + + case null: + case AutoChatToolMode: + runOptions.ToolConstraint = ToolConstraint.Auto; + break; + + case RequiredChatToolMode required when required.RequiredFunctionName is { } functionName: + runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateFunction(functionName)); + break; + + case RequiredChatToolMode required: + runOptions.ToolConstraint = ToolConstraint.Required; + break; + } + } + + // Store the response format, if relevant. + if (runOptions.ResponseFormat is null) + { + switch (options.ResponseFormat) + { + case ChatResponseFormatText: + runOptions.ResponseFormat = AssistantResponseFormat.CreateTextFormat(); + break; + + case ChatResponseFormatJson jsonFormat when jsonFormat.Schema is not null: + runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat( + jsonFormat.SchemaName, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonFormat.Schema, AssistantJsonContext.Default.JsonElement)), + jsonFormat.SchemaDescription); + break; + + case ChatResponseFormatJson jsonFormat: + runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonObjectFormat(); + break; + } + } + } + + // Process ChatMessages. + StringBuilder? instructions = null; + List? functionResults = null; + foreach (var chatMessage in messages) + { + List messageContents = []; + + // Assistants doesn't support system/developer messages directly. It does support transient per-request instructions, + // so we can use the system/developer messages to build up a set of instructions that will be passed to the assistant + // as part of this request. However, in doing so, on a subsequent request that information will be lost, as there's no + // way to store per-thread instructions in the OpenAI Assistants API. We don't want to convert these to user messages, + // however, as that would then expose the system/developer messages in a way that might make the model more likely + // to include that information in its responses. System messages should ideally be instead done as instructions to + // the assistant when the assistant is created. + if (chatMessage.Role == ChatRole.System || + chatMessage.Role == OpenAIResponseChatClient.ChatRoleDeveloper) + { + instructions ??= new(); + foreach (var textContent in chatMessage.Contents.OfType()) + { + _ = instructions.Append(textContent); + } + + continue; + } + + foreach (AIContent content in chatMessage.Contents) + { + switch (content) + { + case TextContent text: + messageContents.Add(MessageContent.FromText(text.Text)); + break; + + case UriContent image when image.HasTopLevelMediaType("image"): + messageContents.Add(MessageContent.FromImageUri(image.Uri)); + break; + + // Assistants doesn't support data URIs. + //case DataContent image when image.HasTopLevelMediaType("image"): + // messageContents.Add(MessageContent.FromImageUri(new Uri(image.Uri))); + // break; + + case FunctionResultContent result: + (functionResults ??= []).Add(result); + break; + + case AIContent when content.RawRepresentation is MessageContent rawRep: + messageContents.Add(rawRep); + break; + } + } + + if (messageContents.Count > 0) + { + runOptions.AdditionalMessages.Add(new ThreadInitializationMessage( + chatMessage.Role == ChatRole.Assistant ? MessageRole.Assistant : MessageRole.User, + messageContents)); + } + } + + if (instructions is not null) + { + runOptions.AdditionalInstructions = instructions.ToString(); + } + + return (runOptions, functionResults); + } + + /// Convert instances to instances. + /// The tool results to process. + /// The generated list of tool outputs, if any could be created. + /// The run ID associated with the corresponding function call requests. + private static string? ConvertFunctionResultsToToolOutput(List? toolResults, out List? toolOutputs) + { + string? runId = null; + toolOutputs = null; + if (toolResults?.Count > 0) + { + foreach (var frc in toolResults) + { + // When creating the FunctionCallContext, we created it with a CallId == [runId, callId]. + // We need to extract the run ID and ensure that the ToolOutput we send back to Azure + // is only the call ID. + string[]? runAndCallIDs; + try + { + runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AssistantJsonContext.Default.StringArray); + } + catch + { + continue; + } + + if (runAndCallIDs is null || + runAndCallIDs.Length != 2 || + string.IsNullOrWhiteSpace(runAndCallIDs[0]) || // run ID + string.IsNullOrWhiteSpace(runAndCallIDs[1]) || // call ID + (runId is not null && runId != runAndCallIDs[0])) + { + continue; + } + + runId = runAndCallIDs[0]; + (toolOutputs ??= []).Add(new(runAndCallIDs[1], frc.Result?.ToString() ?? string.Empty)); + } + } + + return runId; + } + + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(IDictionary))] + private sealed partial class AssistantJsonContext : JsonSerializerContext; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 001f4d1a593..f97ebd492a7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -34,9 +34,6 @@ internal sealed partial class OpenAIChatClient : IChatClient MoveDefaultKeywordToDescription = true, }); - /// Gets the default OpenAI endpoint. - private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); - /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -57,7 +54,7 @@ public OpenAIChatClient(ChatClient chatClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(ChatClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatClient) as Uri ?? DefaultOpenAIEndpoint; + ?.GetValue(chatClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; string? model = typeof(ChatClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(chatClient) as string; @@ -113,8 +110,6 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } - private static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); - /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, JsonSerializerOptions options) { @@ -125,12 +120,12 @@ void IDisposable.Dispose() { if (input.Role == ChatRole.System || input.Role == ChatRole.User || - input.Role == ChatRoleDeveloper) + input.Role == OpenAIResponseChatClient.ChatRoleDeveloper) { var parts = ToOpenAIChatContent(input.Contents); yield return input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } : - input.Role == ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : + input.Role == OpenAIResponseChatClient.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : new UserChatMessage(parts) { ParticipantName = input.AuthorName }; } else if (input.Role == ChatRole.Tool) @@ -261,6 +256,9 @@ private static List ToOpenAIChatContent(IList case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"); + + case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: + return rawContentPart; } return null; @@ -619,7 +617,7 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => ChatMessageRole.User => ChatRole.User, ChatMessageRole.Assistant => ChatRole.Assistant, ChatMessageRole.Tool => ChatRole.Tool, - ChatMessageRole.Developer => ChatRoleDeveloper, + ChatMessageRole.Developer => OpenAIResponseChatClient.ChatRoleDeveloper, _ => new ChatRole(role.ToString()), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 81d2fe55a03..ea43b7e5e31 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using OpenAI; +using OpenAI.Assistants; using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; @@ -25,6 +26,19 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => new OpenAIResponseChatClient(responseClient); + /// Gets an for use with this . + /// The instance to be accessed as an . + /// The unique identifier of the assistant with which to interact. + /// + /// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to + /// or via the + /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request. + /// + /// An instance configured to interact with the specified agent and thread. + [Experimental("OPENAI001")] + public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => + new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); + /// Gets an for use with this . /// The client. /// An that can be used to transcribe audio via the . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 25035e7e225..34e6977e1f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -15,6 +15,7 @@ using OpenAI.Responses; using static Microsoft.Extensions.AI.OpenAIChatClient; +#pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable S3604 // Member initializer values should not be redundant @@ -26,10 +27,10 @@ namespace Microsoft.Extensions.AI; internal sealed partial class OpenAIResponseChatClient : IChatClient { /// Gets the default OpenAI endpoint. - private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); + internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); - /// A for "developer". - private static readonly ChatRole _chatRoleDeveloper = new("developer"); + /// Gets a for "developer". + internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -87,12 +88,13 @@ public async Task GetResponseAsync( // Convert and return the results. ChatResponse response = new() { - ResponseId = openAIResponse.Id, - ConversationId = openAIResponse.Id, + ConversationId = openAIOptions.StoredOutputEnabled is false ? null : openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), Messages = [new(ChatRole.Assistant, [])], ModelId = openAIResponse.Model, + RawRepresentation = openAIResponse, + ResponseId = openAIResponse.Id, Usage = ToUsageDetails(openAIResponse), }; @@ -125,12 +127,20 @@ public async Task GetResponseAsync( case FunctionCallResponseItem functionCall: response.FinishReason ??= ChatFinishReason.ToolCalls; - message.Contents.Add( - FunctionCallContent.CreateFromParsedArguments( - functionCall.FunctionArguments.ToMemory(), - functionCall.CallId, - functionCall.FunctionName, - static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!)); + var fcc = FunctionCallContent.CreateFromParsedArguments( + functionCall.FunctionArguments.ToMemory(), + functionCall.CallId, + functionCall.FunctionName, + static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!); + fcc.RawRepresentation = outputItem; + message.Contents.Add(fcc); + break; + + default: + message.Contents.Add(new() + { + RawRepresentation = outputItem, + }); break; } } @@ -157,6 +167,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( // Make the call to the OpenAIResponseClient and process the streaming results. DateTimeOffset? createdAt = null; string? responseId = null; + string? conversationId = null; string? modelId = null; string? lastMessageId = null; ChatRole? lastRole = null; @@ -169,21 +180,23 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case StreamingResponseCreatedUpdate createdUpdate: createdAt = createdUpdate.Response.CreatedAt; responseId = createdUpdate.Response.Id; + conversationId = openAIOptions.StoredOutputEnabled is false ? null : responseId; modelId = createdUpdate.Response.Model; - break; + goto default; case StreamingResponseCompletedUpdate completedUpdate: yield return new() { Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], + ConversationId = conversationId, CreatedAt = createdAt, - ResponseId = responseId, - ConversationId = responseId, FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? (functionCallInfos is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop), MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, Role = lastRole, }; break; @@ -200,11 +213,11 @@ public async IAsyncEnumerable GetStreamingResponseAsync( break; } - break; + goto default; case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate: _ = outputIndexToMessages.Remove(outputItemDoneUpdate.OutputIndex); - break; + goto default; case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate: _ = outputIndexToMessages.TryGetValue(outputTextDeltaUpdate.OutputIndex, out MessageResponseItem? messageItem); @@ -212,11 +225,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ToChatRole(messageItem?.Role); yield return new ChatResponseUpdate(lastRole, outputTextDeltaUpdate.Delta) { + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, - ConversationId = responseId, }; break; @@ -227,7 +241,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( _ = (callInfo.Arguments ??= new()).Append(functionCallArgumentsDeltaUpdate.Delta); } - break; + goto default; } case StreamingResponseFunctionCallArgumentsDoneUpdate functionCallOutputDoneUpdate: @@ -246,26 +260,23 @@ public async IAsyncEnumerable GetStreamingResponseAsync( lastRole = ChatRole.Assistant; yield return new ChatResponseUpdate(lastRole, [fci]) { + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, - ConversationId = responseId, }; + + break; } - break; + goto default; } case StreamingResponseErrorUpdate errorUpdate: yield return new ChatResponseUpdate { - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - ResponseId = responseId, - Role = lastRole, - ConversationId = responseId, Contents = [ new ErrorContent(errorUpdate.Message) @@ -274,19 +285,40 @@ public async IAsyncEnumerable GetStreamingResponseAsync( Details = errorUpdate.Param, } ], + ConversationId = conversationId, + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, + Role = lastRole, }; break; case StreamingResponseRefusalDoneUpdate refusalDone: yield return new ChatResponseUpdate { + Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], + ConversationId = conversationId, CreatedAt = createdAt, MessageId = lastMessageId, ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, + Role = lastRole, + }; + break; + + default: + yield return new ChatResponseUpdate + { + ConversationId = conversationId, + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + RawRepresentation = streamingUpdate, ResponseId = responseId, Role = lastRole, - ConversationId = responseId, - Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], }; break; } @@ -304,7 +336,7 @@ private static ChatRole ToChatRole(MessageRole? role) => role switch { MessageRole.System => ChatRole.System, - MessageRole.Developer => _chatRoleDeveloper, + MessageRole.Developer => ChatRoleDeveloper, MessageRole.User => ChatRole.User, _ => ChatRole.Assistant, }; @@ -422,7 +454,7 @@ private static IEnumerable ToOpenAIResponseItems( foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || - input.Role == _chatRoleDeveloper) + input.Role == ChatRoleDeveloper) { string text = input.Text; if (!string.IsNullOrWhiteSpace(text)) @@ -487,6 +519,10 @@ private static IEnumerable ToOpenAIResponseItems( callContent.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; + + case AIContent when item.RawRepresentation is ResponseItem rawRep: + yield return rawRep; + break; } } @@ -530,11 +566,25 @@ private static List ToAIContents(IEnumerable con switch (part.Kind) { case ResponseContentPartKind.OutputText: - results.Add(new TextContent(part.Text)); + results.Add(new TextContent(part.Text) + { + RawRepresentation = part, + }); break; case ResponseContentPartKind.Refusal: - results.Add(new ErrorContent(part.Refusal) { ErrorCode = nameof(ResponseContentPartKind.Refusal) }); + results.Add(new ErrorContent(part.Refusal) + { + ErrorCode = nameof(ResponseContentPartKind.Refusal), + RawRepresentation = part, + }); + break; + + default: + results.Add(new() + { + RawRepresentation = part, + }); break; } } @@ -570,6 +620,10 @@ private static List ToOpenAIResponsesContent(IList false; diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs index ea87408da38..5a7bf0b246e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/QuantizationEmbeddingGenerator.cs @@ -52,7 +52,7 @@ private static BinaryEmbedding QuantizeToBinary(Embedding embedding) { if (vector[i] > 0) { - result[i / 8] = true; + result[i] = true; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs new file mode 100644 index 00000000000..e616d5fb87b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable CA2000 // Dispose objects before losing scope +#pragma warning disable S1135 // Track uses of "TODO" tags +#pragma warning disable xUnit1013 // Public method should be marked as test + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using OpenAI.Assistants; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenAIAssistantChatClientIntegrationTests : ChatClientIntegrationTests +{ + protected override IChatClient? CreateChatClient() + { + var openAIClient = IntegrationTestHelpers.GetOpenAIClient(); + if (openAIClient is null) + { + return null; + } + + AssistantClient ac = openAIClient.GetAssistantClient(); + var assistant = + ac.GetAssistants().FirstOrDefault() ?? + ac.CreateAssistant("gpt-4o-mini"); + + return ac.AsIChatClient(assistant.Id); + } + + public override bool FunctionInvokingChatClientSetsConversationId => true; + + // These tests aren't written in a way that works well with threads. + public override Task Caching_AfterFunctionInvocation_FunctionOutputChangedAsync() => Task.CompletedTask; + public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; + + // Assistants doesn't support data URIs. + public override Task MultiModal_DescribeImage() => Task.CompletedTask; + public override Task MultiModal_DescribePdf() => Task.CompletedTask; + + // [Fact] // uncomment and run to clear out _all_ threads in your OpenAI account + public async Task DeleteAllThreads() + { + using HttpClient client = new(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }); + + // These values need to be filled in. The bearer token needs to be sniffed from a browser + // session interacting with the dashboard (e.g. use F12 networking tools to look at request headers + // made to "https://api.openai.com/v1/threads?limit=10" after clicking on Assistants | Threads in the + // OpenAI portal dashboard). + client.DefaultRequestHeaders.Add("authorization", $"Bearer sess-ENTERYOURSESSIONTOKEN"); + client.DefaultRequestHeaders.Add("openai-organization", "org-ENTERYOURORGID"); + client.DefaultRequestHeaders.Add("openai-project", "proj_ENTERYOURPROJECTID"); + + AssistantClient ac = new AssistantClient(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); + while (true) + { + string listing = await client.GetStringAsync("https://api.openai.com/v1/threads?limit=100"); + + var matches = Regex.Matches(listing, @"thread_\w+"); + if (matches.Count == 0) + { + break; + } + + foreach (Match m in matches) + { + var dr = await ac.DeleteThreadAsync(m.Value); + Assert.True(dr.Value.Deleted); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs new file mode 100644 index 00000000000..6d3a02a08ec --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs @@ -0,0 +1,77 @@ +// 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.ClientModel; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using OpenAI; +using OpenAI.Assistants; +using Xunit; + +#pragma warning disable S103 // Lines should not be too long +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace Microsoft.Extensions.AI; + +public class OpenAIAssistantChatClientTests +{ + [Fact] + public void AsIChatClient_InvalidArgs_Throws() + { + Assert.Throws("assistantClient", () => ((AssistantClient)null!).AsIChatClient("assistantId")); + Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + { + Uri endpoint = new("http://localhost/some/endpoint"); + + var client = useAzureOpenAI ? + new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : + new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + IChatClient[] clients = + [ + client.GetAssistantClient().AsIChatClient("assistantId"), + client.GetAssistantClient().AsIChatClient("assistantId", "threadId"), + ]; + + foreach (var chatClient in clients) + { + var metadata = chatClient.GetService(); + Assert.Equal("openai", metadata?.ProviderName); + Assert.Equal(endpoint, metadata?.ProviderUri); + } + } + + [Fact] + public void GetService_AssistantClient_SuccessfullyReturnsUnderlyingClient() + { + AssistantClient assistantClient = new OpenAIClient("key").GetAssistantClient(); + IChatClient chatClient = assistantClient.AsIChatClient("assistantId"); + + Assert.Same(assistantClient, chatClient.GetService()); + + Assert.Null(chatClient.GetService()); + + using IChatClient pipeline = chatClient + .AsBuilder() + .UseFunctionInvocation() + .UseOpenTelemetry() + .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) + .Build(); + + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + Assert.NotNull(pipeline.GetService()); + + Assert.Same(assistantClient, pipeline.GetService()); + Assert.IsType(pipeline.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 2c1d6cdc80e..f8e835bdb81 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Tasks; + namespace Microsoft.Extensions.AI; public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests @@ -11,4 +13,7 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests .AsIChatClient(); public override bool FunctionInvokingChatClientSetsConversationId => true; + + // Test structure doesn't make sense with Respones. + public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 3747b79dc88..8b27cd918a7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -264,19 +264,24 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal("Hello! How can I assist you today?", string.Concat(updates.Select(u => u.Text))); var createdAt = DateTimeOffset.FromUnixTimeSeconds(1_741_892_091); - Assert.Equal(10, updates.Count); + Assert.Equal(17, updates.Count); + for (int i = 0; i < updates.Count; i++) { Assert.Equal("resp_67d329fbc87c81919f8952fe71dafc96029dabe3ee19bb77", updates[i].ResponseId); Assert.Equal("resp_67d329fbc87c81919f8952fe71dafc96029dabe3ee19bb77", updates[i].ConversationId); Assert.Equal(createdAt, updates[i].CreatedAt); Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); - Assert.Equal(ChatRole.Assistant, updates[i].Role); Assert.Null(updates[i].AdditionalProperties); - Assert.Equal(i == 10 ? 0 : 1, updates[i].Contents.Count); + Assert.Equal((i >= 4 && i <= 12) || i == 16 ? 1 : 0, updates[i].Contents.Count); Assert.Equal(i < updates.Count - 1 ? null : ChatFinishReason.Stop, updates[i].FinishReason); } + for (int i = 4; i < updates.Count; i++) + { + Assert.Equal(ChatRole.Assistant, updates[i].Role); + } + UsageContent usage = updates.SelectMany(u => u.Contents).OfType().Single(); Assert.Equal(26, usage.Details.InputTokenCount); Assert.Equal(10, usage.Details.OutputTokenCount); From 2481e9e72251b7b5658a32c7e2ad4cc1d4c3bf8d Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 10 Jun 2025 15:49:15 -0700 Subject: [PATCH 146/472] Fix template tests --- src/ProjectTemplates/GeneratedContent.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index c3287408034..4f3e918a77f 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -20,9 +20,9 @@ - Use specific version numbers to pin to already-released packages --> - $(Version) - $(Version) - $(Version) + 9.6.0 + 9.6.0-preview.1.25310.2 + 9.6.0 From 4bea22376370f25ae6e5041f4638e49fd1de821a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 12 Jun 2025 11:27:35 -0400 Subject: [PATCH 147/472] Workaround OpenAI assistant's RunCreationOption's tools override (#6512) In OpenAIAssistantChatClient, as part of providing ChatOptions.Tools to the service, the only API available to do so is ToolsOverride, which ends up replacing rather than augmenting any tools defined at the assistant level. To work around that, this changes the implementation to retrieve the assistant's tools and send them as part of the override, effectively implementing a merge rather than override. --- .../OpenAIAssistantChatClient.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index c3aab83da61..f3717f1ee41 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -44,6 +44,9 @@ internal sealed partial class OpenAIAssistantChatClient : IChatClient /// The thread ID to use if none is supplied in . private readonly string? _defaultThreadId; + /// List of tools associated with the assistant. + private IReadOnlyList? _assistantTools; + /// Initializes a new instance of the class for the specified . public OpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) { @@ -83,7 +86,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( _ = Throw.IfNull(messages); // Extract necessary state from messages and options. - (RunCreationOptions runOptions, List? toolResults) = CreateRunOptions(messages, options); + (RunCreationOptions runOptions, List? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false); // Get the thread ID. string? threadId = options?.ConversationId ?? _defaultThreadId; @@ -238,8 +241,8 @@ void IDisposable.Dispose() /// Creates the to use for the request and extracts any function result contents /// that need to be submitted as tool results. /// - private (RunCreationOptions RunOptions, List? ToolResults) CreateRunOptions( - IEnumerable messages, ChatOptions? options) + private async ValueTask<(RunCreationOptions RunOptions, List? ToolResults)> CreateRunOptionsAsync( + IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken) { // Create the options instance to populate, either a fresh or using one the caller provides. RunCreationOptions runOptions = @@ -257,6 +260,24 @@ void IDisposable.Dispose() if (options.Tools is { Count: > 0 } tools) { + // If the caller has provided any tool overrides, we'll assume they don't want to use the assistant's tools. + // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to + // just add them. To handle that, we'll get all of the assistant's tools and add them to the override list + // along with our tools. + if (runOptions.ToolsOverride.Count == 0) + { + if (_assistantTools is null) + { + var assistant = await _client.GetAssistantAsync(_assistantId, cancellationToken).ConfigureAwait(false); + _assistantTools = assistant.Value.Tools; + } + + foreach (var tool in _assistantTools) + { + runOptions.ToolsOverride.Add(tool); + } + } + // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. foreach (AITool tool in tools) { @@ -290,7 +311,6 @@ void IDisposable.Dispose() runOptions.ToolConstraint = ToolConstraint.None; break; - case null: case AutoChatToolMode: runOptions.ToolConstraint = ToolConstraint.Auto; break; From 0204116f1aecc9308691d8a3bb6342d70a787e19 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 12 Jun 2025 17:24:33 -0400 Subject: [PATCH 148/472] Add ChatOptions.Instructions (#6505) More services (especially those focused on agents) are starting to support the notion of per-request instructions, effectively system messages that aren't stored as part of a persisted chat history. For existing stateless services that don't have their own notion of separate instructions, we can just translate instructions into a system message. --- .../ChatCompletion/ChatOptions.cs | 22 ++++++++------ .../Microsoft.Extensions.AI.Abstractions.json | 6 +++- .../AzureAIInferenceChatClient.cs | 11 +++++-- .../OllamaChatClient.cs | 12 +++++++- .../OllamaChatRequest.cs | 2 +- .../OpenAIAssistantChatClient.cs | 29 ++++++++++++++----- .../OpenAIChatClient.cs | 15 ++++++---- .../OpenAIResponseChatClient.cs | 6 ++++ .../ChatCompletion/ChatOptionsTests.cs | 6 ++++ 9 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 96a6c6cd36b..0be912430fa 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -15,6 +15,9 @@ public class ChatOptions /// Stateless vs. stateful clients. public string? ConversationId { get; set; } + /// Gets or sets additional per-request instructions to be provided to the . + public string? Instructions { get; set; } + /// Gets or sets the temperature for generating chat responses. /// /// This value controls the randomness of predictions made by the model. Use a lower value to decrease randomness in the response. @@ -146,20 +149,21 @@ public virtual ChatOptions Clone() { ChatOptions options = new() { + AdditionalProperties = AdditionalProperties?.Clone(), + AllowMultipleToolCalls = AllowMultipleToolCalls, ConversationId = ConversationId, - Temperature = Temperature, - MaxOutputTokens = MaxOutputTokens, - TopP = TopP, - TopK = TopK, FrequencyPenalty = FrequencyPenalty, + Instructions = Instructions, + MaxOutputTokens = MaxOutputTokens, + ModelId = ModelId, PresencePenalty = PresencePenalty, - Seed = Seed, + RawRepresentationFactory = RawRepresentationFactory, ResponseFormat = ResponseFormat, - ModelId = ModelId, - AllowMultipleToolCalls = AllowMultipleToolCalls, + Seed = Seed, + Temperature = Temperature, ToolMode = ToolMode, - RawRepresentationFactory = RawRepresentationFactory, - AdditionalProperties = AdditionalProperties?.Clone(), + TopK = TopK, + TopP = TopP, }; if (StopSequences is not null) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 499ff4b4a71..79776b0ecb4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -923,6 +923,10 @@ "Member": "string? Microsoft.Extensions.AI.ChatOptions.ConversationId { get; set; }", "Stage": "Stable" }, + { + "Member": "string? Microsoft.Extensions.AI.ChatOptions.Instructions { get; set; }", + "Stage": "Stable" + }, { "Member": "float? Microsoft.Extensions.AI.ChatOptions.FrequencyPenalty { get; set; }", "Stage": "Stable" @@ -2286,4 +2290,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 5c6b64b138f..0f8a8a90008 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -283,7 +283,7 @@ private static ChatRole ToChatRole(global::Azure.AI.Inference.ChatRole role) => new(s); private ChatCompletionsOptions CreateAzureAIOptions(IEnumerable chatContents, ChatOptions? options) => - new(ToAzureAIInferenceChatMessages(chatContents)) + new(ToAzureAIInferenceChatMessages(chatContents, options)) { Model = options?.ModelId ?? _metadata.DefaultModelId ?? throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.") @@ -299,7 +299,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon if (options.RawRepresentationFactory?.Invoke(this) is ChatCompletionsOptions result) { - result.Messages = ToAzureAIInferenceChatMessages(chatContents).ToList(); + result.Messages = ToAzureAIInferenceChatMessages(chatContents, options).ToList(); result.Model ??= options.ModelId ?? _metadata.DefaultModelId ?? throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options."); } @@ -422,11 +422,16 @@ private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunc } /// Converts an Extensions chat message enumerable to an AzureAI chat message enumerable. - private static IEnumerable ToAzureAIInferenceChatMessages(IEnumerable inputs) + private static IEnumerable ToAzureAIInferenceChatMessages(IEnumerable inputs, ChatOptions? options) { // Maps all of the M.E.AI types to the corresponding AzureAI types. // Unrecognized or non-processable content is ignored. + if (options?.Instructions is { } instructions && !string.IsNullOrWhiteSpace(instructions)) + { + yield return new ChatRequestSystemMessage(instructions); + } + foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System) diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs index 28f8eb8c3ad..42f75af495e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs @@ -307,10 +307,20 @@ private static FunctionCallContent ToFunctionCallContent(OllamaFunctionToolCall private OllamaChatRequest ToOllamaChatRequest(IEnumerable messages, ChatOptions? options, bool stream) { + var requestMessages = messages.SelectMany(ToOllamaChatRequestMessages).ToList(); + if (options?.Instructions is string instructions) + { + requestMessages.Insert(0, new OllamaChatRequestMessage + { + Role = ChatRole.System.Value, + Content = instructions, + }); + } + OllamaChatRequest request = new() { Format = ToOllamaChatResponseFormat(options?.ResponseFormat), - Messages = messages.SelectMany(ToOllamaChatRequestMessages).ToArray(), + Messages = requestMessages, Model = options?.ModelId ?? _metadata.DefaultModelId ?? string.Empty, Stream = stream, Tools = options?.ToolMode is not NoneChatToolMode && options?.Tools is { Count: > 0 } tools ? tools.OfType().Select(ToOllamaTool) : null, diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs index a5b23d567a4..7cdadb91666 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AI; internal sealed class OllamaChatRequest { public required string Model { get; set; } - public required OllamaChatRequestMessage[] Messages { get; set; } + public required IList Messages { get; set; } public JsonElement? Format { get; set; } public bool Stream { get; set; } public IEnumerable? Tools { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index f3717f1ee41..ea541bee91a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -348,8 +348,27 @@ void IDisposable.Dispose() } } - // Process ChatMessages. + // Configure system instructions. StringBuilder? instructions = null; + void AppendSystemInstructions(string? toAppend) + { + if (!string.IsNullOrEmpty(toAppend)) + { + if (instructions is null) + { + instructions = new(toAppend); + } + else + { + _ = instructions.AppendLine().AppendLine(toAppend); + } + } + } + + AppendSystemInstructions(runOptions.AdditionalInstructions); + AppendSystemInstructions(options?.Instructions); + + // Process ChatMessages. List? functionResults = null; foreach (var chatMessage in messages) { @@ -365,10 +384,9 @@ void IDisposable.Dispose() if (chatMessage.Role == ChatRole.System || chatMessage.Role == OpenAIResponseChatClient.ChatRoleDeveloper) { - instructions ??= new(); foreach (var textContent in chatMessage.Contents.OfType()) { - _ = instructions.Append(textContent); + AppendSystemInstructions(textContent.Text); } continue; @@ -409,10 +427,7 @@ void IDisposable.Dispose() } } - if (instructions is not null) - { - runOptions.AdditionalInstructions = instructions.ToString(); - } + runOptions.AdditionalInstructions = instructions?.ToString(); return (runOptions, functionResults); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index f97ebd492a7..a6cd70149d7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -80,7 +80,7 @@ public async Task GetResponseAsync( { _ = Throw.IfNull(messages); - var openAIChatMessages = ToOpenAIChatMessages(messages, AIJsonUtilities.DefaultOptions); + var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions); var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. @@ -95,7 +95,7 @@ public IAsyncEnumerable GetStreamingResponseAsync( { _ = Throw.IfNull(messages); - var openAIChatMessages = ToOpenAIChatMessages(messages, AIJsonUtilities.DefaultOptions); + var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions); var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. @@ -111,11 +111,16 @@ void IDisposable.Dispose() } /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. - private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, JsonSerializerOptions options) + private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, ChatOptions? chatOptions, JsonSerializerOptions jsonOptions) { // Maps all of the M.E.AI types to the corresponding OpenAI types. // Unrecognized or non-processable content is ignored. + if (chatOptions?.Instructions is { } instructions && !string.IsNullOrWhiteSpace(instructions)) + { + yield return new SystemChatMessage(instructions); + } + foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || @@ -139,7 +144,7 @@ void IDisposable.Dispose() { try { - result = JsonSerializer.Serialize(resultContent.Result, options.GetTypeInfo(typeof(object))); + result = JsonSerializer.Serialize(resultContent.Result, jsonOptions.GetTypeInfo(typeof(object))); } catch (NotSupportedException) { @@ -167,7 +172,7 @@ void IDisposable.Dispose() case FunctionCallContent fc: (toolCalls ??= []).Add( ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes( - fc.Arguments, options.GetTypeInfo(typeof(IDictionary)))))); + fc.Arguments, jsonOptions.GetTypeInfo(typeof(IDictionary)))))); break; default: diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 34e6977e1f7..ffa5bf19b63 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -366,6 +366,12 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt result.TopP ??= options.TopP; result.Temperature ??= options.Temperature; result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; + if (options.Instructions is { } instructions) + { + result.Instructions = string.IsNullOrEmpty(result.Instructions) ? + instructions : + $"{result.Instructions}{Environment.NewLine}{instructions}"; + } // Populate tools if there are any. if (options.Tools is { Count: > 0 } tools) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs index cdf1aab09c9..b7645c26245 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs @@ -15,6 +15,7 @@ public void Constructor_Parameterless_PropsDefaulted() { ChatOptions options = new(); Assert.Null(options.ConversationId); + Assert.Null(options.Instructions); Assert.Null(options.Temperature); Assert.Null(options.MaxOutputTokens); Assert.Null(options.TopP); @@ -33,6 +34,7 @@ public void Constructor_Parameterless_PropsDefaulted() ChatOptions clone = options.Clone(); Assert.Null(clone.ConversationId); + Assert.Null(clone.Instructions); Assert.Null(clone.Temperature); Assert.Null(clone.MaxOutputTokens); Assert.Null(clone.TopP); @@ -75,6 +77,7 @@ public void Properties_Roundtrip() Func rawRepresentationFactory = (c) => null; options.ConversationId = "12345"; + options.Instructions = "Some instructions"; options.Temperature = 0.1f; options.MaxOutputTokens = 2; options.TopP = 0.3f; @@ -92,6 +95,7 @@ public void Properties_Roundtrip() options.AdditionalProperties = additionalProps; Assert.Equal("12345", options.ConversationId); + Assert.Equal("Some instructions", options.Instructions); Assert.Equal(0.1f, options.Temperature); Assert.Equal(2, options.MaxOutputTokens); Assert.Equal(0.3f, options.TopP); @@ -144,6 +148,7 @@ public void JsonSerialization_Roundtrips() }; options.ConversationId = "12345"; + options.Instructions = "Some instructions"; options.Temperature = 0.1f; options.MaxOutputTokens = 2; options.TopP = 0.3f; @@ -170,6 +175,7 @@ public void JsonSerialization_Roundtrips() Assert.NotNull(deserialized); Assert.Equal("12345", deserialized.ConversationId); + Assert.Equal("Some instructions", deserialized.Instructions); Assert.Equal(0.1f, deserialized.Temperature); Assert.Equal(2, deserialized.MaxOutputTokens); Assert.Equal(0.3f, deserialized.TopP); From 694b95ef75c6bd9de00ef761dadae4e70ee8739f Mon Sep 17 00:00:00 2001 From: Shyam N Date: Fri, 13 Jun 2025 01:19:06 -0700 Subject: [PATCH 149/472] Introduce evaluators for agentic workflows (#6514) Introduces the following new evaluators as part of the Quality package: `ToolCallAccuracyEvaluator`, `TaskRelevanceEvaluator` and `IntentResolutionEvalutor`, all currently marked `[Experimental]`. Also includes following changes: * Fixes a regex bug that was causing reasoning and chain of thought outputs present in the evaluation response to not be parsed correctly into the corresponding metrics. * Adds support for displaying tool calls and tool results in the conversation displayed in the report. This fixes #6370 * Adds support for displaying JSON content both in the conversation as well as in context (along with a new settings toggle for controlling pretty printing for the displayed JSON). * Adds tests for the new evaluators. Fixes #6350 Fixes #6370 --- .../AIToolExtensions.cs | 44 ++ .../ChatMessageExtensions.cs | 35 ++ .../ChatResponseExtensions.cs | 51 +++ .../CompletenessEvaluator.cs | 2 +- .../EquivalenceEvaluator.cs | 2 +- .../EvaluationMetricExtensions.cs | 26 +- .../GroundednessEvaluator.cs | 2 +- .../IntentResolutionEvaluator.cs | 407 ++++++++++++++++++ .../IntentResolutionEvaluatorContext.cs | 88 ++++ .../IntentResolutionRating.cs | 83 ++++ .../JsonSerialization/SerializerContext.cs | 14 + .../RelevanceEvaluator.cs | 5 +- ...nceTruthAndCompletenessEvaluator.Rating.cs | 72 ---- ...CompletenessEvaluator.SerializerContext.cs | 16 - .../RelevanceTruthAndCompletenessEvaluator.cs | 40 +- .../RelevanceTruthAndCompletenessRating.cs | 84 ++++ .../RetrievalEvaluator.cs | 16 +- .../TaskAdherenceEvaluator.cs | 268 ++++++++++++ .../TaskAdherenceEvaluatorContext.cs | 90 ++++ .../ToolCallAccuracyEvaluator.cs | 226 ++++++++++ .../ToolCallAccuracyEvaluatorContext.cs | 92 ++++ .../TypeScript/components/App.tsx | 7 +- .../components/ConversationDetails.tsx | 27 +- .../TypeScript/components/ReportContext.tsx | 13 +- .../EvaluationMetricExtensions.cs | 2 +- .../GroundednessProEvaluator.cs | 2 +- .../UngroundedAttributesEvaluator.cs | 2 +- .../AgentQualityEvaluatorTests.cs | 280 ++++++++++++ .../QualityEvaluatorTests.cs | 2 + .../SafetyEvaluatorTests.cs | 2 + .../Setup.cs | 13 +- .../IntentResolutionRatingTests.cs | 324 ++++++++++++++ ...ruthAndCompletenessEvaluatorRatingTests.cs | 148 ------- ...elevanceTruthAndCompletenessRatingTests.cs | 382 ++++++++++++++++ 34 files changed, 2580 insertions(+), 287 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatMessageExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatResponseExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/JsonSerialization/SerializerContext.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.Rating.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.SerializerContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/IntentResolutionRatingTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessEvaluatorRatingTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessRatingTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs new file mode 100644 index 00000000000..3dbc8211416 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +internal static class AIToolExtensions +{ + internal static string RenderAsJson( + this IEnumerable toolDefinitions, + JsonSerializerOptions? options = null) + { + _ = Throw.IfNull(toolDefinitions); + + var toolDefinitionsJsonArray = new JsonArray(); + + foreach (AIFunction function in toolDefinitions.OfType()) + { + JsonNode functionJsonNode = + new JsonObject + { + ["name"] = function.Name, + ["description"] = function.Description, + ["functionSchema"] = JsonNode.Parse(function.JsonSchema.GetRawText()), + }; + + if (function.ReturnJsonSchema is not null) + { + functionJsonNode["functionReturnValueSchema"] = + JsonNode.Parse(function.ReturnJsonSchema.Value.GetRawText()); + } + + toolDefinitionsJsonArray.Add(functionJsonNode); + } + + string renderedToolDefinitions = toolDefinitionsJsonArray.ToJsonString(options); + return renderedToolDefinitions; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatMessageExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatMessageExtensions.cs new file mode 100644 index 00000000000..cfad90f85f4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatMessageExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +internal static class ChatMessageExtensions +{ + internal static string RenderAsJson(this IEnumerable messages, JsonSerializerOptions? options = null) + { + _ = Throw.IfNull(messages); + + var messagesJsonArray = new JsonArray(); + + foreach (ChatMessage message in messages) + { + JsonNode? messageJsonNode = + JsonSerializer.SerializeToNode( + message, + AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ChatMessage))); + + if (messageJsonNode is not null) + { + messagesJsonArray.Add(messageJsonNode); + } + } + + string renderedMessages = messagesJsonArray.ToJsonString(options); + return renderedMessages; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatResponseExtensions.cs new file mode 100644 index 00000000000..c579caa7cb1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ChatResponseExtensions.cs @@ -0,0 +1,51 @@ +// 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.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +internal static class ChatResponseExtensions +{ + internal static string RenderAsJson(this ChatResponse modelResponse, JsonSerializerOptions? options = null) + { + _ = Throw.IfNull(modelResponse); + + return modelResponse.Messages.RenderAsJson(options); + } + + internal static string RenderToolCallsAndResultsAsJson( + this ChatResponse modelResponse, + JsonSerializerOptions? options = null) + { + _ = Throw.IfNull(modelResponse); + + var toolCallsAndResultsJsonArray = new JsonArray(); + + foreach (AIContent content in modelResponse.Messages.SelectMany(m => m.Contents)) + { + if (content is FunctionCallContent or FunctionResultContent) + { + Type contentType = + content is FunctionCallContent ? typeof(FunctionCallContent) : typeof(FunctionResultContent); + + JsonNode? toolCallOrResultJsonNode = + JsonSerializer.SerializeToNode( + content, + AIJsonUtilities.DefaultOptions.GetTypeInfo(contentType)); + + if (toolCallOrResultJsonNode is not null) + { + toolCallsAndResultsJsonArray.Add(toolCallOrResultJsonNode); + } + } + } + + string renderedToolCallsAndResults = toolCallsAndResultsJsonArray.ToJsonString(options); + return renderedToolCallsAndResults; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs index a5f04300f70..3bd57cf322b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs @@ -87,7 +87,7 @@ public async ValueTask EvaluateAsync( { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"A value of type '{nameof(CompletenessEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + $"A value of type {nameof(CompletenessEvaluatorContext)} was not found in the {nameof(additionalContext)} collection.")); return result; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs index 714a027b4a1..ced79652bc7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs @@ -86,7 +86,7 @@ public async ValueTask EvaluateAsync( { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"A value of type '{nameof(EquivalenceEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + $"A value of type {nameof(EquivalenceEvaluatorContext)} was not found in the {nameof(additionalContext)} collection.")); return result; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EvaluationMetricExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EvaluationMetricExtensions.cs index 51acfa37d10..792db414d1c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EvaluationMetricExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EvaluationMetricExtensions.cs @@ -33,6 +33,25 @@ internal static EvaluationMetricInterpretation InterpretScore(this NumericMetric : new EvaluationMetricInterpretation(rating); } + internal static EvaluationMetricInterpretation InterpretScore( + this BooleanMetric metric, + bool passValue = true) + { + EvaluationRating rating = metric.Value switch + { + null => EvaluationRating.Inconclusive, + true => passValue ? EvaluationRating.Exceptional : EvaluationRating.Unacceptable, + false => passValue ? EvaluationRating.Unacceptable : EvaluationRating.Exceptional, + }; + + return metric.Value is bool value && value == passValue + ? new EvaluationMetricInterpretation(rating) + : new EvaluationMetricInterpretation( + rating, + failed: true, + reason: $"{metric.Name} is not {passValue}."); + } + internal static bool TryParseEvaluationResponseWithValue( this EvaluationMetric metric, ChatResponse evaluationResponse, @@ -81,7 +100,7 @@ internal static bool TryParseEvaluationResponseWithTags( static bool TryParseTag(string text, string tagName, [NotNullWhen(true)] out string? tagValue) { - const RegexOptions Options = RegexOptions.Multiline; + const RegexOptions Options = RegexOptions.Singleline; Match match = Regex.Match(text, $@"<{tagName}>(?.*?)", Options); if (!match.Success || match.Groups["value"] is not Group valueGroup || !valueGroup.Success) @@ -131,6 +150,11 @@ private static bool TryParseValue(this EvaluationMetric metric, string val booleanMetric.Value = booleanValue; return true; } + else if (int.TryParse(valueText, out int intValue) && (intValue is 0 or 1)) + { + booleanMetric.Value = intValue is 1; + return true; + } else { metric.AddDiagnostics( diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs index bf9b499ebc7..a52fbcf2ad9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs @@ -85,7 +85,7 @@ public async ValueTask EvaluateAsync( { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"A value of type '{nameof(GroundednessEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + $"A value of type {nameof(GroundednessEvaluatorContext)} was not found in the {nameof(additionalContext)} collection.")); return result; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs new file mode 100644 index 00000000000..4f19d308f10 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs @@ -0,0 +1,407 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Quality.Utilities; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// An that evaluates an AI system's effectiveness at identifying and resolving user intent. +/// +/// +/// +/// evaluates an AI system's effectiveness at identifying and resolving user +/// intent based on the supplied conversation history and the tool definitions supplied via +/// . +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +/// returns a that contains a score for 'Intent +/// Resolution'. The score is a number between 1 and 5, with 1 indicating a poor score, and 5 indicating an excellent +/// score. +/// +/// +/// Note: is an AI-based evaluator that uses an AI model to perform its +/// evaluation. While the prompt that this evaluator uses to perform its evaluation is designed to be model-agnostic, +/// the performance of this prompt (and the resulting evaluation) can vary depending on the model used, and can be +/// especially poor when a smaller / local model is used. +/// +/// +/// The prompt that uses has been tested against (and tuned to work well with) +/// the following models. So, using this evaluator with a model from the following list is likely to produce the best +/// results. (The model to be used can be configured via .) +/// +/// +/// GPT-4o +/// +/// +[Experimental("AIEVAL001")] +public sealed class IntentResolutionEvaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string IntentResolutionMetricName => "Intent Resolution"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [IntentResolutionMetricName]; + + private static readonly ChatOptions _chatOptions = + new ChatOptions + { + Temperature = 0.0f, + MaxOutputTokens = 800, + TopP = 1.0f, + PresencePenalty = 0.0f, + FrequencyPenalty = 0.0f, + ResponseFormat = ChatResponseFormat.Json + }; + + /// + public async ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + _ = Throw.IfNull(chatConfiguration); + + var metric = new NumericMetric(IntentResolutionMetricName); + var result = new EvaluationResult(metric); + + if (!messages.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + "The conversation history supplied for evaluation did not include any messages.")); + + return result; + } + + if (!modelResponse.Messages.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation did not include any messages.")); + + return result; + } + + IntentResolutionEvaluatorContext? context = + additionalContext?.OfType().FirstOrDefault(); + + if (context is not null && context.ToolDefinitions.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied {nameof(IntentResolutionEvaluatorContext)} did not contain any {nameof(IntentResolutionEvaluatorContext.ToolDefinitions)}.")); + + return result; + } + + var toolDefinitionNames = new HashSet(context?.ToolDefinitions.Select(td => td.Name) ?? []); + IEnumerable toolCalls = + modelResponse.Messages.SelectMany(m => m.Contents).OfType(); + + if (toolCalls.Any(t => !toolDefinitionNames.Contains(t.Name))) + { + if (context is null) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation contained calls to tools that were not supplied via {nameof(IntentResolutionEvaluatorContext)}.")); + } + else + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation contained calls to tools that were not included in the supplied {nameof(IntentResolutionEvaluatorContext)}.")); + } + + return result; + } + + List evaluationInstructions = GetEvaluationInstructions(messages, modelResponse, context); + + (ChatResponse evaluationResponse, TimeSpan evaluationDuration) = + await TimingHelper.ExecuteWithTimingAsync(() => + chatConfiguration.ChatClient.GetResponseAsync( + evaluationInstructions, + _chatOptions, + cancellationToken)).ConfigureAwait(false); + + if (context is not null) + { + metric.AddOrUpdateContext(context); + } + + await ParseEvaluationResponseAsync( + metric, + evaluationResponse, + evaluationDuration, + chatConfiguration, + cancellationToken).ConfigureAwait(false); + + return result; + } + + private static List GetEvaluationInstructions( + IEnumerable messages, + ChatResponse modelResponse, + IntentResolutionEvaluatorContext? context) + { + const string SystemPrompt = + "You are an expert in evaluating the quality of a RESPONSE from an intelligent assistant based on provided definition and Data."; + + List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; + + string renderedConversation = messages.RenderAsJson(); + string renderedModelResponse = modelResponse.RenderAsJson(); + string? renderedToolDefinitions = context?.ToolDefinitions.RenderAsJson(); + +#pragma warning disable S103 // Lines should not be too long + string evaluationPrompt = + $$""" + # Goal + Your goal is to assess the quality of the RESPONSE of an assistant in relation to a QUERY from a user, specifically focusing on + the assistant's ability to understand and resolve the user intent expressed in the QUERY. There is also a field for tool definitions + describing the functions, if any, that are accessible to the agent and that the agent may invoke in the RESPONSE if necessary. + + There are two components to intent resolution: + - Intent Understanding: The extent to which the agent accurately discerns the user's underlying need or inquiry. + - Response Resolution: The degree to which the agent's response is comprehensive, relevant, and adequately addresses the user's request. + + Note that the QUERY can either be a string with a user request or an entire conversation history including previous requests and responses from the assistant. + In this case, the assistant's response should be evaluated in the context of the entire conversation but the focus should be on the last intent. + + # Data + QUERY: {{renderedConversation}} + RESPONSE: {{renderedModelResponse}} + TOOL_DEFINITIONS: {{renderedToolDefinitions}} + + + # Ratings + ## [Score: 1] (Response completely unrelated to user intent) + **Definition:** The agent's response does not address the query at all. + + **Example:** + **Query:** How do I bake a chocolate cake? + **Response:** The latest smartphone models have incredible features and performance. + **Tool Definitions:** [] + + **Expected output** + { + "explanation": "The agent's response is entirely off-topic, discussing smartphones instead of providing any information about baking a chocolate cake." + "conversation_has_intent": true, + "agent_perceived_intent": "discussion about smartphone features", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": false, + "intent_resolved": false, + "resolution_score": 1, + } + + + ## [Score: 2] (Response minimally relates to user intent) + **Definition:** The response shows a token attempt to address the query by mentioning a relevant keyword or concept, but it provides almost no useful or actionable information. + + **Example input:** + **Query:** How do I bake a chocolate cake? + **Response:** Chocolate cake involves some ingredients. + **Tool Definitions:** [] + + **Expected output** + { + "explanation": "While the response mentions 'ingredients' related to a chocolate cake, it barely addresses the process or any detailed steps, leaving the query unresolved." + "conversation_has_intent": true, + "agent_perceived_intent": "mention of ingredients", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": false, + "intent_resolved": false, + "resolution_score": 2, + } + + + ## [Score: 3] (Response partially addresses the user intent but lacks complete details) + **Definition:** The response provides a basic idea related to the query by mentioning a few relevant elements, but it omits several key details and specifics needed for fully resolving the user's query. + + **Example input:** + **Query:** How do I bake a chocolate cake? + **Response:** Preheat your oven and mix the ingredients before baking the cake. + **Tool Definitions:** [] + + **Expected output** + { + "explanation": "The response outlines a minimal process (preheating and mixing) but omits critical details like ingredient measurements, baking time, and temperature specifics, resulting in only a partial resolution of the query." + "conversation_has_intent": true, + "agent_perceived_intent": "basic baking process", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": false, + "resolution_score": 3, + } + + + ## [Score: 4] (Response addresses the user intent with moderate accuracy but has minor inaccuracies or omissions) + **Definition:** The response offers a moderately detailed answer that includes several specific elements relevant to the query, yet it still lacks some finer details or complete information. + + **Example input:** + **Query:** How do I bake a chocolate cake? + **Response:** Preheat your oven to 350°F. In a bowl, combine flour, sugar, cocoa, eggs, and milk, mix well, and bake for about 30 minutes. + **Tool Definitions:** [] + + **Expected output** + { + "explanation": "The response includes specific steps and ingredients, indicating a clear intent to provide a recipe. However, it lacks detailed measurements and finer instructions, which makes the resolution only moderately complete." + "conversation_has_intent": true, + "agent_perceived_intent": "provide a basic chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": false, + "resolution_score": 4, + } + + + ## [Score: 5] (Response directly addresses the user intent and fully resolves it) + **Definition:** The response provides a complete, detailed, and accurate answer that fully resolves the user's query with all necessary information and precision. + + **Example input:** + **Query:** How do I bake a chocolate cake? + **Response:** Preheat your oven to 350°F (175°C) and grease a 9-inch round cake pan. In a large bowl, sift together 1 ¾ cups all-purpose flour, 1 cup sugar, ¾ cup unsweetened cocoa powder, 1 ½ tsp baking powder, and 1 tsp salt. In another bowl, beat 2 large eggs with 1 cup milk, ½ cup vegetable oil, and 2 tsp vanilla extract. Combine the wet ingredients with the dry ingredients, then gradually mix in 1 cup boiling water until smooth. Pour the batter into the prepared pan and bake for 30-35 minutes or until a toothpick inserted into the center comes out clean. Allow the cake to cool before serving. + **Tool Definitions:** [] + + **Expected output** + { + "explanation": "The response delivers a complete and precise recipe with detailed instructions and measurements, fully addressing the user's query about baking a chocolate cake." + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5, + } + + + # Task + + Please provide your evaluation for the assistant RESPONSE in relation to the user QUERY and tool definitions based on the Definitions and examples above. + Your output should consist only of a JSON object, as provided in the examples, that has the following keys: + - explanation: a string that explains why you think the input Data should get this resolution_score. + - conversation_has_intent: true or false + - agent_perceived_intent: a string that describes the intent the agent perceived from the user query + - actual_user_intent: a string that describes the actual user intent + - correct_intent_detected: true or false + - intent_resolved: true or false + - resolution_score: an integer between 1 and 5 that represents the resolution score + + + # Output + """; +#pragma warning restore S103 + + evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); + + return evaluationInstructions; + } + + private static async ValueTask ParseEvaluationResponseAsync( + NumericMetric metric, + ChatResponse evaluationResponse, + TimeSpan evaluationDuration, + ChatConfiguration chatConfiguration, + CancellationToken cancellationToken) + { + IntentResolutionRating rating; + + string evaluationResponseText = evaluationResponse.Text.Trim(); + if (string.IsNullOrEmpty(evaluationResponseText)) + { + rating = IntentResolutionRating.Inconclusive; + metric.AddDiagnostics( + EvaluationDiagnostic.Error("The model failed to produce a valid evaluation response.")); + } + else + { + try + { + rating = IntentResolutionRating.FromJson(evaluationResponseText); + } + catch (JsonException) + { + try + { + string repairedJson = + await JsonOutputFixer.RepairJsonAsync( + evaluationResponseText, + chatConfiguration, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(repairedJson)) + { + rating = IntentResolutionRating.Inconclusive; + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $""" + Failed to repair the following response from the model and parse the score for '{IntentResolutionMetricName}': + {evaluationResponseText} + """)); + } + else + { + rating = IntentResolutionRating.FromJson(repairedJson); + } + } + catch (JsonException ex) + { + rating = IntentResolutionRating.Inconclusive; + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $""" + Failed to repair the following response from the model and parse the score for '{IntentResolutionMetricName}': + {evaluationResponseText} + {ex} + """)); + } + } + } + + UpdateMetric(); + + void UpdateMetric() + { + metric.AddOrUpdateChatMetadata(evaluationResponse, evaluationDuration); + metric.Value = rating.ResolutionScore; + metric.Interpretation = metric.InterpretScore(); + metric.Reason = rating.Explanation; + + if (!string.IsNullOrWhiteSpace(rating.AgentPerceivedIntent)) + { + metric.AddOrUpdateMetadata("agent_perceived_intent", rating.AgentPerceivedIntent!); + } + + if (!string.IsNullOrWhiteSpace(rating.ActualUserIntent)) + { + metric.AddOrUpdateMetadata("actual_user_intent", rating.ActualUserIntent!); + } + + metric.AddOrUpdateMetadata("conversation_has_intent", rating.ConversationHasIntent.ToString()); + metric.AddOrUpdateMetadata("correct_intent_detected", rating.CorrectIntentDetected.ToString()); + metric.AddOrUpdateMetadata("intent_resolved", rating.IntentResolved.ToString()); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs new file mode 100644 index 00000000000..c8dcbc996b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// Contextual information that the uses to evaluate an AI system's +/// effectiveness at identifying and resolving user intent. +/// +/// +/// +/// evaluates an AI system's effectiveness at identifying and resolving user +/// intent based on the supplied conversation history and the tool definitions supplied via +/// . +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +[Experimental("AIEVAL001")] +public sealed class IntentResolutionEvaluatorContext : EvaluationContext +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) + : base(name: IntentResolutionContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + { + ToolDefinitions = [.. toolDefinitions]; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) + : this(toolDefinitions as IEnumerable) + { + } + + /// + /// Gets the unique that is used for + /// . + /// + public static string IntentResolutionContextName => "Tool Definitions (Intent Resolution)"; + + /// + /// Gets set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// + /// evaluates an AI system's effectiveness at identifying and resolving user + /// intent based on the supplied conversation history and the tool definitions supplied via + /// . + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions that are supplied via + /// will be ignored. + /// + /// + public IReadOnlyList ToolDefinitions { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs new file mode 100644 index 00000000000..a1d9b0ef90e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs @@ -0,0 +1,83 @@ +// 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 System.Text.Json.Serialization; +using Microsoft.Extensions.AI.Evaluation.Quality.JsonSerialization; +using Microsoft.Extensions.AI.Evaluation.Quality.Utilities; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +internal sealed class IntentResolutionRating +{ + public static IntentResolutionRating Inconclusive { get; } = + new IntentResolutionRating( + resolutionScore: 0, + explanation: string.Empty, + agentPerceivedIntent: string.Empty, + actualUserIntent: string.Empty, + conversationHasIntent: false, + correctIntentDetected: false, + intentResolved: false); + + [JsonRequired] + [JsonPropertyName("resolution_score")] + public int ResolutionScore { get; set; } + + [JsonRequired] + [JsonPropertyName("explanation")] + public string Explanation { get; set; } + + [JsonRequired] + [JsonPropertyName("agent_perceived_intent")] + public string AgentPerceivedIntent { get; set; } + + [JsonRequired] + [JsonPropertyName("actual_user_intent")] + public string ActualUserIntent { get; set; } + + [JsonRequired] + [JsonPropertyName("conversation_has_intent")] + public bool ConversationHasIntent { get; set; } + + [JsonRequired] + [JsonPropertyName("correct_intent_detected")] + public bool CorrectIntentDetected { get; set; } + + [JsonRequired] + [JsonPropertyName("intent_resolved")] + public bool IntentResolved { get; set; } + + private const int MinValue = 1; + private const int MaxValue = 5; + + public bool IsInconclusive => ResolutionScore < MinValue || ResolutionScore > MaxValue; + + [JsonConstructor] +#pragma warning disable S107 // Methods should not have too many parameters + public IntentResolutionRating( + int resolutionScore, + string explanation, + string agentPerceivedIntent, + string actualUserIntent, + bool conversationHasIntent, + bool correctIntentDetected, + bool intentResolved) +#pragma warning restore S107 + { + ResolutionScore = resolutionScore; + Explanation = explanation; + AgentPerceivedIntent = agentPerceivedIntent; + ActualUserIntent = actualUserIntent; + ConversationHasIntent = conversationHasIntent; + CorrectIntentDetected = correctIntentDetected; + IntentResolved = intentResolved; + } + + public static IntentResolutionRating FromJson(string jsonResponse) + { + ReadOnlySpan trimmed = JsonOutputFixer.TrimMarkdownDelimiters(jsonResponse); + return JsonSerializer.Deserialize(trimmed, SerializerContext.Default.IntentResolutionRating)!; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/JsonSerialization/SerializerContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/JsonSerialization/SerializerContext.cs new file mode 100644 index 00000000000..588b3d23a7e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/JsonSerialization/SerializerContext.cs @@ -0,0 +1,14 @@ +// 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.Serialization; + +namespace Microsoft.Extensions.AI.Evaluation.Quality.JsonSerialization; + +[JsonSourceGenerationOptions( + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(RelevanceTruthAndCompletenessRating))] +[JsonSerializable(typeof(IntentResolutionRating))] +internal sealed partial class SerializerContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs index 86fb950e720..3946853a2a4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs @@ -75,12 +75,11 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(RelevanceMetricName); var result = new EvaluationResult(metric); - if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || - string.IsNullOrWhiteSpace(userRequest.Text)) + if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || string.IsNullOrWhiteSpace(userRequest.Text)) { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"The ${messages} supplied for evaluation did not contain a user request as the last message.")); + $"The {nameof(messages)} supplied for evaluation did not contain a user request as the last message.")); return result; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.Rating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.Rating.cs deleted file mode 100644 index 8ff913fefe7..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.Rating.cs +++ /dev/null @@ -1,72 +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 System.Text.Json.Serialization; -using Microsoft.Extensions.AI.Evaluation.Quality.Utilities; - -namespace Microsoft.Extensions.AI.Evaluation.Quality; - -public partial class RelevanceTruthAndCompletenessEvaluator -{ - internal sealed class Rating - { - public static Rating Inconclusive { get; } = new Rating(relevance: -1, truth: -1, completeness: -1); - - public int Relevance { get; } - public string? RelevanceReasoning { get; } - public string[] RelevanceReasons { get; } = []; - - public int Truth { get; } - public string? TruthReasoning { get; } - public string[] TruthReasons { get; } = []; - - public int Completeness { get; } - public string? CompletenessReasoning { get; } - public string[] CompletenessReasons { get; } = []; - - public string? Error { get; } - - private const int MinValue = 1; - private const int MaxValue = 5; - -#pragma warning disable S1067 // Expressions should not be too complex. - public bool IsInconclusive => - Error is not null || - Relevance < MinValue || Relevance > MaxValue || - Truth < MinValue || Truth > MaxValue || - Completeness < MinValue || Completeness > MaxValue; -#pragma warning restore S1067 - - public Rating(int relevance, int truth, int completeness, string? error = null) - { - (Relevance, Truth, Completeness, Error) = (relevance, truth, completeness, error); - } - - [JsonConstructor] -#pragma warning disable S107 // Methods should not have too many parameters. - public Rating( - int relevance, string? relevanceReasoning, string[] relevanceReasons, - int truth, string? truthReasoning, string[] truthReasons, - int completeness, string? completenessReasoning, string[] completenessReasons, - string? error = null) -#pragma warning restore S107 - { - (Relevance, RelevanceReasoning, RelevanceReasons, - Truth, TruthReasoning, TruthReasons, - Completeness, CompletenessReasoning, CompletenessReasons, - Error) = - (relevance, relevanceReasoning, relevanceReasons ?? [], - truth, truthReasoning, truthReasons ?? [], - completeness, completenessReasoning, completenessReasons ?? [], - error); - } - - public static Rating FromJson(string jsonResponse) - { - ReadOnlySpan trimmed = JsonOutputFixer.TrimMarkdownDelimiters(jsonResponse); - return JsonSerializer.Deserialize(trimmed, SerializerContext.Default.Rating)!; - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.SerializerContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.SerializerContext.cs deleted file mode 100644 index 211213d4873..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.SerializerContext.cs +++ /dev/null @@ -1,16 +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.Serialization; - -namespace Microsoft.Extensions.AI.Evaluation.Quality; - -public partial class RelevanceTruthAndCompletenessEvaluator -{ - [JsonSourceGenerationOptions( - WriteIndented = true, - AllowTrailingCommas = true, - PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] - [JsonSerializable(typeof(Rating))] - internal sealed partial class SerializerContext : JsonSerializerContext; -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs index d10c259a4de..4eb41b15361 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs @@ -43,7 +43,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// Tutorial: Evaluate a model's response with response caching and reporting. /// [Experimental("AIEVAL001")] -public sealed partial class RelevanceTruthAndCompletenessEvaluator : IEvaluator +public sealed class RelevanceTruthAndCompletenessEvaluator : IEvaluator { /// /// Gets the of the returned by @@ -97,7 +97,7 @@ public async ValueTask EvaluateAsync( { result.AddDiagnosticsToAllMetrics( EvaluationDiagnostic.Error( - $"The ${messages} supplied for evaluation did not contain a user request as the last message.")); + $"The {nameof(messages)} supplied for evaluation did not contain a user request as the last message.")); return result; } @@ -271,12 +271,12 @@ private static async ValueTask ParseEvaluationResponseAsync( ChatConfiguration chatConfiguration, CancellationToken cancellationToken) { - Rating rating; + RelevanceTruthAndCompletenessRating rating; string evaluationResponseText = evaluationResponse.Text.Trim(); if (string.IsNullOrEmpty(evaluationResponseText)) { - rating = Rating.Inconclusive; + rating = RelevanceTruthAndCompletenessRating.Inconclusive; result.AddDiagnosticsToAllMetrics( EvaluationDiagnostic.Error("The model failed to produce a valid evaluation response.")); } @@ -284,7 +284,7 @@ private static async ValueTask ParseEvaluationResponseAsync( { try { - rating = Rating.FromJson(evaluationResponseText); + rating = RelevanceTruthAndCompletenessRating.FromJson(evaluationResponseText); } catch (JsonException) { @@ -298,26 +298,26 @@ await JsonOutputFixer.RepairJsonAsync( if (string.IsNullOrWhiteSpace(repairedJson)) { - rating = Rating.Inconclusive; + rating = RelevanceTruthAndCompletenessRating.Inconclusive; result.AddDiagnosticsToAllMetrics( EvaluationDiagnostic.Error( $""" - Failed to repair the following response from the model and parse scores for '{RelevanceMetricName}', '{TruthMetricName}' and '{CompletenessMetricName}'.: + Failed to repair the following response from the model and parse scores for '{RelevanceMetricName}', '{TruthMetricName}' and '{CompletenessMetricName}': {evaluationResponseText} """)); } else { - rating = Rating.FromJson(repairedJson); + rating = RelevanceTruthAndCompletenessRating.FromJson(repairedJson); } } catch (JsonException ex) { - rating = Rating.Inconclusive; + rating = RelevanceTruthAndCompletenessRating.Inconclusive; result.AddDiagnosticsToAllMetrics( EvaluationDiagnostic.Error( $""" - Failed to repair the following response from the model and parse scores for '{RelevanceMetricName}', '{TruthMetricName}' and '{CompletenessMetricName}'.: + Failed to repair the following response from the model and parse scores for '{RelevanceMetricName}', '{TruthMetricName}' and '{CompletenessMetricName}': {evaluationResponseText} {ex} """)); @@ -336,10 +336,7 @@ void UpdateResult() relevance.AddOrUpdateChatMetadata(evaluationResponse, evaluationDuration); relevance.Value = rating.Relevance; relevance.Interpretation = relevance.InterpretScore(); - if (!string.IsNullOrWhiteSpace(rating.RelevanceReasoning)) - { - relevance.Reason = rating.RelevanceReasoning!; - } + relevance.Reason = rating.RelevanceReasoning; if (rating.RelevanceReasons.Any()) { @@ -351,10 +348,7 @@ void UpdateResult() truth.AddOrUpdateChatMetadata(evaluationResponse, evaluationDuration); truth.Value = rating.Truth; truth.Interpretation = truth.InterpretScore(); - if (!string.IsNullOrWhiteSpace(rating.TruthReasoning)) - { - truth.Reason = rating.TruthReasoning!; - } + truth.Reason = rating.TruthReasoning; if (rating.TruthReasons.Any()) { @@ -366,21 +360,13 @@ void UpdateResult() completeness.AddOrUpdateChatMetadata(evaluationResponse, evaluationDuration); completeness.Value = rating.Completeness; completeness.Interpretation = completeness.InterpretScore(); - if (!string.IsNullOrWhiteSpace(rating.CompletenessReasoning)) - { - completeness.Reason = rating.CompletenessReasoning!; - } + completeness.Reason = rating.CompletenessReasoning; if (rating.CompletenessReasons.Any()) { string value = string.Join(Separator, rating.CompletenessReasons); completeness.AddOrUpdateMetadata(name: Rationales, value); } - - if (!string.IsNullOrWhiteSpace(rating.Error)) - { - result.AddDiagnosticsToAllMetrics(EvaluationDiagnostic.Error(rating.Error!)); - } } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs new file mode 100644 index 00000000000..83c76a1825e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs @@ -0,0 +1,84 @@ +// 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 System.Text.Json.Serialization; +using Microsoft.Extensions.AI.Evaluation.Quality.JsonSerialization; +using Microsoft.Extensions.AI.Evaluation.Quality.Utilities; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +internal sealed class RelevanceTruthAndCompletenessRating +{ + public static RelevanceTruthAndCompletenessRating Inconclusive { get; } = + new RelevanceTruthAndCompletenessRating( + relevance: 0, + relevanceReasoning: string.Empty, + relevanceReasons: [], + truth: 0, + truthReasoning: string.Empty, + truthReasons: [], + completeness: 0, + completenessReasoning: string.Empty, + completenessReasons: []); + + [JsonRequired] + public int Relevance { get; set; } + + [JsonRequired] + public string RelevanceReasoning { get; set; } + + [JsonRequired] + public string[] RelevanceReasons { get; set; } + + [JsonRequired] + public int Truth { get; set; } + + [JsonRequired] + public string TruthReasoning { get; set; } + + [JsonRequired] + public string[] TruthReasons { get; set; } + + [JsonRequired] + public int Completeness { get; set; } + + [JsonRequired] + public string CompletenessReasoning { get; set; } + + [JsonRequired] + public string[] CompletenessReasons { get; set; } + + private const int MinValue = 1; + private const int MaxValue = 5; + +#pragma warning disable S1067 // Expressions should not be too complex. + public bool IsInconclusive => + Relevance < MinValue || Relevance > MaxValue || + Truth < MinValue || Truth > MaxValue || + Completeness < MinValue || Completeness > MaxValue; +#pragma warning restore S1067 + + [JsonConstructor] +#pragma warning disable S107 // Methods should not have too many parameters. + public RelevanceTruthAndCompletenessRating( + int relevance, string relevanceReasoning, string[] relevanceReasons, + int truth, string truthReasoning, string[] truthReasons, + int completeness, string completenessReasoning, string[] completenessReasons) +#pragma warning restore S107 + { + (Relevance, RelevanceReasoning, RelevanceReasons, + Truth, TruthReasoning, TruthReasons, + Completeness, CompletenessReasoning, CompletenessReasons) = + (relevance, relevanceReasoning, relevanceReasons ?? [], + truth, truthReasoning, truthReasons ?? [], + completeness, completenessReasoning, completenessReasons ?? []); + } + + public static RelevanceTruthAndCompletenessRating FromJson(string jsonResponse) + { + ReadOnlySpan trimmed = JsonOutputFixer.TrimMarkdownDelimiters(jsonResponse); + return JsonSerializer.Deserialize(trimmed, SerializerContext.Default.RelevanceTruthAndCompletenessRating)!; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs index 9ecfbb182f5..cd2f94456e6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs @@ -80,12 +80,11 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(RetrievalMetricName); var result = new EvaluationResult(metric); - if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || - string.IsNullOrWhiteSpace(userRequest.Text)) + if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || string.IsNullOrWhiteSpace(userRequest.Text)) { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"The ${messages} supplied for evaluation did not contain a user request as the last message.")); + $"The {nameof(messages)} supplied for evaluation did not contain a user request as the last message.")); return result; } @@ -95,7 +94,16 @@ public async ValueTask EvaluateAsync( { metric.AddDiagnostics( EvaluationDiagnostic.Error( - $"A value of type '{nameof(RetrievalEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + $"A value of type {nameof(RetrievalEvaluatorContext)} was not found in the {nameof(additionalContext)} collection.")); + + return result; + } + + if (context.RetrievedContextChunks.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied {nameof(RetrievalEvaluatorContext)} did not contain any {nameof(RetrievalEvaluatorContext.RetrievedContextChunks)}.")); return result; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs new file mode 100644 index 00000000000..cf4ba4073ee --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs @@ -0,0 +1,268 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// An that evaluates an AI system's effectiveness at adhering to the task assigned to it. +/// +/// +/// +/// measures how accurately an AI system adheres to the task assigned to it by +/// examining the alignment of the supplied response with instructions and definitions present in the conversation +/// history, the accuracy and clarity of the response, and the proper use of tool definitions supplied via +/// . +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +/// returns a that contains a score for 'Task +/// Adherence'. The score is a number between 1 and 5, with 1 indicating a poor score, and 5 indicating an excellent +/// score. +/// +/// +/// Note: is an AI-based evaluator that uses an AI model to perform its +/// evaluation. While the prompt that this evaluator uses to perform its evaluation is designed to be model-agnostic, +/// the performance of this prompt (and the resulting evaluation) can vary depending on the model used, and can be +/// especially poor when a smaller / local model is used. +/// +/// +/// The prompt that uses has been tested against (and tuned to work well with) +/// the following models. So, using this evaluator with a model from the following list is likely to produce the best +/// results. (The model to be used can be configured via .) +/// +/// +/// GPT-4o +/// +/// +[Experimental("AIEVAL001")] +public sealed class TaskAdherenceEvaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string TaskAdherenceMetricName => "Task Adherence"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [TaskAdherenceMetricName]; + + private static readonly ChatOptions _chatOptions = + new ChatOptions + { + Temperature = 0.0f, + MaxOutputTokens = 800, + TopP = 1.0f, + PresencePenalty = 0.0f, + FrequencyPenalty = 0.0f, + ResponseFormat = ChatResponseFormat.Text + }; + + /// + public async ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + _ = Throw.IfNull(chatConfiguration); + + var metric = new NumericMetric(TaskAdherenceMetricName); + var result = new EvaluationResult(metric); + + if (!messages.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + "The conversation history supplied for evaluation did not include any messages.")); + + return result; + } + + if (!modelResponse.Messages.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation did not include any messages.")); + + return result; + } + + TaskAdherenceEvaluatorContext? context = + additionalContext?.OfType().FirstOrDefault(); + + if (context is not null && context.ToolDefinitions.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied {nameof(TaskAdherenceEvaluatorContext)} did not contain any {nameof(TaskAdherenceEvaluatorContext.ToolDefinitions)}.")); + + return result; + } + + var toolDefinitionNames = new HashSet(context?.ToolDefinitions.Select(td => td.Name) ?? []); + IEnumerable toolCalls = + modelResponse.Messages.SelectMany(m => m.Contents).OfType(); + + if (toolCalls.Any(t => !toolDefinitionNames.Contains(t.Name))) + { + if (context is null) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation contained calls to tools that were not supplied via {nameof(TaskAdherenceEvaluatorContext)}.")); + } + else + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation contained calls to tools that were not included in the supplied {nameof(TaskAdherenceEvaluatorContext)}.")); + } + + return result; + } + + List evaluationInstructions = GetEvaluationInstructions(messages, modelResponse, context); + + (ChatResponse evaluationResponse, TimeSpan evaluationDuration) = + await TimingHelper.ExecuteWithTimingAsync(() => + chatConfiguration.ChatClient.GetResponseAsync( + evaluationInstructions, + _chatOptions, + cancellationToken)).ConfigureAwait(false); + + _ = metric.TryParseEvaluationResponseWithTags(evaluationResponse, evaluationDuration); + + if (context is not null) + { + metric.AddOrUpdateContext(context); + } + + metric.Interpretation = metric.InterpretScore(); + return result; + } + + private static List GetEvaluationInstructions( + IEnumerable messages, + ChatResponse modelResponse, + TaskAdherenceEvaluatorContext? context) + { + string renderedConversation = messages.RenderAsJson(); + string renderedModelResponse = modelResponse.RenderAsJson(); + string? renderedToolDefinitions = context?.ToolDefinitions.RenderAsJson(); + +#pragma warning disable S103 // Lines should not be too long + string systemPrompt = + $$""" + # Instruction + ## Context + ### You are an expert in evaluating the quality of an answer from an intelligent system based on provided definitions and data. Your goal will involve answering the questions below using the information provided. + - **Definition**: Based on the provided query, response, and tool definitions, evaluate the agent's adherence to the assigned task. + - **Data**: Your input data includes query, response, and tool definitions. + - **Questions**: To complete your evaluation you will be asked to evaluate the Data in different ways. + + # Definition + + **Level 1: Fully Inadherent** + + **Definition:** + Response completely ignores instructions or deviates significantly + + **Example:** + **Query:** What is a recommended weekend itinerary in Paris? + **Response:** Paris is a lovely city with a rich history. + + Explanation: This response completely misses the task by not providing any itinerary details. It offers a generic statement about Paris rather than a structured travel plan. + + + **Level 2: Barely Adherent** + + **Definition:** + Response partially aligns with instructions but has critical gaps. + + **Example:** + **Query:** What is a recommended weekend itinerary in Paris? + **Response:** Spend your weekend visiting famous places in Paris. + + Explanation: While the response hints at visiting well-known sites, it is extremely vague and lacks specific details, such as which sites to visit or any order of activities, leaving major gaps in the instructions. + + + **Level 3: Moderately Adherent** + + **Definition:** + Response meets the core requirements but lacks precision or clarity. + + **Example:** + **Query:** What is a recommended weekend itinerary in Paris? + **Response:** Visit the Eiffel Tower and the Louvre on Saturday, and stroll through Montmartre on Sunday. + + Explanation: This answer meets the basic requirement by naming a few key attractions and assigning them to specific days. However, it lacks additional context, such as timings, additional activities, or details to make the itinerary practical and clear. + + + **Level 4: Mostly Adherent** + + **Definition:** + Response is clear, accurate, and aligns with instructions with minor issues. + + **Example:** + **Query:** What is a recommended weekend itinerary in Paris? + **Response:** For a weekend in Paris, start Saturday with a morning visit to the Eiffel Tower, then head to the Louvre in the early afternoon. In the evening, enjoy a leisurely walk along the Seine. On Sunday, begin with a visit to Notre-Dame Cathedral, followed by exploring the art and cafés in Montmartre. This plan offers a mix of cultural visits and relaxing experiences. + + Explanation: This response is clear, structured, and provides a concrete itinerary with specific attractions and a suggested order of activities. It is accurate and useful, though it might benefit from a few more details like exact timings or restaurant suggestions to be perfect. + + + **Level 5: Fully Adherent** + + **Definition:** + Response is flawless, accurate, and follows instructions to the letter. + + **Example:** + **Query:** What is a recommended weekend itinerary in Paris? + **Response:** Here is a detailed weekend itinerary in Paris: + Saturday: + Morning: Begin your day with a visit to the Eiffel Tower to admire the views from the top. + Early Afternoon: Head to the Louvre for a guided tour of its most famous exhibits. + Late Afternoon: Take a relaxing walk along the Seine, stopping at local boutiques. + Evening: Enjoy dinner at a classic Parisian bistro near the river. + Sunday: + Morning: Visit the Notre-Dame Cathedral to explore its architecture and history. + Midday: Wander the charming streets of Montmartre, stopping by art galleries and cafés. + Afternoon: Finish your trip with a scenic boat tour on the Seine. + This itinerary balances cultural immersion, leisure, and local dining experiences, ensuring a well-rounded visit. + + Explanation: This response is comprehensive and meticulously follows the instructions. It provides detailed steps, timings, and a variety of activities that fully address the query, leaving no critical gaps. + + # Data + Query: {{renderedConversation}} + Response: {{renderedModelResponse}} + Tool Definitions: {{renderedToolDefinitions}} + + # Tasks + ## Please provide your assessment Score for the previous answer. Your output should include the following information: + - **ThoughtChain**: To improve the reasoning process, Think Step by Step and include a step-by-step explanation of your thought process as you analyze the data based on the definitions. Keep it brief and Start your ThoughtChain with "Let's think step by step:". + - **Explanation**: a very short explanation of why you think the input data should get that Score. + - **Score**: based on your previous analysis, provide your Score. The answer you give MUST be an integer score ("1", "2", ...) based on the categories of the definitions. + + ## Please provide your answers between the tags: your chain of thoughts, your explanation, your score. + # Output + """; +#pragma warning restore S103 + + List evaluationInstructions = [new ChatMessage(ChatRole.System, systemPrompt)]; + return evaluationInstructions; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs new file mode 100644 index 00000000000..3d54ed74dab --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// Contextual information that the uses to evaluate an AI system's +/// effectiveness at adhering to the task assigned to it. +/// +/// +/// +/// measures how accurately an AI system adheres to the task assigned to it by +/// examining the alignment of the supplied response with instructions and definitions present in the conversation +/// history, the accuracy and clarity of the response, and the proper use of tool definitions supplied via +/// . +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +[Experimental("AIEVAL001")] +public sealed class TaskAdherenceEvaluatorContext : EvaluationContext +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) + : base(name: TaskAdherenceContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + { + ToolDefinitions = [.. toolDefinitions]; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) + : this(toolDefinitions as IEnumerable) + { + } + + /// + /// Gets the unique that is used for + /// . + /// + public static string TaskAdherenceContextName => "Tool Definitions (Task Adherence)"; + + /// + /// Gets set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// + /// measures how accurately an AI system adheres to the task assigned to it by + /// examining the alignment of the supplied response with instructions and definitions present in the conversation + /// history, the accuracy and clarity of the response, and the proper use of tool definitions supplied via + /// . + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that are + /// defined as s. Any other definitions that are supplied via + /// will be ignored. + /// + /// + public IReadOnlyList ToolDefinitions { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs new file mode 100644 index 00000000000..5b3631bf598 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs @@ -0,0 +1,226 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// An that evaluates an AI system's effectiveness at using the tools supplied to it. +/// +/// +/// +/// measures how accurately an AI system uses tools by examining tool calls +/// (i.e., s) present in the supplied response to assess the relevance of these tool +/// calls to the conversation, the parameter correctness for these tool calls with regard to the tool definitions +/// supplied via , and the accuracy of the parameter +/// value extraction from the supplied conversation. +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +/// returns a that contains a score for 'Tool Call +/// Accuracy'. The score is if the tool call is irrelevant or contains information not present +/// in the conversation and if the tool call is relevant with properly extracted parameters +/// from the conversation. +/// +/// +/// Note: is an AI-based evaluator that uses an AI model to perform its +/// evaluation. While the prompt that this evaluator uses to perform its evaluation is designed to be model-agnostic, +/// the performance of this prompt (and the resulting evaluation) can vary depending on the model used, and can be +/// especially poor when a smaller / local model is used. +/// +/// +/// The prompt that uses has been tested against (and tuned to work well with) +/// the following models. So, using this evaluator with a model from the following list is likely to produce the best +/// results. (The model to be used can be configured via .) +/// +/// +/// GPT-4o +/// +/// +[Experimental("AIEVAL001")] +public sealed class ToolCallAccuracyEvaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string ToolCallAccuracyMetricName => "Tool Call Accuracy"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [ToolCallAccuracyMetricName]; + + private static readonly ChatOptions _chatOptions = + new ChatOptions + { + Temperature = 0.0f, + MaxOutputTokens = 800, + TopP = 1.0f, + PresencePenalty = 0.0f, + FrequencyPenalty = 0.0f, + ResponseFormat = ChatResponseFormat.Text + }; + + /// + public async ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + _ = Throw.IfNull(chatConfiguration); + + var metric = new BooleanMetric(ToolCallAccuracyMetricName); + var result = new EvaluationResult(metric); + + if (!messages.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + "The conversation history supplied for evaluation did not include any messages.")); + + return result; + } + + IEnumerable toolCalls = + modelResponse.Messages.SelectMany(m => m.Contents).OfType(); + + if (!toolCalls.Any()) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error($"The {nameof(modelResponse)} supplied for evaluation did not contain any tool calls (i.e., {nameof(FunctionCallContent)}s).")); + + return result; + } + + if (additionalContext?.OfType().FirstOrDefault() + is not ToolCallAccuracyEvaluatorContext context) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"A value of type {nameof(ToolCallAccuracyEvaluatorContext)} was not found in the {nameof(additionalContext)} collection.")); + + return result; + } + + if (context.ToolDefinitions.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied {nameof(ToolCallAccuracyEvaluatorContext)} did not contain any {nameof(ToolCallAccuracyEvaluatorContext.ToolDefinitions)}.")); + + return result; + } + + var toolDefinitionNames = new HashSet(context.ToolDefinitions.Select(td => td.Name)); + + if (toolCalls.Any(t => !toolDefinitionNames.Contains(t.Name))) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"The {nameof(modelResponse)} supplied for evaluation contained calls to tools that were not included in the supplied {nameof(ToolCallAccuracyEvaluatorContext)}.")); + + return result; + } + + List evaluationInstructions = GetEvaluationInstructions(messages, modelResponse, context); + + (ChatResponse evaluationResponse, TimeSpan evaluationDuration) = + await TimingHelper.ExecuteWithTimingAsync(() => + chatConfiguration.ChatClient.GetResponseAsync( + evaluationInstructions, + _chatOptions, + cancellationToken)).ConfigureAwait(false); + + _ = metric.TryParseEvaluationResponseWithTags(evaluationResponse, evaluationDuration); + metric.AddOrUpdateContext(context); + metric.Interpretation = metric.InterpretScore(); + return result; + } + + private static List GetEvaluationInstructions( + IEnumerable messages, + ChatResponse modelResponse, + ToolCallAccuracyEvaluatorContext context) + { +#pragma warning disable S103 // Lines should not be too long + const string SystemPrompt = + """ + # Instruction + ## Goal + ### You are an expert in evaluating the accuracy of a tool call considering relevance and potential usefulness including syntactic and semantic correctness of a proposed tool call from an intelligent system based on provided definition and data. Your goal will involve answering the questions below using the information provided. + - **Definition**: You are given a definition of the communication trait that is being evaluated to help guide your Score. + - **Data**: Your input data include CONVERSATION , TOOL CALL and TOOL DEFINITION. + - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. + """; +#pragma warning restore S103 + + List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; + + string renderedConversation = messages.RenderText(); + string renderedToolCallsAndResults = modelResponse.RenderToolCallsAndResultsAsJson(); + string renderedToolDefinitions = context.ToolDefinitions.RenderAsJson(); + +#pragma warning disable S103 // Lines should not be too long + string evaluationPrompt = + $$""" + # Definition + **Tool Call Accuracy** refers to the relevance and potential usefulness of a TOOL CALL in the context of an ongoing CONVERSATION and EXTRACTION of RIGHT PARAMETER VALUES from the CONVERSATION.It assesses how likely the TOOL CALL is to contribute meaningfully to the CONVERSATION and help address the user's needs. Focus on evaluating the potential value of the TOOL CALL within the specific context of the given CONVERSATION, without making assumptions beyond the provided information. + Consider the following factors in your evaluation: + + 1. Relevance: How well does the proposed tool call align with the current topic and flow of the conversation? + 2. Parameter Appropriateness: Do the parameters used in the TOOL CALL match the TOOL DEFINITION and are the parameters relevant to the latest user's query? + 3. Parameter Value Correctness: Are the parameters values used in the TOOL CALL present or inferred by CONVERSATION and relevant to the latest user's query? + 4. Potential Value: Is the information this tool call might provide likely to be useful in advancing the conversation or addressing the user expressed or implied needs? + 5. Context Appropriateness: Does the tool call make sense at this point in the conversation, given what has been discussed so far? + + + # Ratings + ## [Tool Call Accuracy: 0] (Irrelevant) + **Definition:** + 1. The TOOL CALL is not relevant and will not help resolve the user's need. + 2. TOOL CALL include parameters values that are not present or inferred from CONVERSATION. + 3. TOOL CALL has parameters that is not present in TOOL DEFINITION. + + ## [Tool Call Accuracy: 1] (Relevant) + **Definition:** + 1. The TOOL CALL is directly relevant and very likely to help resolve the user's need. + 2. TOOL CALL include parameters values that are present or inferred from CONVERSATION. + 3. TOOL CALL has parameters that is present in TOOL DEFINITION. + + # Data + CONVERSATION : {{renderedConversation}} + TOOL CALL: {{renderedToolCallsAndResults}} + TOOL DEFINITION: {{renderedToolDefinitions}} + + + # Tasks + ## Please provide your assessment Score for the previous CONVERSATION , TOOL CALL and TOOL DEFINITION based on the Definitions above. Your output should include the following information: + - **ThoughtChain**: To improve the reasoning process, think step by step and include a step-by-step explanation of your thought process as you analyze the data based on the definitions. Keep it brief and start your ThoughtChain with "Let's think step by step:". + - **Explanation**: a very short explanation of why you think the input Data should get that Score. + - **Score**: based on your previous analysis, provide your Score. The Score you give MUST be a integer score (i.e., "0", "1") based on the levels of the definitions. + + + ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. + # Output + """; +#pragma warning restore S103 + + evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); + + return evaluationInstructions; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs new file mode 100644 index 00000000000..d25e586163a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Evaluation.Quality; + +/// +/// Contextual information that the uses to evaluate an AI system's +/// effectiveness at using the tools supplied to it. +/// +/// +/// +/// measures how accurately an AI system uses tools by examining tool calls +/// (i.e., s) present in the supplied response to assess the relevance of these tool +/// calls to the conversation, the parameter correctness for these tool calls with regard to the tool definitions +/// supplied via , and the accuracy of the parameter value extraction from the supplied +/// conversation history. +/// +/// +/// Note that at the moment, only supports evaluating calls to tools that are +/// defined as s. Any other definitions that are supplied via +/// will be ignored. +/// +/// +[Experimental("AIEVAL001")] +public sealed class ToolCallAccuracyEvaluatorContext : EvaluationContext +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) + : base(name: ToolCallAccuracyContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + { + ToolDefinitions = [.. toolDefinitions]; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// The set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions will be ignored. + /// + /// + public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) + : this(toolDefinitions as IEnumerable) + { + } + + /// + /// Gets the unique that is used for + /// . + /// + public static string ToolCallAccuracyContextName => "Tool Definitions (Tool Call Accuracy)"; + + /// + /// Gets set of tool definitions (see ) that were used when generating the model + /// response that is being evaluated. + /// + /// + /// + /// measures how accurately an AI system uses tools by examining tool calls + /// (i.e., s) present in the supplied response to assess the relevance of these + /// tool calls to the conversation, the parameter correctness for these tool calls with regard to the tool + /// definitions supplied via , and the accuracy of the parameter value extraction from + /// the supplied conversation history. + /// + /// + /// Note that at the moment, only supports evaluating calls to tools that + /// are defined as s. Any other definitions that are supplied via + /// will be ignored. + /// + /// + public IReadOnlyList ToolDefinitions { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx index b38c691bbb3..6d73c8220e1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx @@ -52,7 +52,7 @@ export const App = () => { const classes = useStyles(); const { dataset, scoreSummary, selectedTags, clearFilters, searchValue, setSearchValue } = useReportContext(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const { renderMarkdown, setRenderMarkdown } = useReportContext(); + const { renderMarkdown, setRenderMarkdown, prettifyJson, setPrettifyJson } = useReportContext(); const { globalTags, filterableTags } = categorizeAndSortTags(dataset, scoreSummary.primaryResult.executionName); const toggleSettings = () => setIsSettingsOpen(!isSettingsOpen); @@ -127,6 +127,11 @@ export const App = () => { onChange={(_ev, data) => setRenderMarkdown(data.checked)} label={Render markdown for conversations} /> + setPrettifyJson(data.checked)} + label={Pretty print JSON content} + /> diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ConversationDetails.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ConversationDetails.tsx index 6acf38673de..9cf40a7a574 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ConversationDetails.tsx +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ConversationDetails.tsx @@ -18,7 +18,7 @@ export const ConversationDetails = ({ messages, model, usage, selectedMetric }: }) => { const classes = useStyles(); const [isExpanded, setIsExpanded] = useState(true); - const { renderMarkdown } = useReportContext(); + const { renderMarkdown, prettifyJson } = useReportContext(); const isUserSide = (role: string) => role.toLowerCase() === 'user' || role.toLowerCase() === 'system'; @@ -29,14 +29,33 @@ export const ConversationDetails = ({ messages, model, usage, selectedMetric }: usage?.totalTokenCount && `Total Tokens: ${usage.totalTokenCount}`, ].filter(Boolean).join(' • '); + const isValidJson = (text: string): { isValid: boolean; parsedJson?: any } => { + try { + const parsedJson = JSON.parse(text.trim()); + return { isValid: true, parsedJson }; + } catch { + return { isValid: false }; + } + }; + const renderContent = (content: AIContent) => { if (isTextContent(content)) { - return renderMarkdown ? - {content.text} : -
    {content.text}
    ; + const { isValid, parsedJson } = isValidJson(content.text); + if (isValid) { + const jsonContent = JSON.stringify(parsedJson, null, prettifyJson ? 2 : 0); + return
    {jsonContent}
    ; + } else { + return renderMarkdown ? + {content.text} : +
    {content.text}
    ; + } } else if (isImageContent(content)) { const imageUrl = (content as UriContent).uri || (content as DataContent).uri; return Content; + } else { + // For any other content type, display the serialized JSON + const jsonContent = JSON.stringify(content, null, prettifyJson ? 2 : 0); + return
    {jsonContent}
    ; } }; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ReportContext.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ReportContext.tsx index 64a1e4a3c20..74a645c70b7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ReportContext.tsx +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ReportContext.tsx @@ -11,6 +11,8 @@ export type ReportContextType = { selectScenarioLevel: (key: string) => void, renderMarkdown: boolean, setRenderMarkdown: (renderMarkdown: boolean) => void, + prettifyJson: boolean, + setPrettifyJson: (prettifyJson: boolean) => void, searchValue: string, setSearchValue: (searchValue: string) => void, selectedTags: string[], @@ -38,6 +40,10 @@ const defaultReportContext = createContext({ setRenderMarkdown: (_renderMarkdown: boolean) => { throw new Error("setRenderMarkdown function not implemented"); }, + prettifyJson: true, + setPrettifyJson: (_prettifyJson: boolean) => { + throw new Error("setPrettifyJson function not implemented"); + }, searchValue: '', setSearchValue: (_searchValue: string | undefined) => { throw new Error("setSearchValue function not implemented"); }, selectedTags: [], @@ -65,6 +71,7 @@ export const useReportContext = () => { const useProvideReportContext = (dataset: Dataset, scoreSummary: ScoreSummary): ReportContextType => { const [selectedScenarioLevel, setSelectedScenarioLevel] = useState(undefined); const [renderMarkdown, setRenderMarkdown] = useState(true); + const [prettifyJson, setPrettifyJson] = useState(true); const [selectedTags, setSelectedTags] = useState([]); const [searchValue, setSearchValue] = useState(""); @@ -114,7 +121,7 @@ const useProvideReportContext = (dataset: Dataset, scoreSummary: ScoreSummary): } return null; - }; + }; return srch(node); } @@ -126,6 +133,8 @@ const useProvideReportContext = (dataset: Dataset, scoreSummary: ScoreSummary): selectScenarioLevel, renderMarkdown, setRenderMarkdown, + prettifyJson, + setPrettifyJson, searchValue, setSearchValue, selectedTags, @@ -133,4 +142,4 @@ const useProvideReportContext = (dataset: Dataset, scoreSummary: ScoreSummary): clearFilters, filterTree, }; -}; \ No newline at end of file +}; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/EvaluationMetricExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/EvaluationMetricExtensions.cs index 8a0ffcbd31b..f6ab393750b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/EvaluationMetricExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/EvaluationMetricExtensions.cs @@ -73,7 +73,7 @@ internal static EvaluationMetricInterpretation InterpretContentSafetyScore( : new EvaluationMetricInterpretation( rating, failed: true, - reason: $"{metric.Name} is {passValue}."); + reason: $"{metric.Name} is not {passValue}."); } internal static void LogJsonData(this EvaluationMetric metric, string data) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluator.cs index f65ddae4662..24408d5a1ad 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluator.cs @@ -87,6 +87,6 @@ private static GroundednessProEvaluatorContext GetRelevantContext( } throw new InvalidOperationException( - $"A value of type '{nameof(GroundednessProEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection."); + $"A value of type {nameof(GroundednessProEvaluatorContext)} was not found in the {nameof(additionalContext)} collection."); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluator.cs index 06019969345..4b3fe84cb4e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluator.cs @@ -91,6 +91,6 @@ private static UngroundedAttributesEvaluatorContext GetRelevantContext( } throw new InvalidOperationException( - $"A value of type '{nameof(UngroundedAttributesEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection."); + $"A value of type {nameof(UngroundedAttributesEvaluatorContext)} was not found in the {nameof(additionalContext)} collection."); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs new file mode 100644 index 00000000000..134d6a50f32 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs @@ -0,0 +1,280 @@ +// 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.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Quality; +using Microsoft.Extensions.AI.Evaluation.Reporting; +using Microsoft.Extensions.AI.Evaluation.Reporting.Storage; +using Microsoft.Extensions.AI.Evaluation.Tests; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Integration.Tests; + +[Experimental("AIEVAL001")] +public class AgentQualityEvaluatorTests +{ + private static readonly ChatOptions? _chatOptions; + private static readonly ChatOptions? _chatOptionsWithTools; + private static readonly ReportingConfiguration? _agentQualityReportingConfiguration; + private static readonly ReportingConfiguration? _needsContextReportingConfiguration; + + static AgentQualityEvaluatorTests() + { + if (Settings.Current.Configured) + { + _chatOptions = + new ChatOptions + { + Temperature = 0.0f, + ResponseFormat = ChatResponseFormat.Text + }; + + _chatOptionsWithTools = + new ChatOptions + { + Temperature = 0.0f, + ResponseFormat = ChatResponseFormat.Text, + Tools = [AIFunctionFactory.Create(GetOrders), AIFunctionFactory.Create(GetOrderStatus)] + }; + + ChatConfiguration chatConfiguration = Setup.CreateChatConfiguration(); + ChatClientMetadata? clientMetadata = chatConfiguration.ChatClient.GetService(); + + IChatClient chatClient = chatConfiguration.ChatClient; + IChatClient chatClientWithToolCalling = chatClient.AsBuilder().UseFunctionInvocation().Build(); + ChatConfiguration chatConfigurationWithToolCalling = new ChatConfiguration(chatClientWithToolCalling); + + string version = $"Product Version: {Constants.Version}"; + string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; + string projectName = $"Project: Integration Tests"; + string testClass = $"Test Class: {nameof(AgentQualityEvaluatorTests)}"; + string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; + string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + string temperature = $"Temperature: {_chatOptionsWithTools.Temperature}"; + string usesContext = $"Feature: Context"; + + IEvaluator toolCallAccuracyEvaluator = new ToolCallAccuracyEvaluator(); + IEvaluator taskAdherenceEvaluator = new TaskAdherenceEvaluator(); + IEvaluator intentResolutionEvaluator = new IntentResolutionEvaluator(); + + _agentQualityReportingConfiguration = + DiskBasedReportingConfiguration.Create( + storageRootPath: Settings.Current.StorageRootPath, + evaluators: [taskAdherenceEvaluator, intentResolutionEvaluator], + chatConfiguration: chatConfigurationWithToolCalling, + executionName: Constants.Version, + tags: [version, date, projectName, testClass, provider, model, temperature]); + + _needsContextReportingConfiguration = + DiskBasedReportingConfiguration.Create( + storageRootPath: Settings.Current.StorageRootPath, + evaluators: [toolCallAccuracyEvaluator, taskAdherenceEvaluator, intentResolutionEvaluator], + chatConfiguration: chatConfigurationWithToolCalling, + executionName: Constants.Version, + tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); + } + } + + [ConditionalFact] + public async Task ToolDefinitionsAreNotNeededAndNotPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _agentQualityReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(AgentQualityEvaluatorTests)}.{nameof(ToolDefinitionsAreNotNeededAndNotPassed)}"); + + (IEnumerable messages, ChatResponse response) = + await GetConversationWithoutToolsAsync(scenarioRun.ChatConfiguration!.ChatClient); + + EvaluationResult result = await scenarioRun.EvaluateAsync(messages, response); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(2, result.Metrics.Count); + Assert.True(result.TryGet(TaskAdherenceEvaluator.TaskAdherenceMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(IntentResolutionEvaluator.IntentResolutionMetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task ToolDefinitionsAreNotNeededButPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _agentQualityReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(AgentQualityEvaluatorTests)}.{nameof(ToolDefinitionsAreNotNeededButPassed)}"); + + (IEnumerable messages, ChatResponse response) = + await GetConversationWithoutToolsAsync(scenarioRun.ChatConfiguration!.ChatClient); + + var toolDefinitionsForTaskAdherenceEvaluator = + new TaskAdherenceEvaluatorContext(toolDefinitions: _chatOptionsWithTools.Tools!); + + var toolDefinitionsForIntentResolution = + new IntentResolutionEvaluatorContext(toolDefinitions: _chatOptionsWithTools.Tools!); + + EvaluationResult result = + await scenarioRun.EvaluateAsync( + messages, + response, + additionalContext: [toolDefinitionsForTaskAdherenceEvaluator, toolDefinitionsForIntentResolution]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(2, result.Metrics.Count); + Assert.True(result.TryGet(TaskAdherenceEvaluator.TaskAdherenceMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(IntentResolutionEvaluator.IntentResolutionMetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task ToolDefinitionsAreNeededButNotPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _needsContextReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(AgentQualityEvaluatorTests)}.{nameof(ToolDefinitionsAreNeededButNotPassed)}"); + + (IEnumerable messages, ChatResponse response) = + await GetConversationWithToolsAsync(scenarioRun.ChatConfiguration!.ChatClient); + + EvaluationResult result = await scenarioRun.EvaluateAsync(messages, response); + + Assert.True( + result.Metrics.Values.All(m => m.ContainsDiagnostics(d => d.Severity is EvaluationDiagnosticSeverity.Error)), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(ToolCallAccuracyEvaluator.ToolCallAccuracyMetricName, out BooleanMetric? _)); + Assert.True(result.TryGet(TaskAdherenceEvaluator.TaskAdherenceMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(IntentResolutionEvaluator.IntentResolutionMetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task ToolDefinitionsAreNeededAndPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _needsContextReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(AgentQualityEvaluatorTests)}.{nameof(ToolDefinitionsAreNeededAndPassed)}"); + + (IEnumerable messages, ChatResponse response) = + await GetConversationWithToolsAsync(scenarioRun.ChatConfiguration!.ChatClient); + + var toolDefinitionsForToolCallAccuracyEvaluator = + new ToolCallAccuracyEvaluatorContext(toolDefinitions: _chatOptionsWithTools.Tools!); + + var toolDefinitionsForTaskAdherenceEvaluator = + new TaskAdherenceEvaluatorContext(toolDefinitions: _chatOptionsWithTools.Tools!); + + var toolDefinitionsForIntentResolutionEvaluator = + new IntentResolutionEvaluatorContext(toolDefinitions: _chatOptionsWithTools.Tools!); + + EvaluationResult result = + await scenarioRun.EvaluateAsync( + messages, + response, + additionalContext: [ + toolDefinitionsForToolCallAccuracyEvaluator, + toolDefinitionsForTaskAdherenceEvaluator, + toolDefinitionsForIntentResolutionEvaluator]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(ToolCallAccuracyEvaluator.ToolCallAccuracyMetricName, out BooleanMetric? _)); + Assert.True(result.TryGet(TaskAdherenceEvaluator.TaskAdherenceMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(IntentResolutionEvaluator.IntentResolutionMetricName, out NumericMetric? _)); + } + + private static async Task<(IEnumerable messages, ChatResponse response)> + GetConversationWithoutToolsAsync(IChatClient chatClient) + { + List messages = + [ + "You are a friendly and helpful assistant that can answer questions.".ToSystemMessage(), + "Hi, could you help me figure out the correct pronunciation for the word rendezvous?".ToUserMessage() + ]; + + ChatResponse response = await chatClient.GetResponseAsync(messages, _chatOptions); + return (messages, response); + } + + private static async Task<(IEnumerable messages, ChatResponse response)> + GetConversationWithToolsAsync(IChatClient chatClient) + { + List messages = + [ + "You are a friendly and helpful customer service agent.".ToSystemMessage(), + "Hi, I need help with the last 2 orders on my account #888. Could you please update me on their status?".ToUserMessage() + ]; + + ChatResponse response = await chatClient.GetResponseAsync(messages, _chatOptionsWithTools); + return (messages, response); + } + + [Description("Gets the orders for a customer")] + private static IReadOnlyList GetOrders(int accountNumber) + { + if (accountNumber != 888) + { + throw new InvalidOperationException($"Account number {accountNumber} is not valid."); + } + + return [new Order(123), new Order(124)]; + } + + [Description("Gets the delivery status of an order")] + private static OrderStatus GetOrderStatus(int orderId) + { + if (orderId == 123) + { + return new OrderStatus(orderId, "shipped", DateTime.Now.AddDays(1)); + } + else if (orderId == 124) + { + return new OrderStatus(orderId, "delayed", DateTime.Now.AddDays(10)); + } + else + { + throw new InvalidOperationException($"Order with ID {orderId} not found."); + } + } + + private record Order(int OrderId) + { + } + + private record OrderStatus(int OrderId, string Status, DateTime ExpectedDelivery) + { + } + + [MemberNotNull(nameof(_chatOptionsWithTools))] + [MemberNotNull(nameof(_agentQualityReportingConfiguration))] + [MemberNotNull(nameof(_needsContextReportingConfiguration))] + private static void SkipIfNotConfigured() + { + if (!Settings.Current.Configured) + { + throw new SkipTestException("Test is not configured"); + } + + Assert.NotNull(_chatOptionsWithTools); + Assert.NotNull(_agentQualityReportingConfiguration); + Assert.NotNull(_needsContextReportingConfiguration); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs index b56a2673b60..ecec3ad51e5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs @@ -278,6 +278,7 @@ await scenarioRun.EvaluateAsync( ReferenceEquals(context4, retrievedContextChunksForRetrievalEvaluator)); } + [MemberNotNull(nameof(_chatOptions))] [MemberNotNull(nameof(_qualityReportingConfiguration))] [MemberNotNull(nameof(_needsContextReportingConfiguration))] private static void SkipIfNotConfigured() @@ -287,6 +288,7 @@ private static void SkipIfNotConfigured() throw new SkipTestException("Test is not configured"); } + Assert.NotNull(_chatOptions); Assert.NotNull(_qualityReportingConfiguration); Assert.NotNull(_needsContextReportingConfiguration); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs index 609646c8061..630adbffd8e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs @@ -548,6 +548,7 @@ await _mixedQualityAndSafetyReportingConfiguration.CreateScenarioRunAsync( Assert.True(result.TryGet(ViolenceEvaluator.ViolenceMetricName, out NumericMetric? _)); } + [MemberNotNull(nameof(_chatOptions))] [MemberNotNull(nameof(_contentSafetyReportingConfiguration))] [MemberNotNull(nameof(_imageContentSafetyReportingConfiguration))] [MemberNotNull(nameof(_codeVulnerabilityReportingConfiguration))] @@ -559,6 +560,7 @@ private static void SkipIfNotConfigured() throw new SkipTestException("Test is not configured"); } + Assert.NotNull(_chatOptions); Assert.NotNull(_contentSafetyReportingConfiguration); Assert.NotNull(_codeVulnerabilityReportingConfiguration); Assert.NotNull(_imageContentSafetyReportingConfiguration); diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs index 30cb541e700..388ba1f1415 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs @@ -14,16 +14,23 @@ internal static class Setup Environment.GetEnvironmentVariable("AITESTING_OFFLINE") == "1"; internal static ChatConfiguration CreateChatConfiguration() + { + AzureOpenAIClient azureOpenAIClient = GetAzureOpenAIClient(); + IChatClient chatClient = azureOpenAIClient.GetChatClient(Settings.Current.DeploymentName).AsIChatClient(); + return new ChatConfiguration(chatClient); + } + + private static AzureOpenAIClient GetAzureOpenAIClient() { var endpoint = new Uri(Settings.Current.Endpoint); AzureOpenAIClientOptions options = new(); var credential = new ChainedTokenCredential(new AzureCliCredential(), new DefaultAzureCredential()); - AzureOpenAIClient azureClient = + + AzureOpenAIClient azureOpenAIClient = OfflineOnly ? new AzureOpenAIClient(endpoint, new ApiKeyCredential("Bogus"), options) : new AzureOpenAIClient(endpoint, credential, options); - IChatClient chatClient = azureClient.GetChatClient(Settings.Current.DeploymentName).AsIChatClient(); - return new ChatConfiguration(chatClient); + return azureOpenAIClient; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/IntentResolutionRatingTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/IntentResolutionRatingTests.cs new file mode 100644 index 00000000000..da839387e20 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/IntentResolutionRatingTests.cs @@ -0,0 +1,324 @@ +// 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 Microsoft.Extensions.AI.Evaluation.Quality; +using Microsoft.Extensions.AI.Evaluation.Quality.JsonSerialization; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Tests; + +public class IntentResolutionRatingTests +{ + [Fact] + public void JsonIsValid() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5 + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.Equal(5, rating.ResolutionScore); + Assert.Equal("The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", rating.Explanation); + Assert.Equal("provide a comprehensive chocolate cake recipe", rating.AgentPerceivedIntent); + Assert.Equal("bake a chocolate cake", rating.ActualUserIntent); + Assert.True(rating.ConversationHasIntent); + Assert.True(rating.CorrectIntentDetected); + Assert.True(rating.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonIsSurroundedWithMarkdownSyntax() + { + string json = + """ + + ``` + { + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5, + } + ``` + + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.Equal(5, rating.ResolutionScore); + Assert.Equal("The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", rating.Explanation); + Assert.Equal("provide a comprehensive chocolate cake recipe", rating.AgentPerceivedIntent); + Assert.Equal("bake a chocolate cake", rating.ActualUserIntent); + Assert.True(rating.ConversationHasIntent); + Assert.True(rating.CorrectIntentDetected); + Assert.True(rating.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonIsSurroundedWithMarkdownSyntaxWithJsonPrefix() + { + string json = + """ + + ```json + { + "resolution_score": 5, + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true + } + ``` + + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.Equal(5, rating.ResolutionScore); + Assert.Equal("The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", rating.Explanation); + Assert.Equal("provide a comprehensive chocolate cake recipe", rating.AgentPerceivedIntent); + Assert.Equal("bake a chocolate cake", rating.ActualUserIntent); + Assert.True(rating.ConversationHasIntent); + Assert.True(rating.CorrectIntentDetected); + Assert.True(rating.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonCanBeRoundTripped() + { + IntentResolutionRating rating = + new IntentResolutionRating( + resolutionScore: 1, + explanation: "explanation", + agentPerceivedIntent: "perceived intent", + actualUserIntent: "actual intent", + conversationHasIntent: false, + correctIntentDetected: true, + intentResolved: true); + + string json = JsonSerializer.Serialize(rating, SerializerContext.Default.IntentResolutionRating); + IntentResolutionRating deserialized = IntentResolutionRating.FromJson(json); + + Assert.Equal(rating.ResolutionScore, deserialized.ResolutionScore); + Assert.Equal(rating.Explanation, deserialized.Explanation); + Assert.Equal(rating.AgentPerceivedIntent, deserialized.AgentPerceivedIntent); + Assert.Equal(rating.ActualUserIntent, deserialized.ActualUserIntent); + Assert.Equal(rating.ConversationHasIntent, deserialized.ConversationHasIntent); + Assert.Equal(rating.CorrectIntentDetected, deserialized.CorrectIntentDetected); + Assert.Equal(rating.IntentResolved, deserialized.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void InconclusiveJsonCanBeRoundTripped() + { + IntentResolutionRating rating = IntentResolutionRating.Inconclusive; + + string json = JsonSerializer.Serialize(rating, SerializerContext.Default.IntentResolutionRating); + IntentResolutionRating deserialized = IntentResolutionRating.FromJson(json); + + Assert.Equal(rating.ResolutionScore, deserialized.ResolutionScore); + Assert.Equal(rating.Explanation, deserialized.Explanation); + Assert.Equal(rating.AgentPerceivedIntent, deserialized.AgentPerceivedIntent); + Assert.Equal(rating.ActualUserIntent, deserialized.ActualUserIntent); + Assert.Equal(rating.ConversationHasIntent, deserialized.ConversationHasIntent); + Assert.Equal(rating.CorrectIntentDetected, deserialized.CorrectIntentDetected); + Assert.Equal(rating.IntentResolved, deserialized.IntentResolved); + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithNegativeScoreIsInconclusive() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": -1 + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithZeroScoreIsInconclusive() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 0 + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithExcessivelyHighScoreIsInconclusive() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 200 + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithAdditionalHallucinatedPropertyIsProcessedCorrectly() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "hallucinated_property": "Some hallucinated text.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5, + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.Equal(5, rating.ResolutionScore); + Assert.Equal("The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", rating.Explanation); + Assert.Equal("provide a comprehensive chocolate cake recipe", rating.AgentPerceivedIntent); + Assert.Equal("bake a chocolate cake", rating.ActualUserIntent); + Assert.True(rating.ConversationHasIntent); + Assert.True(rating.CorrectIntentDetected); + Assert.True(rating.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonWithDuplicatePropertyUsesLastValue() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "explanation": "Duplicate explanation.", + "conversation_has_intent": true, + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5, + } + """; + + IntentResolutionRating rating = IntentResolutionRating.FromJson(json); + + Assert.Equal(5, rating.ResolutionScore); + Assert.Equal("Duplicate explanation.", rating.Explanation); + Assert.Equal("provide a comprehensive chocolate cake recipe", rating.AgentPerceivedIntent); + Assert.Equal("bake a chocolate cake", rating.ActualUserIntent); + Assert.True(rating.ConversationHasIntent); + Assert.True(rating.CorrectIntentDetected); + Assert.True(rating.IntentResolved); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonWithSemicolonsInsteadOfCommasThrowsException() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake."; + "conversation_has_intent": true; + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe"; + "actual_user_intent": "bake a chocolate cake"; + "correct_intent_detected": true; + "intent_resolved": true; + "resolution_score": 5 + } + """; + + Assert.Throws(() => IntentResolutionRating.FromJson(json)); + } + + [Fact] + public void JsonWithMissingPropertiesThrowsException() + { + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "intent_resolved": true, + "resolution_score": 5 + } + """; + + Assert.Throws(() => IntentResolutionRating.FromJson(json)); + } + + [Fact] + public void JsonWithIncorrectPropertyValueTypeThrowsException() + { + // Incorrect property value (string instead of boolean for conversation_has_intent). + string json = + """ + { + "explanation": "The response delivers a complete and precise recipe, fully addressing the user's query about baking a chocolate cake.", + "conversation_has_intent": "A string value", + "agent_perceived_intent": "provide a comprehensive chocolate cake recipe", + "actual_user_intent": "bake a chocolate cake", + "correct_intent_detected": true, + "intent_resolved": true, + "resolution_score": 5, + } + """; + + Assert.Throws(() => IntentResolutionRating.FromJson(json)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessEvaluatorRatingTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessEvaluatorRatingTests.cs deleted file mode 100644 index db7cc6e3a26..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessEvaluatorRatingTests.cs +++ /dev/null @@ -1,148 +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.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.Json; -using Microsoft.Extensions.AI.Evaluation.Quality; -using Xunit; - -namespace Microsoft.Extensions.AI.Evaluation.Tests; - -[Experimental("AIEVAL001")] -public class RelevanceTruthAndCompletenessEvaluatorRatingTests -{ - [Fact] - public void JsonIsValid() - { - string json = """ - {"relevance": 1, "truth": 5, "completeness": 4} - """; - - var rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - - Assert.Equal(1, rating.Relevance); - Assert.Equal(5, rating.Truth); - Assert.Equal(4, rating.Completeness); - Assert.Null(rating.RelevanceReasoning); - Assert.Null(rating.TruthReasoning); - Assert.Null(rating.CompletenessReasoning); - Assert.Empty(rating.RelevanceReasons); - Assert.Empty(rating.TruthReasons); - Assert.Empty(rating.CompletenessReasons); - Assert.False(rating.IsInconclusive); - } - - [Fact] - public void JsonIsSurroundedWithMarkdownSyntax() - { - string json = """ - - ``` - {"relevance": 1, "truth": 5, "completeness": 4} - ``` - - """; - - var rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - - Assert.Equal(1, rating.Relevance); - Assert.Equal(5, rating.Truth); - Assert.Equal(4, rating.Completeness); - Assert.Null(rating.RelevanceReasoning); - Assert.Null(rating.TruthReasoning); - Assert.Null(rating.CompletenessReasoning); - Assert.Empty(rating.RelevanceReasons); - Assert.Empty(rating.TruthReasons); - Assert.Empty(rating.CompletenessReasons); - Assert.False(rating.IsInconclusive); - } - - [Fact] - public void JsonIsSurroundedWithMarkdownSyntaxWithJsonPrefix() - { - string json = """ - - ```json - {"relevance": 1, "truth": 5, "completeness": 4} - ``` - - """; - - var rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - - Assert.Equal(1, rating.Relevance); - Assert.Equal(5, rating.Truth); - Assert.Equal(4, rating.Completeness); - Assert.Null(rating.RelevanceReasoning); - Assert.Null(rating.TruthReasoning); - Assert.Null(rating.CompletenessReasoning); - Assert.Empty(rating.RelevanceReasons); - Assert.Empty(rating.TruthReasons); - Assert.Empty(rating.CompletenessReasons); - Assert.False(rating.IsInconclusive); - } - - [Fact] - public void JsonCanBeRoundTripped() - { - var rating = new RelevanceTruthAndCompletenessEvaluator.Rating( - relevance: 1, - relevanceReasoning: "The response is not relevant to the request.", - relevanceReasons: ["Reason 1", "Reason 2"], - truth: 5, - truthReasoning: "The response is mostly true.", - truthReasons: ["Reason 1", "Reason 2"], - completeness: 4, - completenessReasoning: "The response is mostly complete.", - completenessReasons: ["Reason 1", "Reason 2"]); - - string json = JsonSerializer.Serialize(rating, RelevanceTruthAndCompletenessEvaluator.SerializerContext.Default.Rating); - var deserialized = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.Equal(rating.Relevance, deserialized.Relevance); - Assert.Equal(rating.RelevanceReasoning, deserialized.RelevanceReasoning); - Assert.True(rating.RelevanceReasons.SequenceEqual(deserialized.RelevanceReasons)); - Assert.Equal(rating.Truth, deserialized.Truth); - Assert.Equal(rating.TruthReasoning, deserialized.TruthReasoning); - Assert.True(rating.TruthReasons.SequenceEqual(deserialized.TruthReasons)); - Assert.Equal(rating.Completeness, deserialized.Completeness); - Assert.Equal(rating.CompletenessReasoning, deserialized.CompletenessReasoning); - Assert.True(rating.CompletenessReasons.SequenceEqual(deserialized.CompletenessReasons)); - Assert.False(rating.IsInconclusive); - } - - [Fact] - public void JsonContainsInconclusiveMetrics() - { - string json = """{"relevance": -1, "truth": 4, "completeness": 7}"""; - var rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - - json = """{"relevance": 0, "truth": -1, "completeness": 3}"""; - rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - - json = """{"relevance": 0, "truth": 4, "completeness": -5}"""; - rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - - json = """{"relevance": 10, "truth": 4, "completeness": 3}"""; - rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - - json = """{"relevance": 0, "truth": 5, "completeness": 3}"""; - rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - - json = """{"relevance": 1, "truth": 4, "completeness": 6}"""; - rating = RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json); - Assert.True(rating.IsInconclusive); - } - - [Fact] - public void JsonContainsErrors() - { - string json = """{"relevance": 0, "truth": 2 ;"completeness": 3}"""; - Assert.Throws(() => RelevanceTruthAndCompletenessEvaluator.Rating.FromJson(json)); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessRatingTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessRatingTests.cs new file mode 100644 index 00000000000..6be1a8ba142 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/RelevanceTruthAndCompletenessRatingTests.cs @@ -0,0 +1,382 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.AI.Evaluation.Quality; +using Microsoft.Extensions.AI.Evaluation.Quality.JsonSerialization; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Tests; + +public class RelevanceTruthAndCompletenessRatingTests +{ + [Fact] + public void JsonIsValid() + { + string json = + """ + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(1, rating.Relevance); + Assert.Equal(1, rating.Truth); + Assert.Equal(1, rating.Completeness); + Assert.Equal("The reason for the relevance score", rating.RelevanceReasoning); + Assert.Equal("The reason for the truth score", rating.TruthReasoning); + Assert.Equal("The reason for the completeness score", rating.CompletenessReasoning); + Assert.Single(rating.RelevanceReasons); + Assert.Equal("relevance_reason_distant_topic", rating.RelevanceReasons[0]); + Assert.Equal(3, rating.TruthReasons.Length); + Assert.Contains("truth_reason_incorrect_information", rating.TruthReasons); + Assert.Contains("truth_reason_outdated_information", rating.TruthReasons); + Assert.Contains("truth_reason_misleading_incorrectforintent", rating.TruthReasons); + Assert.Equal(2, rating.CompletenessReasons.Length); + Assert.Contains("completeness_reason_no_solution", rating.CompletenessReasons); + Assert.Contains("completeness_reason_genericsolution_missingcode", rating.CompletenessReasons); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonIsSurroundedWithMarkdownSyntax() + { + string json = + """ + + ``` + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": [], + "truth": 4, + "truthReasoning": "The reason for the truth score", + "truthReasons": [], + "completeness": 5, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": [] + } + ``` + + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(1, rating.Relevance); + Assert.Equal(4, rating.Truth); + Assert.Equal(5, rating.Completeness); + Assert.Equal("The reason for the relevance score", rating.RelevanceReasoning); + Assert.Equal("The reason for the truth score", rating.TruthReasoning); + Assert.Equal("The reason for the completeness score", rating.CompletenessReasoning); + Assert.Empty(rating.RelevanceReasons); + Assert.Empty(rating.TruthReasons); + Assert.Empty(rating.CompletenessReasons); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonIsSurroundedWithMarkdownSyntaxWithJsonPrefix() + { + string json = + """ + + ```json + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 3, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_misleading_incorrectforintent"], + "completeness": 2, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution"], + } + ``` + + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(1, rating.Relevance); + Assert.Equal(3, rating.Truth); + Assert.Equal(2, rating.Completeness); + Assert.Equal("The reason for the relevance score", rating.RelevanceReasoning); + Assert.Equal("The reason for the truth score", rating.TruthReasoning); + Assert.Equal("The reason for the completeness score", rating.CompletenessReasoning); + Assert.Single(rating.RelevanceReasons); + Assert.Equal("relevance_reason_distant_topic", rating.RelevanceReasons[0]); + Assert.Single(rating.TruthReasons); + Assert.Contains("truth_reason_misleading_incorrectforintent", rating.TruthReasons); + Assert.Single(rating.CompletenessReasons); + Assert.Contains("completeness_reason_no_solution", rating.CompletenessReasons); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonCanBeRoundTripped() + { + RelevanceTruthAndCompletenessRating rating = + new RelevanceTruthAndCompletenessRating( + relevance: 1, + relevanceReasoning: "The response is not relevant to the request.", + relevanceReasons: ["Reason 1", "Reason 2"], + truth: 5, + truthReasoning: "The response is mostly true.", + truthReasons: ["Reason 1", "Reason 2"], + completeness: 4, + completenessReasoning: "The response is mostly complete.", + completenessReasons: ["Reason 1", "Reason 2"]); + + string json = JsonSerializer.Serialize(rating, SerializerContext.Default.RelevanceTruthAndCompletenessRating); + RelevanceTruthAndCompletenessRating deserialized = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(rating.Relevance, deserialized.Relevance); + Assert.Equal(rating.RelevanceReasoning, deserialized.RelevanceReasoning); + Assert.True(rating.RelevanceReasons.SequenceEqual(deserialized.RelevanceReasons)); + Assert.Equal(rating.Truth, deserialized.Truth); + Assert.Equal(rating.TruthReasoning, deserialized.TruthReasoning); + Assert.True(rating.TruthReasons.SequenceEqual(deserialized.TruthReasons)); + Assert.Equal(rating.Completeness, deserialized.Completeness); + Assert.Equal(rating.CompletenessReasoning, deserialized.CompletenessReasoning); + Assert.True(rating.CompletenessReasons.SequenceEqual(deserialized.CompletenessReasons)); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void InconclusiveJsonCanBeRoundTripped() + { + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.Inconclusive; + + string json = JsonSerializer.Serialize(rating, SerializerContext.Default.RelevanceTruthAndCompletenessRating); + RelevanceTruthAndCompletenessRating deserialized = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(rating.Relevance, deserialized.Relevance); + Assert.Equal(rating.RelevanceReasoning, deserialized.RelevanceReasoning); + Assert.True(rating.RelevanceReasons.SequenceEqual(deserialized.RelevanceReasons)); + Assert.Equal(rating.Truth, deserialized.Truth); + Assert.Equal(rating.TruthReasoning, deserialized.TruthReasoning); + Assert.True(rating.TruthReasons.SequenceEqual(deserialized.TruthReasons)); + Assert.Equal(rating.Completeness, deserialized.Completeness); + Assert.Equal(rating.CompletenessReasoning, deserialized.CompletenessReasoning); + Assert.True(rating.CompletenessReasons.SequenceEqual(deserialized.CompletenessReasons)); + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithNegativeScoreIsInconclusive() + { + string json = + """ + { + "relevance": -1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithZeroScoreIsInconclusive() + { + string json = + """ + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 0, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithExcessivelyHighScoreIsInconclusive() + { + string json = + """ + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 100, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.True(rating.IsInconclusive); + } + + [Fact] + public void JsonWithAdditionalHallucinatedPropertyIsProcessedCorrectly() + { + string json = + """ + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "hallucinatedProperty": "Some hallucinated text", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(1, rating.Relevance); + Assert.Equal(1, rating.Truth); + Assert.Equal(1, rating.Completeness); + Assert.Equal("The reason for the relevance score", rating.RelevanceReasoning); + Assert.Equal("The reason for the truth score", rating.TruthReasoning); + Assert.Equal("The reason for the completeness score", rating.CompletenessReasoning); + Assert.Single(rating.RelevanceReasons); + Assert.Equal("relevance_reason_distant_topic", rating.RelevanceReasons[0]); + Assert.Equal(3, rating.TruthReasons.Length); + Assert.Contains("truth_reason_incorrect_information", rating.TruthReasons); + Assert.Contains("truth_reason_outdated_information", rating.TruthReasons); + Assert.Contains("truth_reason_misleading_incorrectforintent", rating.TruthReasons); + Assert.Equal(2, rating.CompletenessReasons.Length); + Assert.Contains("completeness_reason_no_solution", rating.CompletenessReasons); + Assert.Contains("completeness_reason_genericsolution_missingcode", rating.CompletenessReasons); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonWithDuplicatePropertyUsesLastValue() + { + string json = + """ + { + "relevance": 1, + "relevanceReasoning": "The reason for the relevance score", + "relevanceReasoning": "Duplicate reason for the relevance score", + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + RelevanceTruthAndCompletenessRating rating = RelevanceTruthAndCompletenessRating.FromJson(json); + + Assert.Equal(1, rating.Relevance); + Assert.Equal(1, rating.Truth); + Assert.Equal(1, rating.Completeness); + Assert.Equal("Duplicate reason for the relevance score", rating.RelevanceReasoning); + Assert.Equal("The reason for the truth score", rating.TruthReasoning); + Assert.Equal("The reason for the completeness score", rating.CompletenessReasoning); + Assert.Single(rating.RelevanceReasons); + Assert.Equal("relevance_reason_distant_topic", rating.RelevanceReasons[0]); + Assert.Equal(3, rating.TruthReasons.Length); + Assert.Contains("truth_reason_incorrect_information", rating.TruthReasons); + Assert.Contains("truth_reason_outdated_information", rating.TruthReasons); + Assert.Contains("truth_reason_misleading_incorrectforintent", rating.TruthReasons); + Assert.Equal(2, rating.CompletenessReasons.Length); + Assert.Contains("completeness_reason_no_solution", rating.CompletenessReasons); + Assert.Contains("completeness_reason_genericsolution_missingcode", rating.CompletenessReasons); + Assert.False(rating.IsInconclusive); + } + + [Fact] + public void JsonWithSemicolonsInsteadOfCommasThrowsException() + { + string json = + """ + { + "relevance": 1; + "relevanceReasoning": "The reason for the relevance score"; + "relevanceReasons": ["relevance_reason_distant_topic"]; + "truth": 1; + "truthReasoning": "The reason for the truth score"; + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"]; + "completeness": 1; + "completenessReasoning": "The reason for the completeness score"; + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"]; + } + """; + + Assert.Throws(() => RelevanceTruthAndCompletenessRating.FromJson(json)); + } + + [Fact] + public void JsonWithMissingPropertiesThrowsException() + { + string json = + """ + { + "relevance": 1, + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + } + """; + + Assert.Throws(() => RelevanceTruthAndCompletenessRating.FromJson(json)); + } + + [Fact] + public void JsonWithIncorrectPropertyValueTypeThrowsException() + { + // Incorrect property value (integer instead of string for relevanceReasoning). + string json = + """ + { + "relevance": 1, + "relevanceReasoning": 6, + "relevanceReasons": ["relevance_reason_distant_topic"], + "truth": 1, + "truthReasoning": "The reason for the truth score", + "truthReasons": ["truth_reason_incorrect_information", "truth_reason_outdated_information", "truth_reason_misleading_incorrectforintent"], + "completeness": 1, + "completenessReasoning": "The reason for the completeness score", + "completenessReasons": ["completeness_reason_no_solution", "completeness_reason_genericsolution_missingcode"], + } + """; + + Assert.Throws(() => RelevanceTruthAndCompletenessRating.FromJson(json)); + } +} From adb96dfec7af5b3e9e2a55ae521467c9bbdb27d2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 18 Jun 2025 11:54:12 -0400 Subject: [PATCH 150/472] Allow a CachingChatClient to control per-request caching (#6524) --- .../ChatCompletion/CachingChatClient.cs | 23 +++++++--- .../Microsoft.Extensions.AI.json | 4 ++ .../DistributedCachingChatClientTest.cs | 42 +++++++++++++++---- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs index 211fc39ec85..2923b0ad62d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs @@ -51,7 +51,7 @@ public override Task GetResponseAsync( { _ = Throw.IfNull(messages); - return UseCaching(options) ? + return EnableCaching(messages, options) ? GetCachedResponseAsync(messages, options, cancellationToken) : base.GetResponseAsync(messages, options, cancellationToken); } @@ -79,7 +79,7 @@ public override IAsyncEnumerable GetStreamingResponseAsync( { _ = Throw.IfNull(messages); - return UseCaching(options) ? + return EnableCaching(messages, options) ? GetCachedStreamingResponseAsync(messages, options, cancellationToken) : base.GetStreamingResponseAsync(messages, options, cancellationToken); } @@ -196,12 +196,25 @@ private async IAsyncEnumerable GetCachedStreamingResponseAsy /// is . protected abstract Task WriteCacheStreamingAsync(string key, IReadOnlyList value, CancellationToken cancellationToken); - /// Determine whether to use caching with the request. - private static bool UseCaching(ChatOptions? options) + /// Determines whether caching should be used with the specified request. + /// The sequence of chat messages included in the request. + /// The chat options included in the request. + /// + /// if caching should be used for the request, such that the + /// will try to satisfy the request from the cache, or if it can't, will try to cache the fetched response. + /// if caching should not be used for the request, such that the request will + /// be passed through to the inner without attempting to read from or write to the cache. + /// + /// + /// The default implementation returns as long as the + /// does not have a set. + /// + protected virtual bool EnableCaching(IEnumerable messages, ChatOptions? options) { // We want to skip caching if options.ConversationId is set. If it's set, that implies there's // some state that will impact the response and that's not represented in the messages. Since - // that state could change even with the same ID, we have to assume caching isn't valid. + // that state could change even with the same ID (e.g. if it's a thread ID representing the + // mutable state of a conversation), we have to assume caching isn't valid. return options?.ConversationId is null; } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index 5dcad329637..f7f246eb35c 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -16,6 +16,10 @@ "Member": "abstract string Microsoft.Extensions.AI.CachingChatClient.GetCacheKey(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options, params System.ReadOnlySpan additionalValues);", "Stage": "Stable" }, + { + "Member": "virtual bool Microsoft.Extensions.AI.CachingChatClient.EnableCaching(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options);", + "Stage": "Stable" + }, { "Member": "override System.Threading.Tasks.Task Microsoft.Extensions.AI.CachingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs index 4f2427d133c..2c755da7be9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs @@ -33,9 +33,11 @@ public void Ctor_ExpectedDefaults() } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task CachesSuccessResultsAsync(bool conversationIdSet) + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task CachesSuccessResultsAsync(bool conversationIdSet, bool customCaching) { // Arrange ChatOptions options = new() { ConversationId = conversationIdSet ? "123" : null }; @@ -79,10 +81,16 @@ public async Task CachesSuccessResultsAsync(bool conversationIdSet) return Task.FromResult(expectedResponse); } }; - using var outer = new DistributedCachingChatClient(testClient, _storage) - { - JsonSerializerOptions = TestJsonSerializerContext.Default.Options - }; + + int enableCachingInvocations = 0; + using var outer = customCaching ? + new CustomCachingChatClient(testClient, _storage, (m, o) => + { + return ++enableCachingInvocations % 2 == 0; + }) : + new DistributedCachingChatClient(testClient, _storage); + + outer.JsonSerializerOptions = TestJsonSerializerContext.Default.Options; // Make the initial request and do a quick sanity check var result1 = await outer.GetResponseAsync("some input", options); @@ -93,12 +101,28 @@ public async Task CachesSuccessResultsAsync(bool conversationIdSet) var result2 = await outer.GetResponseAsync("some input", options); // Assert - Assert.Equal(conversationIdSet ? 2 : 1, innerCallCount); + if (customCaching) + { + Assert.Equal(enableCachingInvocations % 2 == 0 ? 2 : 1, innerCallCount); + } + else + { + Assert.Equal(conversationIdSet ? 2 : 1, innerCallCount); + } + AssertResponsesEqual(expectedResponse, result2); // Act/Assert 2: Cache misses do not return cached results await outer.GetResponseAsync("some modified input", options); - Assert.Equal(conversationIdSet ? 3 : 2, innerCallCount); + Assert.Equal(conversationIdSet || customCaching ? 3 : 2, innerCallCount); + + Assert.Equal(customCaching ? 3 : 0, enableCachingInvocations); + } + + private sealed class CustomCachingChatClient(IChatClient innerClient, IDistributedCache storage, Func, ChatOptions?, bool> enableCaching) : + DistributedCachingChatClient(innerClient, storage) + { + protected override bool EnableCaching(IEnumerable messages, ChatOptions? options) => enableCaching(messages, options); } [Fact] From d32357716a5261509bf7527101b21cb6f94a0f89 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 18 Jun 2025 14:24:35 -0400 Subject: [PATCH 151/472] Tweak OpenAI JSON schema transforms (#6523) * Tweak OpenAI JSON schema transforms Only require all properties and force additionalProperties: false when strict is set or when structured output is used. Also update the OpenAI assistant chat client to apply the transforms. --- .../OpenAIAssistantChatClient.cs | 15 ++- .../OpenAIChatClient.cs | 23 +--- .../OpenAIClientExtensions.cs | 28 ++++ .../OpenAIResponseChatClient.cs | 28 ++-- .../ChatClientIntegrationTests.cs | 121 +++++++++++++++--- 5 files changed, 162 insertions(+), 53 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index ea541bee91a..7a64a86721d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -60,7 +60,7 @@ public OpenAIAssistantChatClient(AssistantClient assistantClient, string assista // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(AssistantClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(assistantClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; + ?.GetValue(assistantClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; _metadata = new("openai", providerUrl); } @@ -284,13 +284,16 @@ void IDisposable.Dispose() switch (tool) { case AIFunction aiFunction: - bool? strict = aiFunction.AdditionalProperties.TryGetValue(nameof(strict), out var strictValue) && strictValue is bool strictBool ? + bool? strict = aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out var strictValue) && strictValue is bool strictBool ? strictBool : null; + + JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); + runOptions.ToolsOverride.Add(new FunctionToolDefinition(aiFunction.Name) { Description = aiFunction.Description, - Parameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AssistantJsonContext.Default.JsonElement)), + Parameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AssistantJsonContext.Default.JsonElement)), StrictParameterSchemaEnabled = strict, }); break; @@ -334,10 +337,10 @@ void IDisposable.Dispose() runOptions.ResponseFormat = AssistantResponseFormat.CreateTextFormat(); break; - case ChatResponseFormatJson jsonFormat when jsonFormat.Schema is not null: + case ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema: runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName, - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonFormat.Schema, AssistantJsonContext.Default.JsonElement)), + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AssistantJsonContext.Default.JsonElement)), jsonFormat.SchemaDescription); break; @@ -382,7 +385,7 @@ void AppendSystemInstructions(string? toAppend) // to include that information in its responses. System messages should ideally be instead done as instructions to // the assistant when the assistant is created. if (chatMessage.Role == ChatRole.System || - chatMessage.Role == OpenAIResponseChatClient.ChatRoleDeveloper) + chatMessage.Role == OpenAIClientExtensions.ChatRoleDeveloper) { foreach (var textContent in chatMessage.Contents.OfType()) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index a6cd70149d7..40526b708c0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -25,15 +25,6 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . internal sealed partial class OpenAIChatClient : IChatClient { - /// Gets the JSON schema transformer cache conforming to OpenAI restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. - internal static AIJsonSchemaTransformCache SchemaTransformCache { get; } = new(new() - { - RequireAllProperties = true, - DisallowAdditionalProperties = true, - ConvertBooleanSchemas = true, - MoveDefaultKeywordToDescription = true, - }); - /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -54,7 +45,7 @@ public OpenAIChatClient(ChatClient chatClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(ChatClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatClient) as Uri ?? OpenAIResponseChatClient.DefaultOpenAIEndpoint; + ?.GetValue(chatClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; string? model = typeof(ChatClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(chatClient) as string; @@ -125,12 +116,12 @@ void IDisposable.Dispose() { if (input.Role == ChatRole.System || input.Role == ChatRole.User || - input.Role == OpenAIResponseChatClient.ChatRoleDeveloper) + input.Role == OpenAIClientExtensions.ChatRoleDeveloper) { var parts = ToOpenAIChatContent(input.Contents); yield return input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } : - input.Role == OpenAIResponseChatClient.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : + input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : new UserChatMessage(parts) { ParticipantName = input.AuthorName }; } else if (input.Role == ChatRole.Tool) @@ -553,7 +544,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) } else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) { - result.ResponseFormat = SchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? + result.ResponseFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes( @@ -570,12 +561,12 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) { bool? strict = - aiFunction.AdditionalProperties.TryGetValue("strictJsonSchema", out object? strictObj) && + aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && strictObj is bool strictValue ? strictValue : null; // Perform transformations making the schema legal per OpenAI restrictions - JsonElement jsonSchema = SchemaTransformCache.GetOrCreateTransformedSchema(aiFunction); + JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); // Map to an intermediate model so that redundant properties are skipped. var tool = JsonSerializer.Deserialize(jsonSchema, ChatClientJsonContext.Default.ChatToolJson)!; @@ -622,7 +613,7 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => ChatMessageRole.User => ChatRole.User, ChatMessageRole.Assistant => ChatRole.Assistant, ChatMessageRole.Tool => ChatRole.Tool, - ChatMessageRole.Developer => OpenAIResponseChatClient.ChatRoleDeveloper, + ChatMessageRole.Developer => OpenAIClientExtensions.ChatRoleDeveloper, _ => new ChatRole(role.ToString()), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index ea43b7e5e31..19cd54306fe 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -1,7 +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.Diagnostics.CodeAnalysis; +using System.Text.Json; using OpenAI; using OpenAI.Assistants; using OpenAI.Audio; @@ -14,6 +16,26 @@ namespace Microsoft.Extensions.AI; /// Provides extension methods for working with s. public static class OpenAIClientExtensions { + /// Key into AdditionalProperties used to store a strict option. + internal const string StrictKey = "strictJsonSchema"; + + /// Gets the default OpenAI endpoint. + internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); + + /// Gets a for "developer". + internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); + + /// + /// Gets the JSON schema transformer cache conforming to OpenAI strict restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. + /// + internal static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new() + { + DisallowAdditionalProperties = true, + ConvertBooleanSchemas = true, + MoveDefaultKeywordToDescription = true, + RequireAllProperties = true, + }); + /// Gets an for use with this . /// The client. /// An that can be used to converse via the . @@ -52,4 +74,10 @@ public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioCl /// An that can be used to generate embeddings via the . public static IEmbeddingGenerator> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) => new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions); + + /// Gets the JSON schema to use from the function. + internal static JsonElement GetSchema(AIFunction function, bool? strict) => + strict is true ? + StrictSchemaTransformCache.GetOrCreateTransformedSchema(function) : + function.JsonSchema; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index ffa5bf19b63..e722c6c0c9c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -13,7 +13,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; -using static Microsoft.Extensions.AI.OpenAIChatClient; #pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex @@ -26,12 +25,6 @@ namespace Microsoft.Extensions.AI; /// Represents an for an . internal sealed partial class OpenAIResponseChatClient : IChatClient { - /// Gets the default OpenAI endpoint. - internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); - - /// Gets a for "developer". - internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); - /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -52,7 +45,7 @@ public OpenAIResponseChatClient(OpenAIResponseClient responseClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(OpenAIResponseClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(responseClient) as Uri ?? DefaultOpenAIEndpoint; + ?.GetValue(responseClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; string? model = typeof(OpenAIResponseClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(responseClient) as string; @@ -336,7 +329,7 @@ private static ChatRole ToChatRole(MessageRole? role) => role switch { MessageRole.System => ChatRole.System, - MessageRole.Developer => ChatRoleDeveloper, + MessageRole.Developer => OpenAIClientExtensions.ChatRoleDeveloper, MessageRole.User => ChatRole.User, _ => ChatRole.Assistant, }; @@ -380,10 +373,17 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { switch (tool) { - case AIFunction af: - var oaitool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(af), ResponseClientJsonContext.Default.ResponseToolJson)!; + case AIFunction aiFunction: + bool strict = + aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && + strictObj is bool strictValue && + strictValue; + + JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); + + var oaitool = JsonSerializer.Deserialize(jsonSchema, ResponseClientJsonContext.Default.ResponseToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(oaitool, ResponseClientJsonContext.Default.ResponseToolJson)); - result.Tools.Add(ResponseTool.CreateFunctionTool(af.Name, af.Description, functionParameters)); + result.Tools.Add(ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict)); break; case HostedWebSearchTool: @@ -440,7 +440,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { result.TextOptions = new() { - TextFormat = SchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? + TextFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? ResponseTextFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, ResponseClientJsonContext.Default.JsonElement)), @@ -460,7 +460,7 @@ private static IEnumerable ToOpenAIResponseItems( foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || - input.Role == ChatRoleDeveloper) + input.Role == OpenAIClientExtensions.ChatRoleDeveloper) { string text = input.Text; if (!string.IsNullOrWhiteSpace(text)) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index f34f930f3ea..ab46a0e0c58 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -9,7 +9,9 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; @@ -22,6 +24,10 @@ #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable CA2214 // Do not call overridable methods in constructors #pragma warning disable CA2249 // Consider using 'string.Contains' instead of 'string.IndexOf' +#pragma warning disable S103 // Lines should not be too long +#pragma warning disable S1144 // Unused private types or members should be removed +#pragma warning disable S3604 // Member initializer values should not be redundant +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line namespace Microsoft.Extensions.AI; @@ -352,7 +358,14 @@ private static void AssertUsageAgainstActivities(ChatResponse response, List + AvailableTools_SchemasAreAccepted(strict: true); + + [ConditionalFact] + public virtual Task AvailableTools_SchemasAreAccepted_NonStrict() => + AvailableTools_SchemasAreAccepted(strict: false); + + private async Task AvailableTools_SchemasAreAccepted(bool strict) { SkipIfNotEnabled(); @@ -366,27 +379,84 @@ public virtual async Task AvailableTools_SchemasAreAccepted() using var chatClient = new FunctionInvokingChatClient( new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + int methodCount = 1; + Func createOptions = () => + { + AIFunctionFactoryOptions aiFuncOptions = new() + { + Name = $"Method{methodCount++}", + }; + + if (strict) + { + aiFuncOptions.AdditionalProperties = new Dictionary { ["strictJsonSchema"] = true }; + } + + return aiFuncOptions; + }; + + Func createWithSchema = schema => + { + Dictionary additionalProperties = new(); + + if (strict) + { + additionalProperties["strictJsonSchema"] = true; + } + + return new CustomAIFunction($"CustomMethod{methodCount++}", schema, additionalProperties); + }; + ChatOptions options = new() { MaxOutputTokens = 100, Tools = [ - AIFunctionFactory.Create((int? i) => i, "Method1"), - AIFunctionFactory.Create((string? s) => s, "Method2"), - AIFunctionFactory.Create((int? i = null) => i, "Method3"), - AIFunctionFactory.Create((bool b) => b, "Method4"), - AIFunctionFactory.Create((double d) => d, "Method5"), - AIFunctionFactory.Create((decimal d) => d, "Method6"), - AIFunctionFactory.Create((float f) => f, "Method7"), - AIFunctionFactory.Create((long l) => l, "Method8"), - AIFunctionFactory.Create((char c) => c, "Method9"), - AIFunctionFactory.Create((DateTime dt) => dt, "Method10"), - AIFunctionFactory.Create((DateTime? dt) => dt, "Method11"), - AIFunctionFactory.Create((Guid guid) => guid, "Method12"), - AIFunctionFactory.Create((List list) => list, "Method13"), - AIFunctionFactory.Create((int[] arr) => arr, "Method14"), - AIFunctionFactory.Create((string p1 = "str", int p2 = 42, BindingFlags p3 = BindingFlags.IgnoreCase, char p4 = 'x') => p1, "Method15"), - AIFunctionFactory.Create((string? p1 = "str", int? p2 = 42, BindingFlags? p3 = BindingFlags.IgnoreCase, char? p4 = 'x') => p1, "Method16"), + // Using AIFunctionFactory + AIFunctionFactory.Create((int? i) => i, createOptions()), + AIFunctionFactory.Create((string? s) => s, createOptions()), + AIFunctionFactory.Create((int? i = null) => i, createOptions()), + AIFunctionFactory.Create((bool b) => b, createOptions()), + AIFunctionFactory.Create((double d) => d, createOptions()), + AIFunctionFactory.Create((decimal d) => d, createOptions()), + AIFunctionFactory.Create((float f) => f, createOptions()), + AIFunctionFactory.Create((long l) => l, createOptions()), + AIFunctionFactory.Create((char c) => c, createOptions()), + AIFunctionFactory.Create((DateTime dt) => dt, createOptions()), + AIFunctionFactory.Create((DateTime? dt) => dt, createOptions()), + AIFunctionFactory.Create((Guid guid) => guid, createOptions()), + AIFunctionFactory.Create((List list) => list, createOptions()), + AIFunctionFactory.Create((int[] arr, ComplexObject? co) => arr, createOptions()), + AIFunctionFactory.Create((string p1 = "str", int p2 = 42, BindingFlags p3 = BindingFlags.IgnoreCase, char p4 = 'x') => p1, createOptions()), + AIFunctionFactory.Create((string? p1 = "str", int? p2 = 42, BindingFlags? p3 = BindingFlags.IgnoreCase, char? p4 = 'x') => p1, createOptions()), + + // Selection from @modelcontextprotocol/server-everything + createWithSchema(""" + {"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + createWithSchema(""" + {"type":"object","properties":{"duration":{"type":"number","default":10,"description":"Duration of the operation in seconds"},"steps":{"type":"number","default":5,"description":"Number of steps in the operation"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + createWithSchema(""" + {"type":"object","properties":{"prompt":{"type":"string","description":"The prompt to send to the LLM"},"maxTokens":{"type":"number","default":100,"description":"Maximum number of tokens to generate"}},"required":["prompt"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + createWithSchema(""" + {"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + createWithSchema(""" + {"type":"object","properties":{"messageType":{"type":"string","enum":["error","success","debug"],"description":"Type of message to demonstrate different annotation patterns"},"includeImage":{"type":"boolean","default":false,"description":"Whether to include an example image"}},"required":["messageType"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + createWithSchema(""" + {"type":"object","properties":{"resourceId":{"type":"number","minimum":1,"maximum":100,"description":"ID of the resource to reference (1-100)"}},"required":["resourceId"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} + """), + + // Selection from GH MCP server + createWithSchema(""" + {"properties":{"body":{"description":"The text of the review comment","type":"string"},"line":{"description":"The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range","type":"number"},"owner":{"description":"Repository owner","type":"string"},"path":{"description":"The relative path to the file that necessitates a comment","type":"string"},"pullNumber":{"description":"Pull request number","type":"number"},"repo":{"description":"Repository name","type":"string"},"side":{"description":"The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state","enum":["LEFT","RIGHT"],"type":"string"},"startLine":{"description":"For multi-line comments, the first line of the range that the comment applies to","type":"number"},"startSide":{"description":"For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state","enum":["LEFT","RIGHT"],"type":"string"},"subjectType":{"description":"The level at which the comment is targeted","enum":["FILE","LINE"],"type":"string"}},"required":["owner","repo","pullNumber","path","body","subjectType"],"type":"object"} + """), + createWithSchema(""" + {"properties":{"commit_message":{"description":"Extra detail for merge commit","type":"string"},"commit_title":{"description":"Title for merge commit","type":"string"},"merge_method":{"description":"Merge method","enum":["merge","squash","rebase"],"type":"string"},"owner":{"description":"Repository owner","type":"string"},"pullNumber":{"description":"Pull request number","type":"number"},"repo":{"description":"Repository name","type":"string"}},"required":["owner","repo","pullNumber"],"type":"object"} + """), ], }; @@ -395,6 +465,23 @@ public virtual async Task AvailableTools_SchemasAreAccepted() Assert.NotNull(response); } + private sealed class CustomAIFunction(string name, string jsonSchema, IReadOnlyDictionary additionalProperties) : AIFunction + { + public override string Name => name; + public override IReadOnlyDictionary AdditionalProperties => additionalProperties; + public override JsonElement JsonSchema { get; } = JsonSerializer.Deserialize(jsonSchema, AIJsonUtilities.DefaultOptions); + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => throw new NotSupportedException(); + } + + private class ComplexObject + { + public string? SomeString { get; set; } + + public string AnotherString { get; set; } = "default"; + + public int Value { get; set; } + } + protected virtual bool SupportsParallelFunctionCalling => true; [ConditionalFact] From 8bf082f99b1b3e9b16545e17b41579b3c07fc6bb Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:58:10 +0200 Subject: [PATCH 152/472] Managed implementation of DNS resolver (#6104) This PR brings in a C# implementation of a DNS resolver that is able to signal the TTL information together with the query results. Main features Async network I/O, fully cancellable Mockable Resolves IP Addresses (A/AAAA records) and Service records (SRV + related A/AAAA) Transparent fallback to TCP Autodetection of OS settings (i.e. reads nameservers from /etc/resolv.conf file) Thread-safe --- .../DnsServiceEndpointProvider.cs | 35 +- .../DnsServiceEndpointProviderBase.cs | 2 +- .../DnsServiceEndpointProviderFactory.cs | 4 +- .../DnsSrvServiceEndpointProvider.cs | 53 +- .../DnsSrvServiceEndpointProviderFactory.cs | 10 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 5 +- .../Resolver/DnsDataReader.cs | 133 +++ .../Resolver/DnsDataWriter.cs | 121 +++ .../Resolver/DnsMessageHeader.cs | 36 + .../Resolver/DnsPrimitives.cs | 318 ++++++ .../Resolver/DnsResolver.Log.cs | 39 + .../Resolver/DnsResolver.Telemetry.cs | 115 +++ .../Resolver/DnsResolver.cs | 931 ++++++++++++++++++ .../Resolver/DnsResourceRecord.cs | 22 + .../Resolver/DnsResponse.cs | 39 + .../Resolver/EncodedDomainName.cs | 82 ++ .../Resolver/IDnsResolver.cs | 13 + .../Resolver/NetworkInfo.cs | 36 + .../Resolver/QueryClass.cs | 9 + .../Resolver/QueryFlags.cs | 15 + .../Resolver/QueryResponseCode.cs | 42 + .../Resolver/QueryType.cs | 55 ++ .../Resolver/ResolvConf.cs | 48 + .../Resolver/ResolverOptions.cs | 31 + .../Resolver/ResultTypes.cs | 10 + .../Resolver/SendQueryError.cs | 47 + ...DiscoveryDnsServiceCollectionExtensions.cs | 4 +- .../.gitignore | 2 + .../Fuzzers/DnsResponseFuzzer.cs | 44 + .../Fuzzers/EncodedDomainNameFuzzer.cs | 33 + .../Fuzzers/WriteDomainNameRoundTripFuzzer.cs | 48 + .../GlobalUsings.cs | 5 + .../IFuzzer.cs | 10 + ....ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 18 + .../Program.cs | 64 ++ .../DnsResponseFuzzer/ip-www.example.com | Bin 0 -> 141 bytes .../corpus-seed/DnsResponseFuzzer/name-error | Bin 0 -> 31 bytes .../DnsResponseFuzzer/name-error-2 | Bin 0 -> 91 bytes .../corpus-seed/DnsResponseFuzzer/no-data | Bin 0 -> 91 bytes .../DnsResponseFuzzer/server-error | Bin 0 -> 31 bytes .../ip-www.example.com | Bin 0 -> 143 bytes .../WriteDomainNameRoundTripFuzzer/example | 1 + .../WriteDomainNameRoundTripFuzzer/nonascii | 1 + .../WriteDomainNameRoundTripFuzzer/toolong | 1 + .../run.ps1 | 106 ++ .../DnsServiceEndpointResolverTests.cs | 2 + .../DnsSrvServiceEndpointResolverTests.cs | 123 +-- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 4 + .../Resolver/CancellationTests.cs | 42 + .../Resolver/DnsDataReaderTests.cs | 64 ++ .../Resolver/DnsDataWriterTests.cs | 148 +++ .../Resolver/DnsPrimitivesTests.cs | 195 ++++ .../Resolver/LoopbackDnsServer.cs | 331 +++++++ .../Resolver/LoopbackDnsTestBase.cs | 48 + .../Resolver/ResolvConfTests.cs | 26 + .../Resolver/ResolveAddressesTests.cs | 307 ++++++ .../Resolver/ResolveServiceTests.cs | 37 + .../Resolver/RetryTests.cs | 309 ++++++ .../Resolver/TcpFailoverTests.cs | 132 +++ .../YarpServiceDiscoveryTests.cs | 97 +- 60 files changed, 4233 insertions(+), 220 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/name-error create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/name-error-2 create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/no-data create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/server-error create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/EncodedDomainNameFuzzer/ip-www.example.com create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/example create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/nonascii create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/toolong create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/run.ps1 create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index 6cc9f92bc46..7a2d1b632e0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -4,6 +4,7 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -12,6 +13,7 @@ internal sealed partial class DnsServiceEndpointProvider( string hostName, IOptionsMonitor options, ILogger logger, + IDnsResolver resolver, TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -29,17 +31,14 @@ protected override async Task ResolveAsyncCore() var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); - var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + + var now = _timeProvider.GetUtcNow().DateTime; + var addresses = await resolver.ResolveIPAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + foreach (var address in addresses) { - var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); - serviceEndpoint.Features.Set(this); - if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) - { - serviceEndpoint.Features.Set(this); - } - - endpoints.Add(serviceEndpoint); + ttl = MinTtl(now, address.ExpiresAt, ttl); + endpoints.Add(CreateEndpoint(new IPEndPoint(address.Address, port: 0))); } if (endpoints.Count == 0) @@ -48,5 +47,23 @@ protected override async Task ResolveAsyncCore() } SetResult(endpoints, ttl); + + static TimeSpan MinTtl(DateTime now, DateTime expiresAt, TimeSpan existing) + { + var candidate = expiresAt - now; + return candidate < existing ? candidate : existing; + } + + ServiceEndpoint CreateEndpoint(EndPoint endPoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) + { + serviceEndpoint.Features.Set(this); + } + + return serviceEndpoint; + } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 6c69cc7a760..311c06f631a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -14,7 +14,7 @@ internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpoin private readonly object _lock = new(); private readonly ILogger _logger; private readonly CancellationTokenSource _disposeCancellation = new(); - private readonly TimeProvider _timeProvider; + protected readonly TimeProvider _timeProvider; private long _lastRefreshTimeStamp; private Task _resolveTask = Task.CompletedTask; private bool _hasEndpoints; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs index c241ad89dd3..1da21411e64 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -4,18 +4,20 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsServiceEndpointProviderFactory( IOptionsMonitor options, ILogger logger, + IDnsResolver resolver, TimeProvider timeProvider) : IServiceEndpointProviderFactory { /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index c174cda4f68..6d5ade5059e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using DnsClient; -using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -15,7 +14,7 @@ internal sealed partial class DnsSrvServiceEndpointProvider( string hostName, IOptionsMonitor options, ILogger logger, - IDnsQuery dnsClient, + IDnsResolver resolver, TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -35,56 +34,36 @@ protected override async Task ResolveAsyncCore() var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); - var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); - if (result.HasError) - { - throw CreateException(srvQuery, result.ErrorMessage); - } - var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals.Where(x => x is AddressRecord or CNameRecord)) - { - ttl = MinTtl(record, ttl); - lookupMapping[record.DomainName] = record; - } + var now = _timeProvider.GetUtcNow().DateTime; + var result = await resolver.ResolveServiceAsync(srvQuery, cancellationToken: ShutdownToken).ConfigureAwait(false); - var srvRecords = result.Answers.OfType(); - foreach (var record in srvRecords) + foreach (var record in result) { - if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) - { - continue; - } + ttl = MinTtl(now, record.ExpiresAt, ttl); - ttl = MinTtl(record, ttl); - if (targetRecord is AddressRecord addressRecord) + if (record.Addresses.Length > 0) { - endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); + foreach (var address in record.Addresses) + { + ttl = MinTtl(now, address.ExpiresAt, ttl); + endpoints.Add(CreateEndpoint(new IPEndPoint(address.Address, record.Port))); + } } - else if (targetRecord is CNameRecord canonicalNameRecord) + else { - endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(record.Target.TrimEnd('.'), record.Port))); } } SetResult(endpoints, ttl); - static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + static TimeSpan MinTtl(DateTime now, DateTime expiresAt, TimeSpan existing) { - var candidate = TimeSpan.FromSeconds(record.TimeToLive); + var candidate = expiresAt - now; return candidate < existing ? candidate : existing; } - InvalidOperationException CreateException(string dnsName, string errorMessage) - { - var msg = errorMessage switch - { - { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", - _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." - }; - return new InvalidOperationException(msg); - } - ServiceEndpoint CreateEndpoint(EndPoint endPoint) { var serviceEndpoint = ServiceEndpoint.Create(endPoint); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index fd0cb28353d..085ee30123b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -2,29 +2,29 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsSrvServiceEndpointProviderFactory( IOptionsMonitor options, ILogger logger, - IDnsQuery dnsClient, + IDnsResolver resolver, TimeProvider timeProvider) : IServiceEndpointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); private static readonly string s_resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); - private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); + private readonly string? _querySuffix = options.CurrentValue.QuerySuffix?.TrimStart('.') ?? GetKubernetesHostDomain(); /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md - // SRV records are available for headless services with named ports. + // SRV records are available for headless services with named ports. // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.{suffix}" // The suffix (after the service name) can be parsed from /etc/resolv.conf // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". @@ -39,7 +39,7 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 9854c93c01a..3aba9b3aaea 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -9,7 +9,6 @@ - @@ -23,7 +22,9 @@ - + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs new file mode 100644 index 00000000000..094df3040d1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsDataReader : IDisposable +{ + public ArraySegment MessageBuffer { get; private set; } + bool _returnToPool; + private int _position; + + public DnsDataReader(ArraySegment buffer, bool returnToPool = false) + { + MessageBuffer = buffer; + _position = 0; + _returnToPool = returnToPool; + } + + public bool TryReadHeader(out DnsMessageHeader header) + { + Debug.Assert(_position == 0); + + if (!DnsPrimitives.TryReadMessageHeader(MessageBuffer.AsSpan(), out header, out int bytesRead)) + { + header = default; + return false; + } + + _position += bytesRead; + return true; + } + + internal bool TryReadQuestion(out EncodedDomainName name, out QueryType type, out QueryClass @class) + { + if (!TryReadDomainName(out name) || + !TryReadUInt16(out ushort typeAsInt) || + !TryReadUInt16(out ushort classAsInt)) + { + type = 0; + @class = 0; + return false; + } + + type = (QueryType)typeAsInt; + @class = (QueryClass)classAsInt; + return true; + } + + public bool TryReadUInt16(out ushort value) + { + if (MessageBuffer.Count - _position < 2) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt16BigEndian(MessageBuffer.AsSpan(_position)); + _position += 2; + return true; + } + + public bool TryReadUInt32(out uint value) + { + if (MessageBuffer.Count - _position < 4) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32BigEndian(MessageBuffer.AsSpan(_position)); + _position += 4; + return true; + } + + public bool TryReadResourceRecord(out DnsResourceRecord record) + { + if (!TryReadDomainName(out EncodedDomainName name) || + !TryReadUInt16(out ushort type) || + !TryReadUInt16(out ushort @class) || + !TryReadUInt32(out uint ttl) || + !TryReadUInt16(out ushort dataLength) || + MessageBuffer.Count - _position < dataLength) + { + record = default; + return false; + } + + ReadOnlyMemory data = MessageBuffer.AsMemory(_position, dataLength); + _position += dataLength; + + record = new DnsResourceRecord(name, (QueryType)type, (QueryClass)@class, (int)ttl, data); + return true; + } + + public bool TryReadDomainName(out EncodedDomainName name) + { + if (DnsPrimitives.TryReadQName(MessageBuffer, _position, out name, out int bytesRead)) + { + _position += bytesRead; + return true; + } + + return false; + } + + public bool TryReadSpan(int length, out ReadOnlySpan name) + { + if (MessageBuffer.Count - _position < length) + { + name = default; + return false; + } + + name = MessageBuffer.AsSpan(_position, length); + _position += length; + return true; + } + + public void Dispose() + { + if (_returnToPool && MessageBuffer.Array != null) + { + ArrayPool.Shared.Return(MessageBuffer.Array); + } + + _returnToPool = false; + MessageBuffer = default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs new file mode 100644 index 00000000000..a0a11f0b808 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Diagnostics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed class DnsDataWriter +{ + private readonly Memory _buffer; + private int _position; + + internal DnsDataWriter(Memory buffer) + { + _buffer = buffer; + _position = 0; + } + + public int Position => _position; + + internal bool TryWriteHeader(in DnsMessageHeader header) + { + if (!DnsPrimitives.TryWriteMessageHeader(_buffer.Span.Slice(_position), header, out int written)) + { + return false; + } + + _position += written; + return true; + } + + internal bool TryWriteQuestion(EncodedDomainName name, QueryType type, QueryClass @class) + { + if (!TryWriteDomainName(name) || + !TryWriteUInt16((ushort)type) || + !TryWriteUInt16((ushort)@class)) + { + return false; + } + + return true; + } + + private bool TryWriteDomainName(EncodedDomainName name) + { + foreach (var label in name.Labels) + { + // this should be already validated by the caller + Debug.Assert(label.Length <= 63, "Label length must not exceed 63 bytes."); + + if (!TryWriteByte((byte)label.Length) || + !TryWriteRawData(label.Span)) + { + return false; + } + } + + // root label + return TryWriteByte(0); + } + + internal bool TryWriteDomainName(string name) + { + if (DnsPrimitives.TryWriteQName(_buffer.Span.Slice(_position), name, out int written)) + { + _position += written; + return true; + } + + return false; + } + + internal bool TryWriteByte(byte value) + { + if (_buffer.Length - _position < 1) + { + return false; + } + + _buffer.Span[_position] = value; + _position += 1; + return true; + } + + internal bool TryWriteUInt16(ushort value) + { + if (_buffer.Length - _position < 2) + { + return false; + } + + BinaryPrimitives.WriteUInt16BigEndian(_buffer.Span.Slice(_position), value); + _position += 2; + return true; + } + + internal bool TryWriteUInt32(uint value) + { + if (_buffer.Length - _position < 4) + { + return false; + } + + BinaryPrimitives.WriteUInt32BigEndian(_buffer.Span.Slice(_position), value); + _position += 4; + return true; + } + + internal bool TryWriteRawData(ReadOnlySpan value) + { + if (_buffer.Length - _position < value.Length) + { + return false; + } + + value.CopyTo(_buffer.Span.Slice(_position)); + _position += value.Length; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs new file mode 100644 index 00000000000..b22273a04f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +// RFC 1035 4.1.1. Header section format +internal struct DnsMessageHeader +{ + internal const int HeaderLength = 12; + public ushort TransactionId { get; set; } + + internal QueryFlags QueryFlags { get; set; } + + public ushort QueryCount { get; set; } + + public ushort AnswerCount { get; set; } + + public ushort AuthorityCount { get; set; } + + public ushort AdditionalRecordCount { get; set; } + + public QueryResponseCode ResponseCode + { + get => (QueryResponseCode)(QueryFlags & QueryFlags.ResponseCodeMask); + } + + public bool IsResultTruncated + { + get => (QueryFlags & QueryFlags.ResultTruncated) != 0; + } + + public bool IsResponse + { + get => (QueryFlags & QueryFlags.HasResponse) != 0; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs new file mode 100644 index 00000000000..e549abe2576 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Globalization; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class DnsPrimitives +{ + // Maximum length of a domain name in ASCII (excluding trailing dot) + internal const int MaxDomainNameLength = 253; + + internal static bool TryReadMessageHeader(ReadOnlySpan buffer, out DnsMessageHeader header, out int bytesRead) + { + // RFC 1035 4.1.1. Header section format + if (buffer.Length < DnsMessageHeader.HeaderLength) + { + header = default; + bytesRead = 0; + return false; + } + + header = new DnsMessageHeader + { + TransactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer), + QueryFlags = (QueryFlags)BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)), + QueryCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(4)), + AnswerCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(6)), + AuthorityCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(8)), + AdditionalRecordCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(10)) + }; + + bytesRead = DnsMessageHeader.HeaderLength; + return true; + } + + internal static bool TryWriteMessageHeader(Span buffer, DnsMessageHeader header, out int bytesWritten) + { + // RFC 1035 4.1.1. Header section format + if (buffer.Length < DnsMessageHeader.HeaderLength) + { + bytesWritten = 0; + return false; + } + + BinaryPrimitives.WriteUInt16BigEndian(buffer, header.TransactionId); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)header.QueryFlags); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(4), header.QueryCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(6), header.AnswerCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(8), header.AuthorityCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(10), header.AdditionalRecordCount); + + bytesWritten = DnsMessageHeader.HeaderLength; + return true; + } + + // https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4 + // labels 63 octets or less + // name 255 octets or less + + private static readonly SearchValues s_domainNameValidChars = SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."); + private static readonly IdnMapping s_idnMapping = new IdnMapping(); + internal static bool TryWriteQName(Span destination, string name, out int written) + { + written = 0; + + // + // RFC 1035 4.1.2. + // + // a domain name represented as a sequence of labels, where + // each label consists of a length octet followed by that + // number of octets. The domain name terminates with the + // zero length octet for the null label of the root. Note + // that this field may be an odd number of octets; no + // padding is used. + // + if (!Ascii.IsValid(name)) + { + // IDN name, apply punycode + try + { + // IdnMapping performs some validation internally (such as label + // and domain name lengths), but is more relaxed than RFC + // 1035 (e.g. allows ~ chars), so even if this conversion does + // not throw, we still need to perform additional validation + name = s_idnMapping.GetAscii(name); + } + catch + { + return false; + } + } + + if (name.Length > MaxDomainNameLength || + name.AsSpan().ContainsAnyExcept(s_domainNameValidChars) || + destination.IsEmpty || + !Encoding.ASCII.TryGetBytes(name, destination.Slice(1), out int length) || + destination.Length < length + 2) + { + // buffer too small + return false; + } + + Span nameBuffer = destination.Slice(0, 1 + length); + Span label; + while (true) + { + // figure out the next label and prepend the length + int index = nameBuffer.Slice(1).IndexOf((byte)'.'); + label = index == -1 ? nameBuffer.Slice(1) : nameBuffer.Slice(1, index); + + if (label.Length == 0) + { + // empty label (explicit root) is only allowed at the end + if (index != -1) + { + written = 0; + return false; + } + } + // Label restrictions: + // - maximum 63 octets long + // - must start with a letter or digit (digit is allowed by RFC 1123) + // - may start with an underscore (underscore may be present only + // at the start of the label to support SRV records) + // - must end with a letter or digit + else if (label.Length > 63 || + !char.IsAsciiLetterOrDigit((char)label[0]) && label[0] != '_' || + label.Slice(1).Contains((byte)'_') || + !char.IsAsciiLetterOrDigit((char)label[^1])) + { + written = 0; + return false; + } + + nameBuffer[0] = (byte)label.Length; + written += label.Length + 1; + + if (index == -1) + { + // this was the last label + break; + } + + nameBuffer = nameBuffer.Slice(index + 1); + } + + // Add root label if wasn't explicitly specified + if (label.Length != 0) + { + destination[written] = 0; + written++; + } + + return true; + } + + private static bool TryReadQNameCore(List> labels, int totalLength, ReadOnlyMemory messageBuffer, int offset, out int bytesRead, bool canStartWithPointer = true) + { + // + // domain name can be either + // - a sequence of labels, where each label consists of a length octet + // followed by that number of octets, terminated by a zero length octet + // (root label) + // - a pointer, where the first two bits are set to 1, and the remaining + // 14 bits are an offset (from the start of the message) to the true + // label + // + // It is not specified by the RFC if pointers must be backwards only, + // the code below prohibits forward (and self) pointers to avoid + // infinite loops. It also allows pointers only to point to a + // label, not to another pointer. + // + + bytesRead = 0; + bool allowPointer = canStartWithPointer; + + if (offset < 0 || offset >= messageBuffer.Length) + { + return false; + } + + int currentOffset = offset; + + while (true) + { + byte length = messageBuffer.Span[currentOffset]; + + if ((length & 0xC0) == 0x00) + { + // length followed by the label + if (length == 0) + { + // end of name + bytesRead = currentOffset - offset + 1; + return true; + } + + if (currentOffset + 1 + length >= messageBuffer.Length) + { + // too many labels or truncated data + break; + } + + // read next label/segment + labels.Add(messageBuffer.Slice(currentOffset + 1, length)); + totalLength += 1 + length; + + // subtract one for the length prefix of the first label + if (totalLength - 1 > MaxDomainNameLength) + { + // domain name is too long + return false; + } + + currentOffset += 1 + length; + bytesRead += 1 + length; + + // we read a label, they can be followed by pointer. + allowPointer = true; + } + else if ((length & 0xC0) == 0xC0) + { + // pointer, together with next byte gives the offset of the true label + if (!allowPointer || currentOffset + 1 >= messageBuffer.Length) + { + // pointer to pointer or truncated data + break; + } + + bytesRead += 2; + int pointer = ((length & 0x3F) << 8) | messageBuffer.Span[currentOffset + 1]; + + // we prohibit self-references and forward pointers to avoid + // infinite loops, we do this by truncating the + // messageBuffer at the offset where we started reading the + // name. We also ignore the bytesRead from the recursive + // call, as we are only interested on how many bytes we read + // from the initial start of the name. + return TryReadQNameCore(labels, totalLength, messageBuffer.Slice(0, offset), pointer, out int _, false); + } + else + { + // top two bits are reserved, this means invalid data + break; + } + } + + return false; + + } + + internal static bool TryReadQName(ReadOnlyMemory messageBuffer, int offset, out EncodedDomainName name, out int bytesRead) + { + List> labels = new List>(); + + if (TryReadQNameCore(labels, 0, messageBuffer, offset, out bytesRead)) + { + name = new EncodedDomainName(labels); + return true; + } + else + { + bytesRead = 0; + name = default; + return false; + } + } + + internal static bool TryReadService(ReadOnlyMemory buffer, out ushort priority, out ushort weight, out ushort port, out EncodedDomainName target, out int bytesRead) + { + // https://www.rfc-editor.org/rfc/rfc2782 + if (!BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span, out priority) || + !BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span.Slice(2), out weight) || + !BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span.Slice(4), out port) || + !TryReadQName(buffer.Slice(6), 0, out target, out bytesRead)) + { + target = default; + priority = 0; + weight = 0; + port = 0; + bytesRead = 0; + return false; + } + + bytesRead += 6; + return true; + } + + internal static bool TryReadSoa(ReadOnlyMemory buffer, out EncodedDomainName primaryNameServer, out EncodedDomainName responsibleMailAddress, out uint serial, out uint refresh, out uint retry, out uint expire, out uint minimum, out int bytesRead) + { + // https://www.rfc-editor.org/rfc/rfc1035#section-3.3.13 + if (!TryReadQName(buffer, 0, out primaryNameServer, out int w1) || + !TryReadQName(buffer.Slice(w1), 0, out responsibleMailAddress, out int w2) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2), out serial) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 4), out refresh) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 8), out retry) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 12), out expire) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 16), out minimum)) + { + primaryNameServer = default; + responsibleMailAddress = default; + serial = 0; + refresh = 0; + retry = 0; + expire = 0; + minimum = 0; + bytesRead = 0; + return false; + } + + bytesRead = w1 + w2 + 20; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs new file mode 100644 index 00000000000..adab9161737 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Net; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal partial class DnsResolver : IDnsResolver, IDisposable +{ + internal static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving {QueryType} {QueryName} on {Server} attempt {Attempt}", EventName = "Query")] + public static partial void Query(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(2, LogLevel.Debug, "Result truncated for {QueryType} {QueryName} from {Server} attempt {Attempt}. Restarting over TCP", EventName = "ResultTruncated")] + public static partial void ResultTruncated(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(3, LogLevel.Error, "Server {Server} replied with {ResponseCode} when querying {QueryType} {QueryName}", EventName = "ErrorResponseCode")] + public static partial void ErrorResponseCode(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, QueryResponseCode responseCode); + + [LoggerMessage(4, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} timed out.", EventName = "Timeout")] + public static partial void Timeout(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(5, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt}: no data matching given query type.", EventName = "NoData")] + public static partial void NoData(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(6, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt}: server indicates given name does not exist.", EventName = "NameError")] + public static partial void NameError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(7, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed to return a valid DNS response.", EventName = "MalformedResponse")] + public static partial void MalformedResponse(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(8, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed due to a network error.", EventName = "NetworkError")] + public static partial void NetworkError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt, Exception exception); + + [LoggerMessage(9, LogLevel.Error, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed.", EventName = "QueryError")] + public static partial void QueryError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt, Exception exception); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs new file mode 100644 index 00000000000..4be956cede9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal partial class DnsResolver +{ + internal static class Telemetry + { + private static readonly Meter s_meter = new Meter("Microsoft.Extensions.ServiceDiscovery.Dns.Resolver"); + private static readonly Histogram s_queryDuration = s_meter.CreateHistogram("query.duration", "ms", "DNS query duration"); + + private static bool IsEnabled() => s_queryDuration.Enabled; + + public static NameResolutionActivity StartNameResolution(string hostName, QueryType queryType, long startingTimestamp) + { + if (IsEnabled()) + { + return new NameResolutionActivity(hostName, queryType, startingTimestamp); + } + + return default; + } + + public static void StopNameResolution(string hostName, QueryType queryType, in NameResolutionActivity activity, object? answers, SendQueryError error, long endingTimestamp) + { + activity.Stop(answers, error, endingTimestamp, out TimeSpan duration); + + if (!IsEnabled()) + { + return; + } + + var hostNameTag = KeyValuePair.Create("dns.question.name", (object?)hostName); + var queryTypeTag = KeyValuePair.Create("dns.question.type", (object?)queryType); + + if (answers is not null) + { + s_queryDuration.Record(duration.TotalSeconds, hostNameTag, queryTypeTag); + } + else + { + var errorTypeTag = KeyValuePair.Create("error.type", (object?)error.ToString()); + s_queryDuration.Record(duration.TotalSeconds, hostNameTag, queryTypeTag, errorTypeTag); + } + } + } + + internal readonly struct NameResolutionActivity + { + private const string ActivitySourceName = "Microsoft.Extensions.ServiceDiscovery.Dns.Resolver"; + private const string ActivityName = ActivitySourceName + ".Resolve"; + private static readonly ActivitySource s_activitySource = new ActivitySource(ActivitySourceName); + + private readonly long _startingTimestamp; + private readonly Activity? _activity; // null if activity is not started + + public NameResolutionActivity(string hostName, QueryType queryType, long startingTimestamp) + { + _startingTimestamp = startingTimestamp; + _activity = s_activitySource.StartActivity(ActivityName, ActivityKind.Client); + if (_activity is not null) + { + _activity.DisplayName = $"Resolving {hostName}"; + if (_activity.IsAllDataRequested) + { + _activity.SetTag("dns.question.name", hostName); + _activity.SetTag("dns.question.type", queryType.ToString()); + } + } + } + + public void Stop(object? answers, SendQueryError error, long endingTimestamp, out TimeSpan duration) + { + duration = Stopwatch.GetElapsedTime(_startingTimestamp, endingTimestamp); + + if (_activity is null) + { + return; + } + + if (_activity.IsAllDataRequested) + { + if (answers is not null) + { + static string[] ToStringHelper(T[] array) => array.Select(a => a!.ToString()!).ToArray(); + + string[]? answersArray = answers switch + { + ServiceResult[] serviceResults => ToStringHelper(serviceResults), + AddressResult[] addressResults => ToStringHelper(addressResults), + _ => null + }; + + Debug.Assert(answersArray is not null); + _activity.SetTag("dns.answers", answersArray); + } + else + { + _activity.SetTag("error.type", error.ToString()); + } + } + + if (answers is null) + { + _activity.SetStatus(ActivityStatusCode.Error); + } + + _activity.Stop(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs new file mode 100644 index 00000000000..5722356a1c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -0,0 +1,931 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed partial class DnsResolver : IDnsResolver, IDisposable +{ + private const int IPv4Length = 4; + private const int IPv6Length = 16; + + // CancellationTokenSource.CancelAfter has a maximum timeout of Int32.MaxValue milliseconds. + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + + private bool _disposed; + private readonly ResolverOptions _options; + private readonly CancellationTokenSource _pendingRequestsCts = new(); + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + + public DnsResolver(TimeProvider timeProvider, ILogger logger) : this(timeProvider, logger, OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ? ResolvConf.GetOptions() : NetworkInfo.GetOptions()) + { + } + + internal DnsResolver(TimeProvider timeProvider, ILogger logger, ResolverOptions options) + { + _timeProvider = timeProvider; + _logger = logger; + _options = options; + Debug.Assert(_options.Servers.Count > 0); + + if (options.Timeout != Timeout.InfiniteTimeSpan) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); + ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Timeout, s_maxTimeout); + } + } + + internal DnsResolver(ResolverOptions options) : this(TimeProvider.System, NullLogger.Instance, options) + { + } + + internal DnsResolver(IEnumerable servers) : this(new ResolverOptions(servers.ToArray())) + { + } + + internal DnsResolver(IPEndPoint server) : this(new ResolverOptions(server)) + { + } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); + + // dnsSafeName is Disposed by SendQueryWithTelemetry + EncodedDomainName dnsSafeName = GetNormalizedHostName(name); + return SendQueryWithTelemetry(name, dnsSafeName, QueryType.SRV, ProcessResponse, cancellationToken); + + static (SendQueryError, ServiceResult[]) ProcessResponse(EncodedDomainName dnsSafeName, QueryType queryType, DnsResponse response) + { + var results = new List(response.Answers.Count); + + foreach (var answer in response.Answers) + { + if (answer.Type == QueryType.SRV) + { + if (!DnsPrimitives.TryReadService(answer.Data, out ushort priority, out ushort weight, out ushort port, out EncodedDomainName target, out int bytesRead) || bytesRead != answer.Data.Length) + { + return (SendQueryError.MalformedResponse, []); + } + + List addresses = new List(); + foreach (var additional in response.Additionals) + { + // From RFC 2782: + // + // Target + // The domain name of the target host. There MUST be one or more + // address records for this name, the name MUST NOT be an alias (in + // the sense of RFC 1034 or RFC 2181). Implementors are urged, but + // not required, to return the address record(s) in the Additional + // Data section. Unless and until permitted by future standards + // action, name compression is not to be used for this field. + // + // A Target of "." means that the service is decidedly not + // available at this domain. + if (additional.Name.Equals(target) && (additional.Type == QueryType.A || additional.Type == QueryType.AAAA)) + { + addresses.Add(new AddressResult(response.CreatedAt.AddSeconds(additional.Ttl), new IPAddress(additional.Data.Span))); + } + } + + results.Add(new ServiceResult(response.CreatedAt.AddSeconds(answer.Ttl), priority, weight, port, target.ToString(), addresses.ToArray())); + } + } + + return (SendQueryError.NoError, results.ToArray()); + } + } + + public async ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) + { + if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // name localhost exists outside of DNS and can't be resolved by a DNS server + int len = (Socket.OSSupportsIPv4 ? 1 : 0) + (Socket.OSSupportsIPv6 ? 1 : 0); + AddressResult[] res = new AddressResult[len]; + + int index = 0; + if (Socket.OSSupportsIPv6) // prefer IPv6 + { + res[index] = new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback); + index++; + } + if (Socket.OSSupportsIPv4) + { + res[index] = new AddressResult(DateTime.MaxValue, IPAddress.Loopback); + } + + return res; + } + + var ipv4AddressesTask = ResolveIPAddressesAsync(name, AddressFamily.InterNetwork, cancellationToken); + var ipv6AddressesTask = ResolveIPAddressesAsync(name, AddressFamily.InterNetworkV6, cancellationToken); + + AddressResult[] ipv4Addresses = await ipv4AddressesTask.ConfigureAwait(false); + AddressResult[] ipv6Addresses = await ipv6AddressesTask.ConfigureAwait(false); + + AddressResult[] results = new AddressResult[ipv4Addresses.Length + ipv6Addresses.Length]; + ipv6Addresses.CopyTo(results, 0); + ipv4Addresses.CopyTo(results, ipv6Addresses.Length); + return results; + } + + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); + + if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) + { + throw new ArgumentOutOfRangeException(nameof(addressFamily), addressFamily, "Invalid address family"); + } + + if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // name localhost exists outside of DNS and can't be resolved by a DNS server + if (addressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4) + { + return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.Loopback)]); + } + else if (addressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6) + { + return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback)]); + } + + return ValueTask.FromResult([]); + } + + // dnsSafeName is Disposed by SendQueryWithTelemetry + EncodedDomainName dnsSafeName = GetNormalizedHostName(name); + var queryType = addressFamily == AddressFamily.InterNetwork ? QueryType.A : QueryType.AAAA; + return SendQueryWithTelemetry(name, dnsSafeName, queryType, ProcessResponse, cancellationToken); + + static (SendQueryError error, AddressResult[] result) ProcessResponse(EncodedDomainName dnsSafeName, QueryType queryType, DnsResponse response) + { + List results = new List(response.Answers.Count); + + // Servers send back CNAME records together with associated A/AAAA records. Servers + // send only those CNAME records relevant to the query, and if there is a CNAME record, + // there should not be other records associated with the name. Therefore, we simply follow + // the list of CNAME aliases until we get to the primary name and return the A/AAAA records + // associated. + // + // more info: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 + // + // Most of the servers send the CNAME records in order so that we can sequentially scan the + // answers, but nothing prevents the records from being in arbitrary order. Attempt the linear + // scan first and fallback to a slower but more robust method if necessary. + + bool success = true; + EncodedDomainName currentAlias = dnsSafeName; + + foreach (var answer in response.Answers) + { + switch (answer.Type) + { + case QueryType.CNAME: + if (!TryReadTarget(answer, response.RawMessageBytes, out EncodedDomainName target)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (answer.Name.Equals(currentAlias)) + { + currentAlias = target; + continue; + } + + break; + + case var type when type == queryType: + if (!TryReadAddress(answer, queryType, out IPAddress? address)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (answer.Name.Equals(currentAlias)) + { + results.Add(new AddressResult(response.CreatedAt.AddSeconds(answer.Ttl), address)); + continue; + } + + break; + } + + // unexpected name or record type, fall back to more robust path + results.Clear(); + success = false; + break; + } + + if (success) + { + return (SendQueryError.NoError, results.ToArray()); + } + + // more expensive path for uncommon (but valid) cases where CNAME records are out of order. Use of Dictionary + // allows us to stay within O(n) complexity for the number of answers, but we will use more memory. + Dictionary aliasMap = new(); + Dictionary> aRecordMap = new(); + foreach (var answer in response.Answers) + { + if (answer.Type == QueryType.CNAME) + { + // map the alias to the target name + if (!TryReadTarget(answer, response.RawMessageBytes, out EncodedDomainName target)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (!aliasMap.TryAdd(answer.Name, target)) + { + // Duplicate CNAME record + return (SendQueryError.MalformedResponse, []); + } + } + + if (answer.Type == queryType) + { + if (!TryReadAddress(answer, queryType, out IPAddress? address)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (!aRecordMap.TryGetValue(answer.Name, out List? addressList)) + { + addressList = new List(); + aRecordMap.Add(answer.Name, addressList); + } + + addressList.Add(new AddressResult(response.CreatedAt.AddSeconds(answer.Ttl), address)); + } + } + + // follow the CNAME chain, limit the maximum number of iterations to avoid infinite loops. + int i = 0; + currentAlias = dnsSafeName; + while (aliasMap.TryGetValue(currentAlias, out EncodedDomainName nextAlias)) + { + if (i >= aliasMap.Count) + { + // circular CNAME chain + return (SendQueryError.MalformedResponse, []); + } + + i++; + + if (aRecordMap.ContainsKey(currentAlias)) + { + // both CNAME record and A/AAAA records exist for the current alias + return (SendQueryError.MalformedResponse, []); + } + + currentAlias = nextAlias; + } + + // Now we have the final target name, check if we have any A/AAAA records for it. + aRecordMap.TryGetValue(currentAlias, out List? finalAddressList); + return (SendQueryError.NoError, finalAddressList?.ToArray() ?? []); + + static bool TryReadTarget(in DnsResourceRecord record, ArraySegment messageBytes, out EncodedDomainName target) + { + Debug.Assert(record.Type == QueryType.CNAME, "Only CNAME records should be processed here."); + + target = default; + + // some servers use domain name compression even inside CNAME records. In order to decode those + // correctly, we need to pass the entire message to TryReadQName. The Data span inside the record + // should be backed by the array containing the entire DNS message. We just need to account for the + // 2 byte offset in case of TCP fallback. + var gotArray = MemoryMarshal.TryGetArray(record.Data, out ArraySegment segment); + Debug.Assert(gotArray, "Failed to get array segment"); + Debug.Assert(segment.Array == messageBytes.Array, "record data backed by different array than the original message"); + + int messageOffset = messageBytes.Offset; + + bool result = DnsPrimitives.TryReadQName(segment.Array.AsMemory(messageOffset, segment.Offset + segment.Count - messageOffset), segment.Offset - messageOffset, out EncodedDomainName targetName, out int bytesRead) && bytesRead == record.Data.Length; + if (result) + { + target = targetName; + } + + return result; + } + + static bool TryReadAddress(in DnsResourceRecord record, QueryType type, [NotNullWhen(true)] out IPAddress? target) + { + Debug.Assert(record.Type is QueryType.A or QueryType.AAAA, "Only CNAME records should be processed here."); + + target = null; + if (record.Type == QueryType.A && record.Data.Length != IPv4Length || + record.Type == QueryType.AAAA && record.Data.Length != IPv6Length) + { + return false; + } + + target = new IPAddress(record.Data.Span); + return true; + } + } + } + + private async ValueTask SendQueryWithTelemetry(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func processResponseFunc, CancellationToken cancellationToken) + { + NameResolutionActivity activity = Telemetry.StartNameResolution(name, queryType, _timeProvider.GetTimestamp()); + (SendQueryError error, TResult[] result) = await SendQueryWithRetriesAsync(name, dnsSafeName, queryType, processResponseFunc, cancellationToken).ConfigureAwait(false); + Telemetry.StopNameResolution(name, queryType, activity, null, error, _timeProvider.GetTimestamp()); + dnsSafeName.Dispose(); + + return result; + } + + internal struct SendQueryResult + { + public DnsResponse Response; + public SendQueryError Error; + } + + async ValueTask<(SendQueryError error, TResult[] result)> SendQueryWithRetriesAsync(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func processResponseFunc, CancellationToken cancellationToken) + { + SendQueryError lastError = SendQueryError.InternalError; // will be overwritten by the first attempt + for (int index = 0; index < _options.Servers.Count; index++) + { + IPEndPoint serverEndPoint = _options.Servers[index]; + + for (int attempt = 1; attempt <= _options.Attempts; attempt++) + { + DnsResponse response = default; + try + { + TResult[] results = Array.Empty(); + + try + { + SendQueryResult queryResult = await SendQueryToServerWithTimeoutAsync(serverEndPoint, name, dnsSafeName, queryType, attempt, cancellationToken).ConfigureAwait(false); + lastError = queryResult.Error; + response = queryResult.Response; + + if (lastError == SendQueryError.NoError) + { + // Given that result.Error is NoError, there should be at least one answer. + Debug.Assert(response.Answers.Count > 0); + (lastError, results) = processResponseFunc(dnsSafeName, queryType, queryResult.Response); + } + } + catch (SocketException ex) + { + Log.NetworkError(_logger, queryType, name, serverEndPoint, attempt, ex); + lastError = SendQueryError.NetworkError; + } + catch (Exception ex) when (!cancellationToken.IsCancellationRequested) + { + // internal error, propagate + Log.QueryError(_logger, queryType, name, serverEndPoint, attempt, ex); + throw; + } + + switch (lastError) + { + // + // Definitive answers, no point retrying + // + case SendQueryError.NoError: + return (lastError, results); + + case SendQueryError.NameError: + // authoritative answer that the name does not exist, no point in retrying + Log.NameError(_logger, queryType, name, serverEndPoint, attempt); + return (lastError, results); + + case SendQueryError.NoData: + // no data available for the name from authoritative server + Log.NoData(_logger, queryType, name, serverEndPoint, attempt); + return (lastError, results); + + // + // Transient errors, retry on the same server + // + case SendQueryError.Timeout: + Log.Timeout(_logger, queryType, name, serverEndPoint, attempt); + continue; + + case SendQueryError.NetworkError: + // TODO: retry with exponential backoff? + continue; + + case SendQueryError.ServerError when response.Header.ResponseCode == QueryResponseCode.ServerFailure: + // ServerFailure may indicate transient failure with upstream DNS servers, retry on the same server + Log.ErrorResponseCode(_logger, queryType, name, serverEndPoint, response.Header.ResponseCode); + continue; + + // + // Persistent errors, skip to the next server + // + case SendQueryError.ServerError: + // this should cover all response codes except NoError, NameError which are definite and handled above, and + // ServerFailure which is a transient error and handled above. + Log.ErrorResponseCode(_logger, queryType, name, serverEndPoint, response.Header.ResponseCode); + break; + + case SendQueryError.MalformedResponse: + Log.MalformedResponse(_logger, queryType, name, serverEndPoint, attempt); + break; + + case SendQueryError.InternalError: + // exception logged above. + break; + } + + // actual break that causes skipping to the next server + break; + } + finally + { + response.Dispose(); + } + } + } + + // if we get here, we exhausted all servers and all attempts + return (lastError, []); + } + + internal async ValueTask SendQueryToServerWithTimeoutAsync(IPEndPoint serverEndPoint, string name, EncodedDomainName dnsSafeName, QueryType queryType, int attempt, CancellationToken cancellationToken) + { + (CancellationTokenSource cts, bool disposeTokenSource, CancellationTokenSource pendingRequestsCts) = PrepareCancellationTokenSource(cancellationToken); + + try + { + return await SendQueryToServerAsync(serverEndPoint, name, dnsSafeName, queryType, attempt, cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when ( + !cancellationToken.IsCancellationRequested && // not cancelled by the caller + !pendingRequestsCts.IsCancellationRequested) // not cancelled by the global token (dispose) + // the only remaining token that could cancel this is the linked cts from the timeout. + { + Debug.Assert(cts.Token.IsCancellationRequested); + return new SendQueryResult { Error = SendQueryError.Timeout }; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested && ex.CancellationToken != cancellationToken) + { + // cancellation was initiated by the caller, but exception was triggered by a linked token, + // rethrow the exception with the caller's token. + cancellationToken.ThrowIfCancellationRequested(); + throw new UnreachableException(); + } + finally + { + if (disposeTokenSource) + { + cts.Dispose(); + } + } + } + + private async ValueTask SendQueryToServerAsync(IPEndPoint serverEndPoint, string name, EncodedDomainName dnsSafeName, QueryType queryType, int attempt, CancellationToken cancellationToken) + { + Log.Query(_logger, queryType, name, serverEndPoint, attempt); + + SendQueryError sendError = SendQueryError.NoError; + DateTime queryStartedTime = _timeProvider.GetUtcNow().DateTime; + DnsDataReader responseReader = default; + DnsMessageHeader header; + + try + { + // use transport override if provided + if (_options._transportOverride != null) + { + (responseReader, header, sendError) = SendDnsQueryCustomTransport(_options._transportOverride, dnsSafeName, queryType); + } + else + { + (responseReader, header) = await SendDnsQueryCoreUdpAsync(serverEndPoint, dnsSafeName, queryType, cancellationToken).ConfigureAwait(false); + + if (header.IsResultTruncated) + { + Log.ResultTruncated(_logger, queryType, name, serverEndPoint, 0); + responseReader.Dispose(); + // TCP fallback + (responseReader, header, sendError) = await SendDnsQueryCoreTcpAsync(serverEndPoint, dnsSafeName, queryType, cancellationToken).ConfigureAwait(false); + } + } + + if (sendError != SendQueryError.NoError) + { + // we failed to get back any response + return new SendQueryResult { Error = sendError }; + } + + if ((uint)header.ResponseCode > (uint)QueryResponseCode.Refused) + { + // Response code is outside of valid range + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + // Recheck that the server echoes back the DNS question + if (header.QueryCount != 1 || + !responseReader.TryReadQuestion(out var qName, out var qType, out var qClass) || + !dnsSafeName.Equals(qName) || qType != queryType || qClass != QueryClass.Internet) + { + // DNS Question mismatch + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + // Structurally separate the resource records, this will validate only the + // "outside structure" of the resource record, it will not validate the content. + int ttl = int.MaxValue; + if (!TryReadRecords(header.AnswerCount, ref ttl, ref responseReader, out List? answers) || + !TryReadRecords(header.AuthorityCount, ref ttl, ref responseReader, out List? authorities) || + !TryReadRecords(header.AdditionalRecordCount, ref ttl, ref responseReader, out List? additionals)) + { + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + DateTime expirationTime = + (answers.Count + authorities.Count + additionals.Count) > 0 ? queryStartedTime.AddSeconds(ttl) : queryStartedTime; + + SendQueryError validationError = ValidateResponse(header.ResponseCode, queryStartedTime, answers, authorities, ref expirationTime); + + // we transfer ownership of RawData to the response + DnsResponse response = new DnsResponse(responseReader.MessageBuffer, header, queryStartedTime, expirationTime, answers, authorities, additionals); + responseReader = default; // avoid disposing (and returning RawData to the pool) + + return new SendQueryResult { Response = response, Error = validationError }; + } + finally + { + responseReader.Dispose(); + } + + static bool TryReadRecords(int count, ref int ttl, ref DnsDataReader reader, out List records) + { + // Since `count` is attacker controlled, limit the initial capacity + // to 32 items to avoid excessive memory allocation. More than 32 + // records are unusual so we don't need to optimize for them. + records = new(Math.Min(count, 32)); + + for (int i = 0; i < count; i++) + { + if (!reader.TryReadResourceRecord(out var record)) + { + return false; + } + + ttl = Math.Min(ttl, record.Ttl); + records.Add(new DnsResourceRecord(record.Name, record.Type, record.Class, record.Ttl, record.Data)); + } + + return true; + } + } + + internal static bool GetNegativeCacheExpiration(DateTime createdAt, List authorities, out DateTime expiration) + { + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // Like normal answers negative answers have a time to live (TTL). As + // there is no record in the answer section to which this TTL can be + // applied, the TTL must be carried by another method. This is done by + // including the SOA record from the zone in the authority section of + // the reply. When the authoritative server creates this record its TTL + // is taken from the minimum of the SOA.MINIMUM field and SOA's TTL. + // This TTL decrements in a similar manner to a normal cached answer and + // upon reaching zero (0) indicates the cached negative answer MUST NOT + // be used again. + // + + DnsResourceRecord? soa = authorities.FirstOrDefault(r => r.Type == QueryType.SOA); + if (soa != null && DnsPrimitives.TryReadSoa(soa.Value.Data, out _, out _, out _, out _, out _, out _, out uint minimum, out _)) + { + expiration = createdAt.AddSeconds(Math.Min(minimum, soa.Value.Ttl)); + return true; + } + + expiration = default; + return false; + } + + internal static SendQueryError ValidateResponse(QueryResponseCode responseCode, DateTime createdAt, List answers, List authorities, ref DateTime expiration) + { + if (responseCode == QueryResponseCode.NoError) + { + if (answers.Count > 0) + { + return SendQueryError.NoError; + } + // + // RFC 2308 Section 2.2 - No Data + // + // NODATA is indicated by an answer with the RCODE set to NOERROR and no + // relevant answers in the answer section. The authority section will + // contain an SOA record, or there will be no NS records there. + // + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // A negative answer that resulted from a no data error (NODATA) should + // be cached such that it can be retrieved and returned in response to + // another query for the same that resulted in + // the cached negative response. + // + if (!authorities.Any(r => r.Type == QueryType.NS) && GetNegativeCacheExpiration(createdAt, authorities, out DateTime newExpiration)) + { + expiration = newExpiration; + // _cache.TryAdd(name, queryType, expiration, Array.Empty()); + } + return SendQueryError.NoData; + } + + if (responseCode == QueryResponseCode.NameError) + { + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // A negative answer that resulted from a name error (NXDOMAIN) should + // be cached such that it can be retrieved and returned in response to + // another query for the same that resulted in the + // cached negative response. + // + if (GetNegativeCacheExpiration(createdAt, authorities, out DateTime newExpiration)) + { + expiration = newExpiration; + // _cache.TryAddNonexistent(name, expiration); + } + + return SendQueryError.NameError; + } + + return SendQueryError.ServerError; + } + + internal static (DnsDataReader reader, DnsMessageHeader header, SendQueryError sendError) SendDnsQueryCustomTransport(Func, int, int> callback, EncodedDomainName dnsSafeName, QueryType queryType) + { + byte[] buffer = ArrayPool.Shared.Rent(2048); + try + { + (ushort transactionId, int length) = EncodeQuestion(buffer, dnsSafeName, queryType); + length = callback(buffer, length); + + DnsDataReader responseReader = new DnsDataReader(new ArraySegment(buffer, 0, length), true); + + if (!responseReader.TryReadHeader(out DnsMessageHeader header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + return (default, default, SendQueryError.MalformedResponse); + } + + // transfer ownership of buffer to the caller + buffer = null!; + return (responseReader, header, SendQueryError.NoError); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + internal static async ValueTask<(DnsDataReader reader, DnsMessageHeader header)> SendDnsQueryCoreUdpAsync(IPEndPoint serverEndPoint, EncodedDomainName dnsSafeName, QueryType queryType, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(512); + try + { + Memory memory = buffer; + (ushort transactionId, int length) = EncodeQuestion(memory, dnsSafeName, queryType); + + using var socket = new Socket(serverEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + await socket.SendToAsync(memory.Slice(0, length), SocketFlags.None, serverEndPoint, cancellationToken).ConfigureAwait(false); + + DnsDataReader responseReader; + DnsMessageHeader header; + + while (true) + { + // Because this is UDP, the response must be in a single packet, + // if the response does not fit into a single UDP packet, the server will + // set the Truncated flag in the header, and we will need to retry with TCP. + int packetLength = await socket.ReceiveAsync(memory, SocketFlags.None, cancellationToken).ConfigureAwait(false); + + if (packetLength < DnsMessageHeader.HeaderLength) + { + continue; + } + + responseReader = new DnsDataReader(new ArraySegment(buffer, 0, packetLength), true); + if (!responseReader.TryReadHeader(out header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + // header mismatch, this is not a response to our query + continue; + } + + // ownership of the buffer is transferred to the reader, caller will dispose. + buffer = null!; + return (responseReader, header); + } + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + internal static async ValueTask<(DnsDataReader reader, DnsMessageHeader header, SendQueryError error)> SendDnsQueryCoreTcpAsync(IPEndPoint serverEndPoint, EncodedDomainName dnsSafeName, QueryType queryType, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(8 * 1024); + try + { + // When sending over TCP, the message is prefixed by 2B length + (ushort transactionId, int length) = EncodeQuestion(buffer.AsMemory(2), dnsSafeName, queryType); + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)length); + + using var socket = new Socket(serverEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + await socket.ConnectAsync(serverEndPoint, cancellationToken).ConfigureAwait(false); + await socket.SendAsync(buffer.AsMemory(0, length + 2), SocketFlags.None, cancellationToken).ConfigureAwait(false); + + int responseLength = -1; + int bytesRead = 0; + while (responseLength < 0 || bytesRead < responseLength + 2) + { + int read = await socket.ReceiveAsync(buffer.AsMemory(bytesRead), SocketFlags.None, cancellationToken).ConfigureAwait(false); + bytesRead += read; + + if (read == 0) + { + // connection closed before receiving complete response message + return (default, default, SendQueryError.MalformedResponse); + } + + if (responseLength < 0 && bytesRead >= 2) + { + responseLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2)); + + if (responseLength + 2 > buffer.Length) + { + // even though this is user-controlled pre-allocation, it is limited to + // 64 kB, so it should be fine. + var largerBuffer = ArrayPool.Shared.Rent(responseLength + 2); + Array.Copy(buffer, largerBuffer, bytesRead); + ArrayPool.Shared.Return(buffer); + buffer = largerBuffer; + } + } + } + + DnsDataReader responseReader = new DnsDataReader(new ArraySegment(buffer, 2, responseLength), true); + if (!responseReader.TryReadHeader(out DnsMessageHeader header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + // header mismatch on TCP fallback + return (default, default, SendQueryError.MalformedResponse); + } + + // transfer ownership of buffer to the caller + buffer = null!; + return (responseReader, header, SendQueryError.NoError); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + private static (ushort id, int length) EncodeQuestion(Memory buffer, EncodedDomainName dnsSafeName, QueryType queryType) + { + DnsMessageHeader header = new DnsMessageHeader + { + TransactionId = (ushort)RandomNumberGenerator.GetInt32(ushort.MaxValue + 1), + QueryFlags = QueryFlags.RecursionDesired, + QueryCount = 1 + }; + + DnsDataWriter writer = new DnsDataWriter(buffer); + if (!writer.TryWriteHeader(header) || + !writer.TryWriteQuestion(dnsSafeName, queryType, QueryClass.Internet)) + { + // should never happen since we validated the name length before + throw new InvalidOperationException("Buffer too small"); + } + return (header.TransactionId, writer.Position); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + // Cancel all pending requests (if any). Note that we don't call CancelPendingRequests() but cancel + // the CTS directly. The reason is that CancelPendingRequests() would cancel the current CTS and create + // a new CTS. We don't want a new CTS in this case. + _pendingRequestsCts.Cancel(); + _pendingRequestsCts.Dispose(); + } + } + + private (CancellationTokenSource TokenSource, bool DisposeTokenSource, CancellationTokenSource PendingRequestsCts) PrepareCancellationTokenSource(CancellationToken cancellationToken) + { + // We need a CancellationTokenSource to use with the request. We always have the global + // _pendingRequestsCts to use, plus we may have a token provided by the caller, and we may + // have a timeout. If we have a timeout or a caller-provided token, we need to create a new + // CTS (we can't, for example, timeout the pending requests CTS, as that could cancel other + // unrelated operations). Otherwise, we can use the pending requests CTS directly. + + // Snapshot the current pending requests cancellation source. It can change concurrently due to cancellation being requested + // and it being replaced, and we need a stable view of it: if cancellation occurs and the caller's token hasn't been canceled, + // it's either due to this source or due to the timeout, and checking whether this source is the culprit is reliable whereas + // it's more approximate checking elapsed time. + CancellationTokenSource pendingRequestsCts = _pendingRequestsCts; + TimeSpan timeout = _options.Timeout; + + bool hasTimeout = timeout != System.Threading.Timeout.InfiniteTimeSpan; + if (hasTimeout || cancellationToken.CanBeCanceled) + { + CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, pendingRequestsCts.Token); + if (hasTimeout) + { + cts.CancelAfter(timeout); + } + + return (cts, DisposeTokenSource: true, pendingRequestsCts); + } + + return (pendingRequestsCts, DisposeTokenSource: false, pendingRequestsCts); + } + + private static EncodedDomainName GetNormalizedHostName(string name) + { + byte[] buffer = ArrayPool.Shared.Rent(256); + try + { + if (!DnsPrimitives.TryWriteQName(buffer, name, out _)) + { + throw new ArgumentException($"'{name}' is not a valid DNS name.", nameof(name)); + } + + List> labels = new(); + Memory memory = buffer.AsMemory(); + while (true) + { + int len = memory.Span[0]; + + if (len == 0) + { + // root label, we are finished + break; + } + + labels.Add(memory.Slice(1, len)); + memory = memory.Slice(len + 1); + } + + buffer = null!; // ownership transferred to the EncodedDomainName + return new EncodedDomainName(labels, buffer); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs new file mode 100644 index 00000000000..914ff9aac17 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsResourceRecord +{ + public EncodedDomainName Name { get; } + public QueryType Type { get; } + public QueryClass Class { get; } + public int Ttl { get; } + public ReadOnlyMemory Data { get; } + + public DnsResourceRecord(EncodedDomainName name, QueryType type, QueryClass @class, int ttl, ReadOnlyMemory data) + { + Name = name; + Type = type; + Class = @class; + Ttl = ttl; + Data = data; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs new file mode 100644 index 00000000000..5a7fc8a0b52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsResponse : IDisposable +{ + public DnsMessageHeader Header { get; } + public List Answers { get; } + public List Authorities { get; } + public List Additionals { get; } + public DateTime CreatedAt { get; } + public DateTime Expiration { get; } + public ArraySegment RawMessageBytes { get; private set; } + + public DnsResponse(ArraySegment rawData, DnsMessageHeader header, DateTime createdAt, DateTime expiration, List answers, List authorities, List additionals) + { + RawMessageBytes = rawData; + + Header = header; + CreatedAt = createdAt; + Expiration = expiration; + Answers = answers; + Authorities = authorities; + Additionals = additionals; + } + + public void Dispose() + { + if (RawMessageBytes.Array != null) + { + ArrayPool.Shared.Return(RawMessageBytes.Array); + } + + RawMessageBytes = default; // prevent further access to the raw data + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs new file mode 100644 index 00000000000..4c258cac3ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct EncodedDomainName : IEquatable, IDisposable +{ + public IReadOnlyList> Labels { get; } + private byte[]? _pooledBuffer; + + public EncodedDomainName(List> labels, byte[]? pooledBuffer = null) + { + Labels = labels; + _pooledBuffer = pooledBuffer; + } + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + foreach (var label in Labels) + { + if (sb.Length > 0) + { + sb.Append('.'); + } + sb.Append(Encoding.ASCII.GetString(label.Span)); + } + + return sb.ToString(); + } + + public bool Equals(EncodedDomainName other) + { + if (Labels.Count != other.Labels.Count) + { + return false; + } + + for (int i = 0; i < Labels.Count; i++) + { + if (!Ascii.EqualsIgnoreCase(Labels[i].Span, other.Labels[i].Span)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) + { + return obj is EncodedDomainName other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hash = new HashCode(); + + foreach (var label in Labels) + { + foreach (byte b in label.Span) + { + hash.Add((byte)char.ToLower((char)b)); + } + } + + return hash.ToHashCode(); + } + + public void Dispose() + { + if (_pooledBuffer != null) + { + ArrayPool.Shared.Return(_pooledBuffer); + } + + _pooledBuffer = null; + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs new file mode 100644 index 00000000000..e09168d9552 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal interface IDnsResolver +{ + ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default); + ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default); + ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs new file mode 100644 index 00000000000..c2ef13f922e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.NetworkInformation; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class NetworkInfo +{ + // basic option to get DNS serves via NetworkInfo. We may get it directly later via proper APIs. + public static ResolverOptions GetOptions() + { + List servers = new List(); + + foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) + { + IPInterfaceProperties properties = nic.GetIPProperties(); + // avoid loopback, VPN etc. Should be re-visited. + + if (nic.NetworkInterfaceType == NetworkInterfaceType.Ethernet && nic.OperationalStatus == OperationalStatus.Up) + { + foreach (IPAddress server in properties.DnsAddresses) + { + IPEndPoint ep = new IPEndPoint(server, 53); // 53 is standard DNS port + if (!servers.Contains(ep)) + { + servers.Add(ep); + } + } + } + } + + return new ResolverOptions(servers); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs new file mode 100644 index 00000000000..732ca0216da --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal enum QueryClass +{ + Internet = 1 +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs new file mode 100644 index 00000000000..02474b6cda1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +[Flags] +internal enum QueryFlags : ushort +{ + RecursionAvailable = 0x0080, + RecursionDesired = 0x0100, + ResultTruncated = 0x0200, + HasAuthorityAnswer = 0x0400, + HasResponse = 0x8000, + ResponseCodeMask = 0x000F, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs new file mode 100644 index 00000000000..dd51c712112 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +/// +/// The response code (RCODE) in a DNS query response. +/// +internal enum QueryResponseCode : byte +{ + /// + /// No error condition + /// + NoError = 0, + + /// + /// The name server was unable to interpret the query. + /// + FormatError = 1, + + /// + /// The name server was unable to process this query due to a problem with the name server. + /// + ServerFailure = 2, + + /// + /// Meaningful only for responses from an authoritative name server, this + /// code signifies that the domain name referenced in the query does not + /// exist. + /// + NameError = 3, + + /// + /// The name server does not support the requested kind of query. + /// + NotImplemented = 4, + + /// + /// The name server refuses to perform the specified operation for policy reasons. + /// + Refused = 5, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs new file mode 100644 index 00000000000..2ccc898a5b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +/// +/// DNS Query Types. +/// +internal enum QueryType +{ + /// + /// A host address. + /// + A = 1, + + /// + /// An authoritative name server. + /// + NS = 2, + + /// + /// The canonical name for an alias. + /// + CNAME = 5, + + /// + /// Marks the start of a zone of authority. + /// + SOA = 6, + + /// + /// Mail exchange. + /// + MX = 15, + + /// + /// Text strings. + /// + TXT = 16, + + /// + /// IPv6 host address. (RFC 3596) + /// + AAAA = 28, + + /// + /// Location information. (RFC 2782) + /// + SRV = 33, + + /// + /// Wildcard match. + /// + All = 255 +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs new file mode 100644 index 00000000000..fbfdc5ae027 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Runtime.Versioning; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class ResolvConf +{ + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("osx")] + public static ResolverOptions GetOptions() + { + return GetOptions(new StreamReader("/etc/resolv.conf")); + } + + public static ResolverOptions GetOptions(TextReader reader) + { + List serverList = new(); + + while (reader.ReadLine() is string line) + { + string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (line.StartsWith("nameserver")) + { + if (tokens.Length >= 2 && IPAddress.TryParse(tokens[1], out IPAddress? address)) + { + serverList.Add(new IPEndPoint(address, 53)); // 53 is standard DNS port + + if (serverList.Count == 3) + { + break; // resolv.conf manpage allow max 3 nameservers anyway + } + } + } + } + + if (serverList.Count == 0) + { + // If no nameservers are configured, fall back to the default behavior of using the system resolver configuration. + return NetworkInfo.GetOptions(); + } + + return new ResolverOptions(serverList); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs new file mode 100644 index 00000000000..51d03f64bfd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed class ResolverOptions +{ + public IReadOnlyList Servers; + public int Attempts = 2; + public TimeSpan Timeout = TimeSpan.FromSeconds(3); + + // override for testing purposes + internal Func, int, int>? _transportOverride; + + public ResolverOptions(IReadOnlyList servers) + { + if (servers.Count == 0) + { + throw new ArgumentException("At least one DNS server is required.", nameof(servers)); + } + + Servers = servers; + } + + public ResolverOptions(IPEndPoint server) + { + Servers = new IPEndPoint[] { server }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs new file mode 100644 index 00000000000..aed799ac8d6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal record struct AddressResult(DateTime ExpiresAt, IPAddress Address); + +internal record struct ServiceResult(DateTime ExpiresAt, int Priority, int Weight, int Port, string Target, AddressResult[] Addresses); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs new file mode 100644 index 00000000000..3ba5632e207 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal enum SendQueryError +{ + /// + /// DNS query was successful and returned response message with answers. + /// + NoError, + + /// + /// Server failed to respond to the query withing specified timeout. + /// + Timeout, + + /// + /// Server returned a response with an error code. + /// + ServerError, + + /// + /// Server returned a malformed response. + /// + MalformedResponse, + + /// + /// Server returned a response indicating that the name exists, but no data are available. + /// + NoData, + + /// + /// Server returned a response indicating the name does not exist. + /// + NameError, + + /// + /// Network-level error occurred during the query. + /// + NetworkError, + + /// + /// Internal error on part of the implementation. + /// + InternalError, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 98b9de1fd68..7d05243f741 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.Hosting; @@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore new file mode 100644 index 00000000000..0151cc4e360 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore @@ -0,0 +1,2 @@ +# corpuses generated by the fuzzing engine +corpuses/** \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs new file mode 100644 index 00000000000..1b180d74b9d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +internal sealed class DnsResponseFuzzer : IFuzzer +{ + DnsResolver? _resolver; + byte[]? _buffer; + int _length; + + public void FuzzTarget(ReadOnlySpan data) + { + // lazy init + if (_resolver == null) + { + _buffer = new byte[4096]; + _resolver = new DnsResolver(new ResolverOptions(new IPEndPoint(IPAddress.Loopback, 53)) + { + Timeout = TimeSpan.FromSeconds(5), + Attempts = 1, + _transportOverride = (buffer, length) => + { + // the first two bytes are the random transaction ID, so we keep that + // and use the fuzzing payload for the rest of the DNS response + _buffer.AsSpan(0, Math.Min(_length, buffer.Length - 2)).CopyTo(buffer.Span.Slice(2)); + return _length + 2; + } + }); + } + + data.CopyTo(_buffer!); + _length = data.Length; + + // the _transportOverride makes the execution synchronous + ValueTask task = _resolver!.ResolveIPAddressesAsync("www.example.com", AddressFamily.InterNetwork, CancellationToken.None); + Debug.Assert(task.IsCompleted, "Task should be completed synchronously"); + task.GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs new file mode 100644 index 00000000000..72f84b3c959 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +internal sealed class EncodedDomainNameFuzzer : IFuzzer +{ + public void FuzzTarget(ReadOnlySpan data) + { + byte[] buffer = ArrayPool.Shared.Rent(data.Length); + try + { + data.CopyTo(buffer); + + // attempt to read at any offset to really stress the parser + for (int i = 0; i < data.Length; i++) + { + if (!DnsPrimitives.TryReadQName(buffer.AsMemory(0, data.Length), i, out EncodedDomainName name, out _)) + { + continue; + } + + // the domain name should be readable + _ = name.ToString(); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs new file mode 100644 index 00000000000..f657245a842 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs @@ -0,0 +1,48 @@ +// 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; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +internal sealed class WriteDomainNameRoundTripFuzzer : IFuzzer +{ + private static readonly System.Globalization.IdnMapping s_idnMapping = new(); + public void FuzzTarget(ReadOnlySpan data) + { + // first byte is the offset of the domain name, rest is the actual + // (simulated) DNS message payload + + byte[] buffer = ArrayPool.Shared.Rent(data.Length * 2); + + try + { + string domainName = Encoding.UTF8.GetString(data); + if (!DnsPrimitives.TryWriteQName(buffer, domainName, out int written)) + { + return; + } + + if (!DnsPrimitives.TryReadQName(buffer.AsMemory(0, written), 0, out EncodedDomainName name, out int read)) + { + return; + } + + if (read != written) + { + throw new InvalidOperationException($"Read {read} bytes, but wrote {written} bytes"); + } + + string readName = name.ToString(); + + if (!string.Equals(s_idnMapping.GetAscii(domainName).TrimEnd('.'), readName, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Domain name mismatch: {readName} != {domainName}"); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs new file mode 100644 index 00000000000..2ff9d86b2ce --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +global using System.Buffers; +global using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs new file mode 100644 index 00000000000..4b4c8c99b4b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +public interface IFuzzer +{ + string Name => GetType().Name; + void FuzzTarget(ReadOnlySpan data); +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj new file mode 100644 index 00000000000..6572c27a1fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -0,0 +1,18 @@ + + + + $(DefaultTargetFramework) + enable + enable + Exe + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs new file mode 100644 index 00000000000..22b1580d1ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SharpFuzz; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +public static class Program +{ + public static void Main(string[] args) + { + IFuzzer[] fuzzers = typeof(Program).Assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract) + .Where(t => t.GetInterfaces().Contains(typeof(IFuzzer))) + .Select(t => (IFuzzer)Activator.CreateInstance(t)!) + .OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + void PrintUsage() + { + Console.Error.WriteLine($""" + Usage: + DotnetFuzzing list + DotnetFuzzing [input file/directory] + // DotnetFuzzing prepare-onefuzz + + Available fuzzers: + {string.Join(Environment.NewLine, fuzzers.Select(f => $" {f.Name}"))} + """); + } + + if (args.Length == 0) + { + PrintUsage(); + return; + } + + string arg = args[0]; + IFuzzer? fuzzer = fuzzers.FirstOrDefault(f => string.Equals(f.Name, arg, StringComparison.OrdinalIgnoreCase)); + if (fuzzer == null) + { + Console.Error.WriteLine($"Unknown fuzzer: {arg}"); + PrintUsage(); + return; + } + + string? inputFiles = args.Length > 1 ? args[1] : null; + if (string.IsNullOrEmpty(inputFiles)) + { + // no input files, let the fuzzer generate + Fuzzer.LibFuzzer.Run(fuzzer.FuzzTarget); + return; + } + + string[] files = Directory.Exists(inputFiles) + ? Directory.GetFiles(inputFiles) + : [inputFiles]; + + foreach (string inputFile in files) + { + fuzzer.FuzzTarget(File.ReadAllBytes(inputFile)); + } + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com new file mode 100644 index 0000000000000000000000000000000000000000..bb40cd100c651f4d67ef95ae5d0b10edf886eb34 GIT binary patch literal 141 zcmZo{U|?imVE_W=^73-_)QZI1f}B+5X=X_(b6#o*!vS50YC{(W5!OUQ6C)#*l;Y$fw#4kj+{DZSUI(HJSlAD(timeProvider) + .AddSingleton() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) .BuildServiceProvider(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index b58d9e2f4ec..ec21bf9fa9c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using DnsClient; -using DnsClient.Protocol; +using System.Net.Sockets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; @@ -19,88 +19,38 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; ///
    public class DnsSrvServiceEndpointResolverTests { - private sealed class FakeDnsClient : IDnsQuery + private sealed class FakeDnsResolver : IDnsResolver { - public Func>? QueryAsyncFunc { get; set; } + public Func>? ResolveIPAddressesAsyncFunc { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc!.Invoke(name, addressFamily, cancellationToken); - public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) - => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); - public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - } + public Func>? ResolveIPAddressesAsyncFunc2 { get; set; } - private sealed class FakeDnsQueryResponse : IDnsQueryResponse - { - public IReadOnlyList? Questions { get; set; } - public IReadOnlyList? Additionals { get; set; } - public IEnumerable? AllRecords { get; set; } - public IReadOnlyList? Answers { get; set; } - public IReadOnlyList? Authorities { get; set; } - public string? AuditTrail { get; set; } - public string? ErrorMessage { get; set; } - public bool HasError { get; set; } - public DnsResponseHeader? Header { get; set; } - public int MessageSize { get; set; } - public NameServer? NameServer { get; set; } - public DnsQuerySettings? Settings { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc2!.Invoke(name, cancellationToken); + + public Func>? ResolveServiceAsyncFunc { get; set; } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) => ResolveServiceAsyncFunc!.Invoke(name, cancellationToken); } [Fact] public async Task ResolveServiceEndpoint_DnsSrv() { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), - new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) - } - }; + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", []) + ]; - return Task.FromResult(response); + return ValueTask.FromResult(response); } }; var services = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); @@ -119,7 +69,7 @@ public async Task ResolveServiceEndpoint_DnsSrv() var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + Assert.Equal(new DnsEndPoint("srv-c", 7777), eps[2].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => { @@ -137,28 +87,17 @@ public async Task ResolveServiceEndpoint_DnsSrv() [Theory] public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), - new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) - } - }; + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", []) + ]; - return Task.FromResult(response); + return ValueTask.FromResult(response); } }; var configSource = new MemoryConfigurationSource @@ -171,7 +110,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( }; var config = new ConfigurationBuilder().Add(configSource); var serviceCollection = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddSingleton(config.Build()) .AddServiceDiscoveryCore(); if (dnsFirst) @@ -211,7 +150,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( var eps = initialResult.EndpointSource.Endpoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); + Assert.Equal(new DnsEndPoint("srv-c", 7777), eps[2].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => { diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 24faf1d8abe..f6202d17d37 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs new file mode 100644 index 00000000000..8c646ac18ee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class CancellationTests : LoopbackDnsTestBase +{ + public CancellationTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task PreCanceledToken_Throws() + { + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = await Assert.ThrowsAnyAsync(async () => await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork, cts.Token)); + + Assert.Equal(cts.Token, ex.CancellationToken); + } + + [Fact] + public async Task CancellationInProgress_Throws() + { + CancellationTokenSource cts = new CancellationTokenSource(); + + var task = Assert.ThrowsAnyAsync(async () => await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork, cts.Token)); + + await DnsServer.ProcessUdpRequest(_ => + { + cts.Cancel(); + return Task.CompletedTask; + }); + + OperationCanceledException ex = await task; + Assert.Equal(cts.Token, ex.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs new file mode 100644 index 00000000000..aad32fe785f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsDataReaderTests +{ + [Fact] + public void ReadResourceRecord_Success() + { + // example A record for example.com + byte[] buffer = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsDataReader reader = new DnsDataReader(buffer); + Assert.True(reader.TryReadResourceRecord(out DnsResourceRecord record)); + + Assert.Equal("www.example.com", record.Name.ToString()); + Assert.Equal(QueryType.A, record.Type); + Assert.Equal(QueryClass.Internet, record.Class); + Assert.Equal(3600, record.Ttl); + Assert.Equal(4, record.Data.Length); + } + + [Fact] + public void ReadResourceRecord_Truncated_Fails() + { + // example A record for example.com + byte[] buffer = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + for (int i = 0; i < buffer.Length; i++) + { + DnsDataReader reader = new DnsDataReader(new ArraySegment(buffer, 0, i)); + Assert.False(reader.TryReadResourceRecord(out _)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs new file mode 100644 index 00000000000..b2039ce5a4c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsDataWriterTests +{ + [Fact] + public void WriteResourceRecord_Success() + { + // example A record for example.com + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsResourceRecord record = new DnsResourceRecord(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet, 3600, new byte[4]); + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteResourceRecord(record)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + [Fact] + public void WriteResourceRecord_Truncated_Fails() + { + // example A record for example.com + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsResourceRecord record = new DnsResourceRecord(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet, 3600, new byte[4]); + + byte[] buffer = new byte[512]; + for (int i = 0; i < expected.Length; i++) + { + DnsDataWriter writer = new DnsDataWriter(buffer.AsMemory(0, i)); + Assert.False(writer.TryWriteResourceRecord(record)); + } + } + + [Fact] + public void WriteQuestion_Success() + { + // example question for example.com (A record) + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01 + ]; + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteQuestion(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + [Fact] + public void WriteQuestion_Truncated_Fails() + { + // example question for example.com (A record) + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01 + ]; + + byte[] buffer = new byte[512]; + for (int i = 0; i < expected.Length; i++) + { + DnsDataWriter writer = new DnsDataWriter(buffer.AsMemory(0, i)); + Assert.False(writer.TryWriteQuestion(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet)); + } + } + + [Fact] + public void WriteHeader_Success() + { + // example header + byte[] expected = [ + // ID (0x1234) + 0x12, 0x34, + // Flags (0x5678) + 0x56, 0x78, + // Question count (1) + 0x00, 0x01, + // Answer count (0) + 0x00, 0x02, + // Authority count (0) + 0x00, 0x03, + // Additional count (0) + 0x00, 0x04 + ]; + + DnsMessageHeader header = new() + { + TransactionId = 0x1234, + QueryFlags = (QueryFlags)0x5678, + QueryCount = 1, + AnswerCount = 2, + AuthorityCount = 3, + AdditionalRecordCount = 4, + }; + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteHeader(header)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + private static EncodedDomainName EncodeDomainName(string name) + { + byte[] nameBuffer = new byte[512]; + Assert.True(DnsPrimitives.TryWriteQName(nameBuffer, name, out int nameLength)); + Assert.True(DnsPrimitives.TryReadQName(nameBuffer.AsMemory(0, nameLength), 0, out EncodedDomainName encodedDomainName, out _)); + return encodedDomainName; + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs new file mode 100644 index 00000000000..6733a553bad --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsPrimitivesTests +{ + public static TheoryData QNameData => new() + { + { "www.example.com", "\x0003www\x0007example\x0003com\x0000"u8.ToArray() }, + { "example.com", "\x0007example\x0003com\x0000"u8.ToArray() }, + { "com", "\x0003com\x0000"u8.ToArray() }, + { "example", "\x0007example\x0000"u8.ToArray() }, + { "www", "\x0003www\x0000"u8.ToArray() }, + { "a", "\x0001a\x0000"u8.ToArray() }, + }; + + [Theory] + [MemberData(nameof(QNameData))] + public void TryWriteQName_Success(string name, byte[] expected) + { + byte[] buffer = new byte[512]; + + Assert.True(DnsPrimitives.TryWriteQName(buffer, name, out int written)); + Assert.Equal(name.Length + 2, written); + Assert.Equal(expected, buffer.AsSpan().Slice(0, written).ToArray()); + } + + [Fact] + public void TryWriteQName_LabelTooLong_False() + { + byte[] buffer = new byte[512]; + + Assert.False(DnsPrimitives.TryWriteQName(buffer, new string('a', 70), out _)); + } + + [Fact] + public void TryWriteQName_BufferTooShort_Fails() + { + byte[] buffer = new byte[512]; + string name = "www.example.com"; + + for (int i = 0; i < name.Length + 2; i++) + { + Assert.False(DnsPrimitives.TryWriteQName(buffer.AsSpan(0, i), name, out _)); + } + } + + [Theory] + [InlineData("www.-0.com")] + [InlineData("www.-a.com")] + [InlineData("www.a-.com")] + [InlineData("www.a_a.com")] + [InlineData("www.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com")] // 64 occurrences of 'a' (too long) + [InlineData("www.a~a.com")] // 64 occurrences of 'a' (too long) + [InlineData("www..com")] + [InlineData("www..")] + public void TryWriteQName_InvalidName_ReturnsFalse(string name) + { + byte[] buffer = new byte[512]; + Assert.False(DnsPrimitives.TryWriteQName(buffer, name, out _)); + } + + [Fact] + public void TryWriteQName_ExplicitRoot_Success() + { + string name1 = "www.example.com"; + string name2 = "www.example.com."; + + byte[] buffer1 = new byte[512]; + byte[] buffer2 = new byte[512]; + + Assert.True(DnsPrimitives.TryWriteQName(buffer1, name1, out int written1)); + Assert.True(DnsPrimitives.TryWriteQName(buffer2, name2, out int written2)); + Assert.Equal(written1, written2); + Assert.Equal(buffer1.AsSpan().Slice(0, written1).ToArray(), buffer2.AsSpan().Slice(0, written2).ToArray()); + } + + [Theory] + [MemberData(nameof(QNameData))] + public void TryReadQName_Success(string expected, byte[] serialized) + { + Assert.True(DnsPrimitives.TryReadQName(serialized, 0, out EncodedDomainName actual, out int bytesRead)); + Assert.Equal(expected, actual.ToString()); + Assert.Equal(serialized.Length, bytesRead); + } + + [Fact] + public void TryReadQName_TruncatedData_Fails() + { + ReadOnlyMemory data = "\x0003www\x0007example\x0003com\x0000"u8.ToArray(); + + for (int i = 0; i < data.Length; i++) + { + Assert.False(DnsPrimitives.TryReadQName(data.Slice(0, i), 0, out _, out _)); + } + } + + [Fact] + public void TryReadQName_Pointer_Success() + { + // [7B padding], example.com. www->[ptr to example.com.] + Memory data = "padding\x0007example\x0003com\x0000\x0003www\x00\x07"u8.ToArray(); + data.Span[^2] = 0xc0; + + Assert.True(DnsPrimitives.TryReadQName(data, data.Length - 6, out EncodedDomainName actual, out int bytesRead)); + Assert.Equal("www.example.com", actual.ToString()); + Assert.Equal(6, bytesRead); + } + + [Fact] + public void TryReadQName_PointerTruncated_Fails() + { + // [7B padding], example.com. www->[ptr to example.com.] + Memory data = "padding\x0007example\x0003com\x0000\x0003www\x00\x07"u8.ToArray(); + data.Span[^2] = 0xc0; + + for (int i = 0; i < data.Length; i++) + { + Assert.False(DnsPrimitives.TryReadQName(data.Slice(0, i), data.Length - 6, out _, out _)); + } + } + + [Fact] + public void TryReadQName_ForwardPointer_Fails() + { + // www->[ptr to example.com], [7B padding], example.com. + Memory data = "\x03www\x00\x000dpadding\x0007example\x0003com\x00"u8.ToArray(); + data.Span[4] = 0xc0; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Fact] + public void TryReadQName_PointerToSelf_Fails() + { + // www->[ptr to www->...] + Memory data = "\x0003www\0\0"u8.ToArray(); + data.Span[4] = 0xc0; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Fact] + public void TryReadQName_PointerToPointer_Fails() + { + // com, example[->com], example2[->[->com]] + Memory data = "\x0003com\0\x0007example\0\0\x0008example2\0\0"u8.ToArray(); + data.Span[13] = 0xc0; + data.Span[14] = 0x00; // -> com + data.Span[24] = 0xc0; + data.Span[25] = 13; // -> -> com + + Assert.False(DnsPrimitives.TryReadQName(data, 15, out _, out _)); + } + + [Fact] + public void TryReadQName_ReservedBits() + { + Memory data = "\x0003www\x00c0"u8.ToArray(); + data.Span[0] = 0x40; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Theory] + [InlineData(253)] + [InlineData(254)] + [InlineData(255)] + public void TryReadQName_NameTooLong(int length) + { + // longest possible label is 63 bytes + 1 byte for length + byte[] labelData = new byte[64]; + Array.Fill(labelData, (byte)'a'); + labelData[0] = 63; + + int remainder = length - 3 * 64; + + byte[] lastLabelData = new byte[remainder + 1]; + Array.Fill(lastLabelData, (byte)'a'); + lastLabelData[0] = (byte)remainder; + + byte[] data = Enumerable.Repeat(labelData, 3).SelectMany(x => x).Concat(lastLabelData).Concat(new byte[1]).ToArray(); + if (length > 253) + { + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + else + { + Assert.True(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs new file mode 100644 index 00000000000..4789e21c575 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs @@ -0,0 +1,331 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +internal sealed class LoopbackDnsServer : IDisposable +{ + private readonly Socket _dnsSocket; + private Socket? _tcpSocket; + + public IPEndPoint DnsEndPoint => (IPEndPoint)_dnsSocket.LocalEndPoint!; + + public LoopbackDnsServer() + { + _dnsSocket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _dnsSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + } + + public void Dispose() + { + _dnsSocket.Dispose(); + _tcpSocket?.Dispose(); + } + + private static async Task ProcessRequestCore(IPEndPoint remoteEndPoint, ArraySegment message, Func action, Memory responseBuffer) + { + DnsDataReader reader = new DnsDataReader(message); + + if (!reader.TryReadHeader(out DnsMessageHeader header) || + !reader.TryReadQuestion(out var name, out var type, out var @class)) + { + return 0; + } + + LoopbackDnsResponseBuilder responseBuilder = new(name.ToString(), type, @class); + responseBuilder.TransactionId = header.TransactionId; + responseBuilder.Flags = header.QueryFlags | QueryFlags.HasResponse; + responseBuilder.ResponseCode = QueryResponseCode.NoError; + + await action(responseBuilder, remoteEndPoint); + + return responseBuilder.Write(responseBuffer); + } + + public async Task ProcessUdpRequest(Func action) + { + byte[] buffer = ArrayPool.Shared.Rent(512); + try + { + EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult result = await _dnsSocket.ReceiveFromAsync(buffer, remoteEndPoint); + + int bytesWritten = await ProcessRequestCore((IPEndPoint)result.RemoteEndPoint, new ArraySegment(buffer, 0, result.ReceivedBytes), action, buffer.AsMemory(0, 512)); + + await _dnsSocket.SendToAsync(buffer.AsMemory(0, bytesWritten), SocketFlags.None, result.RemoteEndPoint); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public Task ProcessUdpRequest(Func action) + { + return ProcessUdpRequest((builder, _) => action(builder)); + } + + public async Task ProcessTcpRequest(Func action) + { + if (_tcpSocket is null) + { + _tcpSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _tcpSocket.Bind(new IPEndPoint(IPAddress.Loopback, ((IPEndPoint)_dnsSocket.LocalEndPoint!).Port)); + _tcpSocket.Listen(); + } + + using Socket tcpClient = await _tcpSocket.AcceptAsync(); + + byte[] buffer = ArrayPool.Shared.Rent(8 * 1024); + try + { + int bytesRead = 0; + int length = -1; + while (length < 0 || bytesRead < length + 2) + { + int toRead = length < 0 ? 2 : length + 2 - bytesRead; + int read = await tcpClient.ReceiveAsync(buffer.AsMemory(bytesRead, toRead), SocketFlags.None); + bytesRead += read; + + if (length < 0 && bytesRead >= 2) + { + length = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2)); + } + } + + int bytesWritten = await ProcessRequestCore((IPEndPoint)tcpClient.RemoteEndPoint!, new ArraySegment(buffer, 2, length), action, buffer.AsMemory(2)); + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), (ushort)bytesWritten); + await tcpClient.SendAsync(buffer.AsMemory(0, bytesWritten + 2), SocketFlags.None); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public Task ProcessTcpRequest(Func action) + { + return ProcessTcpRequest((builder, _) => action(builder)); + } +} + +internal sealed class LoopbackDnsResponseBuilder +{ + private static readonly SearchValues s_domainNameValidChars = SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."); + + public LoopbackDnsResponseBuilder(string name, QueryType type, QueryClass @class) + { + Name = name; + Type = type; + Class = @class; + Questions.Add((name, type, @class)); + + if (name.AsSpan().ContainsAnyExcept(s_domainNameValidChars)) + { + throw new ArgumentException($"Invalid characters in domain name '{name}'"); + } + } + + public ushort TransactionId { get; set; } + public QueryFlags Flags { get; set; } + public QueryResponseCode ResponseCode { get; set; } + + public string Name { get; } + public QueryType Type { get; } + public QueryClass Class { get; } + + public List<(string, QueryType, QueryClass)> Questions { get; } = new List<(string, QueryType, QueryClass)>(); + public List Answers { get; } = new List(); + public List Authorities { get; } = new List(); + public List Additionals { get; } = new List(); + + public int Write(Memory responseBuffer) + { + DnsDataWriter writer = new(responseBuffer); + if (!writer.TryWriteHeader(new DnsMessageHeader + { + TransactionId = TransactionId, + QueryFlags = Flags | (QueryFlags)ResponseCode, + QueryCount = (ushort)Questions.Count, + AnswerCount = (ushort)Answers.Count, + AuthorityCount = (ushort)Authorities.Count, + AdditionalRecordCount = (ushort)Additionals.Count + })) + { + throw new InvalidOperationException("Failed to write header"); + } + + byte[] buffer = ArrayPool.Shared.Rent(512); + foreach (var (questionName, questionType, questionClass) in Questions) + { + if (!DnsPrimitives.TryWriteQName(buffer, questionName, out int length) || + !DnsPrimitives.TryReadQName(buffer.AsMemory(0, length), 0, out EncodedDomainName encodedName, out _)) + { + throw new InvalidOperationException("Failed to encode domain name"); + } + if (!writer.TryWriteQuestion(encodedName, questionType, questionClass)) + { + throw new InvalidOperationException("Failed to write question"); + } + } + ArrayPool.Shared.Return(buffer); + + foreach (var answer in Answers) + { + if (!writer.TryWriteResourceRecord(answer)) + { + throw new InvalidOperationException("Failed to write answer"); + } + } + + foreach (var authority in Authorities) + { + if (!writer.TryWriteResourceRecord(authority)) + { + throw new InvalidOperationException("Failed to write authority"); + } + } + + foreach (var additional in Additionals) + { + if (!writer.TryWriteResourceRecord(additional)) + { + throw new InvalidOperationException("Failed to write additional records"); + } + } + + return writer.Position; + } + + public byte[] GetMessageBytes() + { + byte[] buffer = ArrayPool.Shared.Rent(512); + try + { + int bytesWritten = Write(buffer.AsMemory(0, 512)); + return buffer.AsSpan(0, bytesWritten).ToArray(); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} + +internal static class LoopbackDnsServerExtensions +{ + private static readonly IdnMapping s_idnMapping = new IdnMapping(); + + private static EncodedDomainName EncodeDomainName(string name) + { + var encodedLabels = name.Split('.', StringSplitOptions.RemoveEmptyEntries).Select(label => (ReadOnlyMemory)Encoding.UTF8.GetBytes(s_idnMapping.GetAscii(label))) + .ToList(); + + return new EncodedDomainName(encodedLabels); + } + + public static List AddAddress(this List records, string name, int ttl, IPAddress address) + { + QueryType type = address.AddressFamily == AddressFamily.InterNetwork ? QueryType.A : QueryType.AAAA; + records.Add(new DnsResourceRecord(EncodeDomainName(name), type, QueryClass.Internet, ttl, address.GetAddressBytes())); + return records; + } + + public static List AddCname(this List records, string name, int ttl, string alias) + { + byte[] buff = new byte[256]; + if (!DnsPrimitives.TryWriteQName(buff, alias, out int length)) + { + throw new InvalidOperationException("Failed to encode domain name"); + } + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.CNAME, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } + + public static List AddService(this List records, string name, int ttl, ushort priority, ushort weight, ushort port, string target) + { + byte[] buff = new byte[256]; + + // https://www.rfc-editor.org/rfc/rfc2782 + if (!BinaryPrimitives.TryWriteUInt16BigEndian(buff, priority) || + !BinaryPrimitives.TryWriteUInt16BigEndian(buff.AsSpan(2), weight) || + !BinaryPrimitives.TryWriteUInt16BigEndian(buff.AsSpan(4), port) || + !DnsPrimitives.TryWriteQName(buff.AsSpan(6), target, out int length)) + { + throw new InvalidOperationException("Failed to encode SRV record"); + } + + length += 6; + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.SRV, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } + + public static List AddStartOfAuthority(this List records, string name, int ttl, string mname, string rname, uint serial, uint refresh, uint retry, uint expire, uint minimum) + { + byte[] buff = new byte[256]; + + // https://www.rfc-editor.org/rfc/rfc1035#section-3.3.13 + if (!DnsPrimitives.TryWriteQName(buff, mname, out int w1) || + !DnsPrimitives.TryWriteQName(buff.AsSpan(w1), rname, out int w2) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2), serial) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 4), refresh) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 8), retry) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 12), expire) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 16), minimum)) + { + throw new InvalidOperationException("Failed to encode SOA record"); + } + + int length = w1 + w2 + 20; + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.SOA, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } +} + +internal static class DnsDataWriterExtensions +{ + internal static bool TryWriteResourceRecord(this DnsDataWriter writer, DnsResourceRecord record) + { + if (!TryWriteDomainName(writer, record.Name) || + !writer.TryWriteUInt16((ushort)record.Type) || + !writer.TryWriteUInt16((ushort)record.Class) || + !writer.TryWriteUInt32((uint)record.Ttl) || + !writer.TryWriteUInt16((ushort)record.Data.Length) || + !writer.TryWriteRawData(record.Data.Span)) + { + return false; + } + + return true; + } + + internal static bool TryWriteDomainName(this DnsDataWriter writer, EncodedDomainName name) + { + foreach (var label in name.Labels) + { + if (label.Length > 63) + { + throw new InvalidOperationException("Label length exceeds maximum of 63 bytes"); + } + + if (!writer.TryWriteByte((byte)label.Length) || + !writer.TryWriteRawData(label.Span)) + { + return false; + } + } + + // root label + return writer.TryWriteByte(0); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs new file mode 100644 index 00000000000..f76621db93c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public abstract class LoopbackDnsTestBase : IDisposable +{ + protected readonly ITestOutputHelper Output; + + internal LoopbackDnsServer DnsServer { get; } + private readonly Lazy _resolverLazy; + internal DnsResolver Resolver => _resolverLazy.Value; + internal ResolverOptions Options { get; } + protected readonly FakeTimeProvider TimeProvider; + + public LoopbackDnsTestBase(ITestOutputHelper output) + { + Output = output; + DnsServer = new(); + TimeProvider = new(); + Options = new([DnsServer.DnsEndPoint]) + { + Timeout = TimeSpan.FromSeconds(5), + Attempts = 1, + }; + _resolverLazy = new(InitializeResolver); + } + + DnsResolver InitializeResolver() + { + ServiceCollection services = new(); + services.AddXunitLogging(Output); + + // construct DnsResolver manually via internal constructor which accepts ResolverOptions + var resolver = new DnsResolver(TimeProvider, services.BuildServiceProvider().GetRequiredService>(), Options); + return resolver; + } + + public void Dispose() + { + DnsServer.Dispose(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs new file mode 100644 index 00000000000..281ffbecd24 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolvConfTests +{ + [Fact] + public void GetOptions() + { + var contents = @" +nameserver 10.96.0.10 +search default.svc.cluster.local svc.cluster.local cluster.local +options ndots:5 +@"; + + var reader = new StringReader(contents); + ResolverOptions options = ResolvConf.GetOptions(reader); + + IPEndPoint ipAddress = Assert.Single(options.Servers); + Assert.Equal(new IPEndPoint(IPAddress.Parse("10.96.0.10"), 53), ipAddress); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs new file mode 100644 index 00000000000..b87e1362f3d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolveAddressesTests : LoopbackDnsTestBase +{ + public ResolveAddressesTests(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveIPv4_NoData_Success(bool includeSoa) + { + string hostName = "nodata.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + if (includeSoa) + { + builder.Authorities.AddStartOfAuthority("ns.com", 240, "ns.com", "admin.ns.com", 1, 900, 180, 6048000, 3600); + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveIPv4_NoSuchName_Success(bool includeSoa) + { + string hostName = "nosuchname.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.ResponseCode = QueryResponseCode.NameError; + if (includeSoa) + { + builder.Authorities.AddStartOfAuthority("ns.com", 240, "ns.com", "admin.ns.com", 1, 900, 180, 6048000, 3600); + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Theory] + [InlineData("www.resolveipv4.com")] + [InlineData("www.resolveipv4.com.")] + [InlineData("www.ř.com")] + public async Task ResolveIPv4_Simple_Success(string name) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddAddress(name, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_InOrder_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-in-order.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example3.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_OutOfOrder_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-out-of-order.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example3.com", 3600, address); + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_Loop_ReturnsEmpty() + { + string hostName = "alias-loop2.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname("www.example3.com", 3600, hostName); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Aliases_Loop_Reverse_ReturnsEmpty() + { + string hostName = "alias-loop2.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname("www.example3.com", 3600, hostName); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Alias_And_Address() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-address.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example2.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_DuplicateAlias() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "duplicate-alias.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example4.com"); + builder.Answers.AddAddress("www.example2.com", 3600, address); + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Aliases_NotFound_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-no-found.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + + // extra address in the answer not connected to the above + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIP_InvalidAddressFamily_Throws() + { + await Assert.ThrowsAsync(async () => await Resolver.ResolveIPAddressesAsync("invalid-af.test", AddressFamily.Unknown)); + } + + [Theory] + [InlineData(AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData(AddressFamily.InterNetworkV6, "::1")] + public async Task ResolveIP_Localhost_ReturnsLoopback(AddressFamily family, string addressAsString) + { + IPAddress address = IPAddress.Parse(addressAsString); + AddressResult[] results = await Resolver.ResolveIPAddressesAsync("localhost", family); + AddressResult result = Assert.Single(results); + + Assert.Equal(address, result.Address); + } + + [Fact] + public async Task Resolve_Timeout_ReturnsEmpty() + { + Options.Timeout = TimeSpan.FromSeconds(1); + AddressResult[] result = await Resolver.ResolveIPAddressesAsync("timeout-empty.test", AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Theory] + [InlineData("not-example.com", (int)QueryType.A, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.AAAA, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.A, 0)] + public async Task Resolve_QuestionMismatch_ReturnsEmpty(string name, int type, int @class) + { + Options.Timeout = TimeSpan.FromSeconds(1); + + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Questions[0] = (name, (QueryType)type, (QueryClass)@class); + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Fact] + public async Task Resolve_HeaderMismatch_Ignores() + { + string name = "header-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(5); + + SemaphoreSlim responseSemaphore = new SemaphoreSlim(0, 1); + SemaphoreSlim requestSemaphore = new SemaphoreSlim(0, 1); + + IPEndPoint clientAddress = null!; + + IPAddress address = IPAddress.Parse("172.213.245.111"); + ushort transactionId = 0x1234; + _ = DnsServer.ProcessUdpRequest((builder, clientAddr) => + { + clientAddress = clientAddr; + transactionId = (ushort)(builder.TransactionId + 1); + + builder.Answers.AddAddress(name, 3600, address); + requestSemaphore.Release(); + return responseSemaphore.WaitAsync(); + }); + + ValueTask task = Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + + await requestSemaphore.WaitAsync().WaitAsync(Options.Timeout); + + using Socket socket = new Socket(clientAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + LoopbackDnsResponseBuilder responseBuilder = new LoopbackDnsResponseBuilder(name, QueryType.A, QueryClass.Internet) + { + TransactionId = transactionId, + ResponseCode = QueryResponseCode.NoError + }; + + responseBuilder.Questions.Add((name, QueryType.A, QueryClass.Internet)); + responseBuilder.Answers.AddAddress(name, 3600, IPAddress.Loopback); + socket.SendTo(responseBuilder.GetMessageBytes(), clientAddress); + + responseSemaphore.Release(); + + AddressResult[] results = await task; + AddressResult result = Assert.Single(results); + + Assert.Equal(address, result.Address); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs new file mode 100644 index 00000000000..e1cd1df2959 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolveServiceTests : LoopbackDnsTestBase +{ + public ResolveServiceTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task ResolveService_Simple_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddService("_s0._tcp.example.com", 3600, 1, 2, 8080, "www.example.com"); + builder.Additionals.AddAddress("www.example.com", 3600, address); + return Task.CompletedTask; + }); + + ServiceResult[] results = await Resolver.ResolveServiceAsync("_s0._tcp.example.com"); + + ServiceResult result = Assert.Single(results); + Assert.Equal("www.example.com", result.Target); + Assert.Equal(1, result.Priority); + Assert.Equal(2, result.Weight); + Assert.Equal(8080, result.Port); + + AddressResult addressResult = Assert.Single(result.Addresses); + Assert.Equal(address, addressResult.Address); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs new file mode 100644 index 00000000000..800905d1ac5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Sockets; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class RetryTests : LoopbackDnsTestBase +{ + public RetryTests(ITestOutputHelper output) : base(output) + { + Options.Attempts = 3; + } + + private Task SetupUdpProcessFunction(LoopbackDnsServer server, Func func) + { + return Task.Run(async () => + { + try + { + while (true) + { + await server.ProcessUdpRequest(func); + } + } + catch (Exception ex) + { + Output.WriteLine($"UDP server stopped with exception: {ex}"); + // Test teardown closed the socket, ignore + } + }); + } + + private Task SetupUdpProcessFunction(Func func) + { + return SetupUdpProcessFunction(DnsServer, func); + } + + [Fact] + public async Task Retry_Simple_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "retry-simple-success.com"; + + int attempt = 0; + + Task t = SetupUdpProcessFunction(builder => + { + attempt++; + if (attempt == Options.Attempts) + { + builder.Answers.AddAddress(hostName, 3600, address); + } + else + { + builder.ResponseCode = QueryResponseCode.ServerFailure; + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum PersistentErrorType + { + NotImplemented, + Refused, + MalformedResponse + } + + [Theory] + [InlineData(PersistentErrorType.NotImplemented)] + [InlineData(PersistentErrorType.Refused)] + [InlineData(PersistentErrorType.MalformedResponse)] + public async Task PersistentErrorsResponseCode_FailoverToNextServer(PersistentErrorType type) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.persistent.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + switch (type) + { + case PersistentErrorType.NotImplemented: + builder.ResponseCode = QueryResponseCode.NotImplemented; + break; + + case PersistentErrorType.Refused: + builder.ResponseCode = QueryResponseCode.Refused; + break; + + case PersistentErrorType.MalformedResponse: + builder.ResponseCode = (QueryResponseCode)0xFF; + break; + } + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + Assert.Equal(1, primaryAttempt); + Assert.Equal(1, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum DefinitveAnswerType + { + NoError, + NoData, + NameError, + } + + [Theory] + [InlineData(DefinitveAnswerType.NoError, false)] + [InlineData(DefinitveAnswerType.NoData, false)] + [InlineData(DefinitveAnswerType.NoData, true)] + [InlineData(DefinitveAnswerType.NameError, false)] + [InlineData(DefinitveAnswerType.NameError, true)] + public async Task DefinitiveAnswers_NoRetryOrFailover(DefinitveAnswerType type, bool additionalData) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.retry.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + switch (type) + { + case DefinitveAnswerType.NoError: + builder.ResponseCode = QueryResponseCode.NoError; + builder.Answers.AddAddress(hostName, 3600, address); + break; + + case DefinitveAnswerType.NoData: + builder.ResponseCode = QueryResponseCode.NoError; + break; + + case DefinitveAnswerType.NameError: + builder.ResponseCode = QueryResponseCode.NameError; + break; + } + + if (additionalData) + { + builder.Authorities.AddStartOfAuthority(hostName, 300, "ns1.example.com", "hostmaster.example.com", 2023101001, 1, 3600, 300, 86400); + } + + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.ResponseCode = QueryResponseCode.Refused; + return Task.CompletedTask; + }); + + Assert.Equal(1, primaryAttempt); + Assert.Equal(0, secondaryAttempt); + + if (type == DefinitveAnswerType.NoError) + { + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + else + { + Assert.Empty(results); + } + } + + [Fact] + public async Task ExhaustedRetries_FailoverToNextServer() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "ExhaustedRetriesFailoverToNextServer"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + builder.ResponseCode = QueryResponseCode.ServerFailure; + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + Assert.Equal(Options.Attempts, primaryAttempt); + Assert.Equal(1, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum TransientErrorType + { + Timeout, + ServerFailure, + // TODO: simulate NetworkErrors + } + + [Theory] + [InlineData(TransientErrorType.Timeout)] + [InlineData(TransientErrorType.ServerFailure)] + public async Task TransientError_RetryOnSameServer(TransientErrorType type) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.transient.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + async builder => + { + primaryAttempt++; + if (primaryAttempt == 1) + { + switch (type) + { + case TransientErrorType.Timeout: + await Task.Delay(Options.Timeout.Multiply(1.5)); + builder.Answers.AddAddress(hostName, 3600, address); + break; + + case TransientErrorType.ServerFailure: + builder.ResponseCode = QueryResponseCode.ServerFailure; + break; + } + } + else + { + builder.Answers.AddAddress(hostName, 3600, address); + } + }, + builder => + { + secondaryAttempt++; + builder.ResponseCode = QueryResponseCode.Refused; + return Task.CompletedTask; + }); + + Assert.Equal(2, primaryAttempt); + Assert.Equal(0, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + private async Task RunWithFallbackServerHelper(string name, Func primaryHandler, Func fallbackHandler) + { + Task t = SetupUdpProcessFunction(primaryHandler); + using LoopbackDnsServer fallbackServer = new LoopbackDnsServer(); + Task t2 = SetupUdpProcessFunction(fallbackServer, fallbackHandler); + + Options.Servers = [DnsServer.DnsEndPoint, fallbackServer.DnsEndPoint]; + + return await Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + } + + [Fact] + public async Task NameError_NoRetry() + { + int counter = 0; + Task t = SetupUdpProcessFunction(builder => + { + counter++; + // authoritative answer that the name does not exist + builder.ResponseCode = QueryResponseCode.NameError; + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync("nameerror-noretry", AddressFamily.InterNetwork); + + Assert.Empty(results); + Assert.Equal(1, counter); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs new file mode 100644 index 00000000000..40841e3d11a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class TcpFailoverTests : LoopbackDnsTestBase +{ + public TcpFailoverTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TcpFailover_Simple_Success() + { + string hostName = "tcp-simple.test"; + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() + { + string hostName = "tcp-server-closes.test"; + Options.Attempts = 1; + Options.Timeout = TimeSpan.FromSeconds(60); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + Task serverTask = DnsServer.ProcessTcpRequest(builder => + { + throw new InvalidOperationException("This forces closing the socket without writing any data"); + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork).AsTask().WaitAsync(TimeSpan.FromSeconds(10)); + Assert.Empty(results); + + await Assert.ThrowsAsync(() => serverTask); + } + + [Fact] + public async Task TcpFailover_TcpNotAvailable_EmptyResult() + { + string hostName = "tcp-not-available.test"; + Options.Attempts = 1; + Options.Timeout = TimeSpan.FromMilliseconds(100000); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Fact] + public async Task TcpFailover_HeaderMismatch_ReturnsEmpty() + { + string hostName = "tcp-header-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(1); + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.TransactionId++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Theory] + [InlineData("not-example.com", (int)QueryType.A, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.AAAA, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.A, 0)] + public async Task TcpFailover_QuestionMismatch_ReturnsEmpty(string name, int type, int @class) + { + string hostName = "tcp-question-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(1); + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.Questions[0] = (name, (QueryType)type, (QueryClass)@class); + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 96fd46d47ea..c2751823c65 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -6,10 +6,11 @@ using Xunit; using Yarp.ReverseProxy.Configuration; using System.Net; -using DnsClient; -using DnsClient.Protocol; +using System.Net.Sockets; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; @@ -231,7 +232,10 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Dns() { + DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance); + await using var services = new ServiceCollection() + .AddSingleton(resolver) .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); @@ -265,32 +269,22 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() [Fact] public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), - } - }; - - return Task.FromResult(response); + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Loopback)]) + ]; + + return ValueTask.FromResult(response); } }; await using var services = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); @@ -313,56 +307,17 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() a => Assert.Equal("https://127.0.0.1:7777/", a)); } - private sealed class FakeDnsClient : IDnsQuery + private sealed class FakeDnsResolver : IDnsResolver { - public Func>? QueryAsyncFunc { get; set; } - - public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) - => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); - public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - } + public Func>? ResolveIPAddressesAsyncFunc { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc!.Invoke(name, addressFamily, cancellationToken); - private sealed class FakeDnsQueryResponse : IDnsQueryResponse - { - public IReadOnlyList? Questions { get; set; } - public IReadOnlyList? Additionals { get; set; } - public IEnumerable? AllRecords { get; set; } - public IReadOnlyList? Answers { get; set; } - public IReadOnlyList? Authorities { get; set; } - public string? AuditTrail { get; set; } - public string? ErrorMessage { get; set; } - public bool HasError { get; set; } - public DnsResponseHeader? Header { get; set; } - public int MessageSize { get; set; } - public NameServer? NameServer { get; set; } - public DnsQuerySettings? Settings { get; set; } + public Func>? ResolveIPAddressesAsyncFunc2 { get; set; } + + public ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc2!.Invoke(name, cancellationToken); + + public Func>? ResolveServiceAsyncFunc { get; set; } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) => ResolveServiceAsyncFunc!.Invoke(name, cancellationToken); } } From ae446fbb8659307f34b1ecb6f4e943471e98d79c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 25 Jun 2025 12:01:35 -0400 Subject: [PATCH 153/472] Augment AIJsonUtilities.CreateJsonSchema for more types and annotations (#6540) * Augment AIJsonUtilities.CreateJsonSchema for more types and annotations * Stop suppressing existing format handling, and allow most annotations on netfx --- ...icrosoft.Extensions.AI.Abstractions.csproj | 1 + .../AIJsonUtilities.Schema.Create.cs | 277 ++++++++- .../OpenAIClientExtensions.cs | 69 ++- ...ft.Extensions.AI.Abstractions.Tests.csproj | 1 + .../Utilities/AIJsonUtilitiesTests.cs | 528 ++++++++++++++++-- .../ChatClientIntegrationTests.cs | 43 +- 6 files changed, 866 insertions(+), 53 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index 4e472e78473..f5472854def 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index e182d4149bb..a9d3ac3e3ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -3,6 +3,9 @@ using System; using System.ComponentModel; +#if NET || NETFRAMEWORK +using System.ComponentModel.DataAnnotations; +#endif using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -14,11 +17,12 @@ using System.Threading; 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 S109 // Magic numbers should not be used #pragma warning disable S1075 // URIs should not be hardcoded +#pragma warning disable S1121 // Assignments should not be made from within sub-expressions +#pragma warning disable S1199 // Nested block #pragma warning disable SA1118 // Parameter should not span multiple lines -#pragma warning disable S109 // Magic numbers should not be used namespace Microsoft.Extensions.AI; @@ -38,14 +42,25 @@ public static partial class AIJsonUtilities private const string AdditionalPropertiesPropertyName = "additionalProperties"; private const string DefaultPropertyName = "default"; private const string RefPropertyName = "$ref"; +#if NET || NETFRAMEWORK + private const string FormatPropertyName = "format"; + private const string MinLengthStringPropertyName = "minLength"; + private const string MaxLengthStringPropertyName = "maxLength"; + private const string MinLengthCollectionPropertyName = "minItems"; + private const string MaxLengthCollectionPropertyName = "maxItems"; + private const string MinRangePropertyName = "minimum"; + private const string MaxRangePropertyName = "maximum"; +#endif +#if NET + private const string ContentEncodingPropertyName = "contentEncoding"; + private const string ContentMediaTypePropertyName = "contentMediaType"; + private const string MinExclusiveRangePropertyName = "exclusiveMinimum"; + private const string MaxExclusiveRangePropertyName = "exclusiveMaximum"; +#endif /// The uri used when populating the $schema keyword in created schemas. private const string SchemaKeywordUri = "https://json-schema.org/draft/2020-12/schema"; - // List of keywords used by JsonSchemaExporter but explicitly disallowed by some AI vendors. - // cf. https://platform.openai.com/docs/guides/structured-outputs#some-type-specific-keywords-are-not-yet-supported - private static readonly string[] _schemaKeywordsDisallowedByAIVendors = ["minLength", "maxLength", "pattern", "format"]; - /// /// Determines a JSON schema for the provided method. /// @@ -280,12 +295,6 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" }); } - // Filter potentially disallowed keywords. - foreach (string keyword in _schemaKeywordsDisallowedByAIVendors) - { - _ = objSchema.Remove(keyword); - } - // 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. // In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error. @@ -318,6 +327,8 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js ConvertSchemaToObject(ref schema).InsertAtStart(SchemaPropertyName, (JsonNode)SchemaKeywordUri); } + ApplyDataAnnotations(parameterName, ref schema, ctx); + // Finally, apply any user-defined transformations if specified. if (inferenceOptions.TransformSchemaNode is { } transformer) { @@ -345,6 +356,248 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema) return obj; } } + + void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSchemaCreateContext ctx) + { + if (ctx.GetCustomAttribute() is { } displayNameAttribute) + { + ConvertSchemaToObject(ref schema)[TitlePropertyName] ??= displayNameAttribute.DisplayName; + } + +#if NET || NETFRAMEWORK + if (ctx.GetCustomAttribute() is { } emailAttribute) + { + ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "email"; + } + + if (ctx.GetCustomAttribute() is { } urlAttribute) + { + ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "uri"; + } + + if (ctx.GetCustomAttribute() is { } regexAttribute) + { + ConvertSchemaToObject(ref schema)[PatternPropertyName] ??= regexAttribute.Pattern; + } + + if (ctx.GetCustomAttribute() is { } stringLengthAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + + if (stringLengthAttribute.MinimumLength > 0) + { + obj[MinLengthStringPropertyName] ??= stringLengthAttribute.MinimumLength; + } + + obj[MaxLengthStringPropertyName] ??= stringLengthAttribute.MaximumLength; + } + + if (ctx.GetCustomAttribute() is { } minLengthAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + { + obj[MinLengthStringPropertyName] ??= minLengthAttribute.Length; + } + else + { + obj[MinLengthCollectionPropertyName] ??= minLengthAttribute.Length; + } + } + + if (ctx.GetCustomAttribute() is { } maxLengthAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + { + obj[MaxLengthStringPropertyName] ??= maxLengthAttribute.Length; + } + else + { + obj[MaxLengthCollectionPropertyName] ??= maxLengthAttribute.Length; + } + } + + if (ctx.GetCustomAttribute() is { } rangeAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + + JsonNode? minNode = null; + JsonNode? maxNode = null; + switch (rangeAttribute.Minimum) + { + case int minInt32 when rangeAttribute.Maximum is int maxInt32: + maxNode = maxInt32; + if ( +#if NET + !rangeAttribute.MinimumIsExclusive || +#endif + minInt32 > 0) + { + minNode = minInt32; + } + + break; + + case double minDouble when rangeAttribute.Maximum is double maxDouble: + maxNode = maxDouble; + if ( +#if NET + !rangeAttribute.MinimumIsExclusive || +#endif + minDouble > 0) + { + minNode = minDouble; + } + + break; + + case string minString when rangeAttribute.Maximum is string maxString: + maxNode = maxString; + minNode = minString; + break; + } + + if (minNode is not null) + { +#if NET + if (rangeAttribute.MinimumIsExclusive) + { + obj[MinExclusiveRangePropertyName] ??= minNode; + } + else +#endif + { + obj[MinRangePropertyName] ??= minNode; + } + } + + if (maxNode is not null) + { +#if NET + if (rangeAttribute.MaximumIsExclusive) + { + obj[MaxExclusiveRangePropertyName] ??= maxNode; + } + else +#endif + { + obj[MaxRangePropertyName] ??= maxNode; + } + } + } +#endif + +#if NET + if (ctx.GetCustomAttribute() is { } base64Attribute) + { + ConvertSchemaToObject(ref schema)[ContentEncodingPropertyName] ??= "base64"; + } + + if (ctx.GetCustomAttribute() is { } lengthAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + + if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + { + if (lengthAttribute.MinimumLength > 0) + { + obj[MinLengthStringPropertyName] ??= lengthAttribute.MinimumLength; + } + + obj[MaxLengthStringPropertyName] ??= lengthAttribute.MaximumLength; + } + else + { + if (lengthAttribute.MinimumLength > 0) + { + obj[MinLengthCollectionPropertyName] ??= lengthAttribute.MinimumLength; + } + + obj[MaxLengthCollectionPropertyName] ??= lengthAttribute.MaximumLength; + } + } + + if (ctx.GetCustomAttribute() is { } allowedValuesAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + if (!obj.ContainsKey(EnumPropertyName)) + { + if (CreateJsonArray(allowedValuesAttribute.Values, serializerOptions) is { Count: > 0 } enumArray) + { + obj[EnumPropertyName] = enumArray; + } + } + } + + if (ctx.GetCustomAttribute() is { } deniedValuesAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + + JsonNode? notNode = obj[NotPropertyName]; + if (notNode is null or JsonObject) + { + JsonObject notObj = + notNode as JsonObject ?? + (JsonObject)(obj[NotPropertyName] = new JsonObject()); + + if (notObj[EnumPropertyName] is null) + { + if (CreateJsonArray(deniedValuesAttribute.Values, serializerOptions) is { Count: > 0 } enumArray) + { + notObj[EnumPropertyName] = enumArray; + } + } + } + } + + static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions serializerOptions) + { + JsonArray enumArray = new(); + foreach (object? allowedValue in values) + { + if (allowedValue is not null && JsonSerializer.SerializeToNode(allowedValue, serializerOptions.GetTypeInfo(allowedValue.GetType())) is { } valueNode) + { + enumArray.Add(valueNode); + } + } + + return enumArray; + } + + if (ctx.GetCustomAttribute() is { } dataTypeAttribute) + { + JsonObject obj = ConvertSchemaToObject(ref schema); + switch (dataTypeAttribute.DataType) + { + case DataType.DateTime: + obj[FormatPropertyName] ??= "date-time"; + break; + + case DataType.Date: + obj[FormatPropertyName] ??= "date"; + break; + + case DataType.Time: + obj[FormatPropertyName] ??= "time"; + break; + + case DataType.EmailAddress: + obj[FormatPropertyName] ??= "email"; + break; + + case DataType.Url: + obj[FormatPropertyName] ??= "uri"; + break; + + case DataType.ImageUrl: + obj[FormatPropertyName] ??= "uri"; + obj[ContentMediaTypePropertyName] ??= "image/*"; + break; + } + } +#endif + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 19cd54306fe..3b55280f5cd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -3,7 +3,9 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using OpenAI; using OpenAI.Assistants; using OpenAI.Audio; @@ -11,6 +13,11 @@ using OpenAI.Embeddings; using OpenAI.Responses; +#pragma warning disable S103 // Lines should not be too long +#pragma warning disable S1067 // Expressions should not be too complex +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line +#pragma warning disable CA1305 // Specify IFormatProvider + namespace Microsoft.Extensions.AI; /// Provides extension methods for working with s. @@ -26,7 +33,8 @@ public static class OpenAIClientExtensions internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer"); /// - /// Gets the JSON schema transformer cache conforming to OpenAI strict restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. + /// Gets the JSON schema transformer cache conforming to OpenAI strict / structured output restrictions per + /// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. /// internal static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new() { @@ -34,6 +42,65 @@ public static class OpenAIClientExtensions ConvertBooleanSchemas = true, MoveDefaultKeywordToDescription = true, RequireAllProperties = true, + TransformSchemaNode = (ctx, node) => + { + // Move content from common but unsupported properties to description. In particular, we focus on properties that + // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation. + + if (node is JsonObject schemaObj) + { + StringBuilder? additionalDescription = null; + + ReadOnlySpan unsupportedProperties = + [ + // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties: + "contentEncoding", "contentMediaType", "not", + + // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models: + "minLength", "maxLength", "pattern", "format", + "minimum", "maximum", "multipleOf", + "patternProperties", + "minItems", "maxItems", + + // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords + // as being unsupported with Azure OpenAI: + "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties", + "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems", + ]; + + foreach (string propName in unsupportedProperties) + { + if (schemaObj[propName] is { } propNode) + { + _ = schemaObj.Remove(propName); + AppendLine(ref additionalDescription, propName, propNode); + } + } + + if (additionalDescription is not null) + { + schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ? + $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" : + additionalDescription.ToString(); + } + + return node; + + static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode) + { + sb ??= new(); + + if (sb.Length > 0) + { + _ = sb.AppendLine(); + } + + _ = sb.Append(propName).Append(": ").Append(propNode); + } + } + + return node; + }, }); /// Gets an for use with this . diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj index 0e608d0d953..d2ae2802123 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj @@ -38,5 +38,6 @@ + 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 78db93e9380..19b2fc8bb48 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -13,8 +15,11 @@ using System.Text.Json.Serialization.Metadata; using System.Threading; using Microsoft.Extensions.AI.JsonSchemaExporter; +using Microsoft.TestUtilities; using Xunit; +#pragma warning disable SA1114 // parameter list should follow declaration + namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilitiesTests @@ -258,44 +263,6 @@ public static void CreateJsonSchema_UserDefinedTransformer() AssertDeepEquals(expected, actual); } - [Fact] - public static void CreateJsonSchema_FiltersDisallowedKeywords() - { - JsonElement expected = JsonDocument.Parse(""" - { - "type": "object", - "properties": { - "Date": { - "type": "string" - }, - "TimeSpan": { - "$comment": "Represents a System.TimeSpan value.", - "type": "string" - }, - "Char" : { - "type": "string" - } - } - } - """).RootElement; - - JsonElement actual = AIJsonUtilities.CreateJsonSchema(typeof(PocoWithTypesWithOpenAIUnsupportedKeywords), serializerOptions: JsonContext.Default.Options); - - AssertDeepEquals(expected, actual); - } - - public class PocoWithTypesWithOpenAIUnsupportedKeywords - { - // Uses the unsupported "format" keyword - public DateTimeOffset Date { get; init; } - - // Uses the unsupported "pattern" keyword - public TimeSpan TimeSpan { get; init; } - - // Uses the unsupported "minLength" and "maxLength" keywords - public char Char { get; init; } - } - [Fact] public static void CreateFunctionJsonSchema_ReturnsExpectedValue() { @@ -438,6 +405,7 @@ public static void CreateJsonSchema_ValidateWithTestData(ITestData testData) JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions); JsonNode? schemaAsNode = JsonSerializer.SerializeToNode(schema, options); + // NOTE: This is not validating the schemas match, only that they have the same top-level kind. Assert.NotNull(schemaAsNode); Assert.Equal(testData.ExpectedJsonSchema.GetValueKind(), schemaAsNode.GetValueKind()); @@ -480,6 +448,487 @@ public static void CreateJsonSchema_NullableEnum_IncludesTypeKeyword() AssertDeepEquals(expectedSchema, schema); } + [ConditionalFact] + public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_Net() + { + if (RuntimeInformation.FrameworkDescription.Contains(".NET Framework")) + { + return; + } + + AssertDeepEquals(JsonSerializer.Deserialize( + """ + { + "type": "object", + "properties": { + "DisplayNameProp": { + "type": [ + "string", + "null" + ], + "title": "Display Name Title" + }, + "DateTimeProp": { + "type": "string", + "format": "date-time" + }, + "DateTimeOffsetProp": { + "type": "string", + "format": "date-time" + }, + "TimeSpanProp": { + "$comment": "Represents a System.TimeSpan value.", + "type": "string", + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$" + }, + "GuidProp": { + "type": "string", + "format": "uuid" + }, + "UriProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "Base64Prop": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "RegexProp": { + "type": [ + "string", + "null" + ], + "pattern": "[abc]|[def]" + }, + "EmailProp": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "UrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "RangeProp": { + "type": "integer", + "minimum": 12, + "maximum": 34 + }, + "AllowedStringValuesProp": { + "type": [ + "string", + "null" + ], + "enum": [ + "abc", + "def", + "ghi" + ] + }, + "AllowedInt32ValuesProp": { + "type": "integer", + "enum": [ + 1, + 3, + 5 + ] + }, + "AllowedDoubleValuesProp": { + "type": "number", + "enum": [ + 1.2, + 3.4 + ] + }, + "DeniedValuesProp": { + "type": [ + "string", + "null" + ], + "not": { + "enum": [ + "jkl", + "mnop" + ] + } + }, + "DataTypeDateTimeProp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "DataTypeDateProp": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "DataTypeTimeProp": { + "type": [ + "string", + "null" + ], + "format": "time" + }, + "DataTypeEmailProp": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "DataTypeUrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "DataTypeImageUrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri", + "contentMediaType": "image/*" + }, + "DateOnlyProp": { + "type": "string", + "format": "date" + }, + "TimeOnlyProp": { + "type": "string", + "format": "time" + }, + "StringLengthProp": { + "type": [ + "string", + "null" + ], + "minLength": 10, + "maxLength": 100 + }, + "MinLengthProp": { + "type": [ + "string", + "null" + ], + "minItems": 5 + }, + "MaxLengthProp": { + "type": [ + "string", + "null" + ], + "maxItems": 50 + }, + "LengthProp": { + "type": [ + "string", + "null" + ], + "minItems": 3, + "maxItems": 10 + }, + "MinLengthArrayProp": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + }, + "minItems": 2 + }, + "MaxLengthArrayProp": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + }, + "maxItems": 8 + }, + "LengthArrayProp": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + }, + "minItems": 1, + "maxItems": 4 + } + } + } + """, + JsonContext.Default.JsonElement), + AIJsonUtilities.CreateJsonSchema(typeof(CreateJsonSchema_IncorporatesTypesAndAnnotations_Type), serializerOptions: JsonContext.Default.Options)); + } + + // .NET Framework only has a subset of the available data annotation attributes. + // .NET Standard doesn't have any (the M.E.AI.Abstractions library doesn't reference the additional package). + [ConditionalFact] + public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_NetFx() + { + if (!RuntimeInformation.FrameworkDescription.Contains(".NET Framework")) + { + return; + } + + AssertDeepEquals(JsonSerializer.Deserialize( + """ + { + "type": "object", + "properties": { + "DisplayNameProp": { + "type": [ + "string", + "null" + ], + "title": "Display Name Title" + }, + "DateTimeProp": { + "type": "string", + "format": "date-time" + }, + "DateTimeOffsetProp": { + "type": "string", + "format": "date-time" + }, + "TimeSpanProp": { + "$comment": "Represents a System.TimeSpan value.", + "type": "string", + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$" + }, + "GuidProp": { + "type": "string", + "format": "uuid" + }, + "UriProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "RegexProp": { + "type": [ + "string", + "null" + ], + "pattern": "[abc]|[def]" + }, + "EmailProp": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "UrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "RangeProp": { + "type": "integer", + "minimum": 12, + "maximum": 34 + }, + "DataTypeDateTimeProp": { + "type": [ + "string", + "null" + ] + }, + "DataTypeDateProp": { + "type": [ + "string", + "null" + ] + }, + "DataTypeTimeProp": { + "type": [ + "string", + "null" + ] + }, + "DataTypeEmailProp": { + "type": [ + "string", + "null" + ] + }, + "DataTypeUrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "DataTypeImageUrlProp": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "StringLengthProp": { + "type": [ + "string", + "null" + ], + "minLength": 10, + "maxLength": 100 + }, + "MinLengthProp": { + "type": [ + "string", + "null" + ], + "minItems": 5 + }, + "MaxLengthProp": { + "type": [ + "string", + "null" + ], + "maxItems": 50 + }, + "MinLengthArrayProp": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + }, + "minItems": 2 + }, + "MaxLengthArrayProp": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + }, + "maxItems": 8 + } + } + } + """, + JsonContext.Default.JsonElement), + AIJsonUtilities.CreateJsonSchema(typeof(CreateJsonSchema_IncorporatesTypesAndAnnotations_Type), serializerOptions: JsonContext.Default.Options)); + } + + private sealed class CreateJsonSchema_IncorporatesTypesAndAnnotations_Type + { + [DisplayName("Display Name Title")] + public string? DisplayNameProp { get; set; } + + public DateTime DateTimeProp { get; set; } + + public DateTimeOffset DateTimeOffsetProp { get; set; } + + public TimeSpan TimeSpanProp { get; set; } + + public Guid GuidProp { get; set; } + + public Uri? UriProp { get; set; } + + [RegularExpression("[abc]|[def]")] + public string? RegexProp { get; set; } + + [EmailAddress] + public string? EmailProp { get; set; } + + [Url] + public Uri? UrlProp { get; set; } + + [Range(12, 34)] + public int RangeProp { get; set; } + + [DataType(DataType.DateTime)] + public string? DataTypeDateTimeProp { get; set; } + + [DataType(DataType.Date)] + public string? DataTypeDateProp { get; set; } + + [DataType(DataType.Time)] + public string? DataTypeTimeProp { get; set; } + + [DataType(DataType.EmailAddress)] + public string? DataTypeEmailProp { get; set; } + + [DataType(DataType.Url)] + public Uri? DataTypeUrlProp { get; set; } + + [DataType(DataType.ImageUrl)] + public Uri? DataTypeImageUrlProp { get; set; } + + [StringLength(100, MinimumLength = 10)] + public string? StringLengthProp { get; set; } + + [MinLength(5)] + public string? MinLengthProp { get; set; } + + [MaxLength(50)] + public string? MaxLengthProp { get; set; } + + [MinLength(2)] + public int[]? MinLengthArrayProp { get; set; } + + [MaxLength(8)] + public int[]? MaxLengthArrayProp { get; set; } + +#if NET + [Base64String] + public string? Base64Prop { get; set; } + + [AllowedValues("abc", "def", "ghi")] + public string? AllowedStringValuesProp { get; set; } + + [AllowedValues(1, 3, 5)] + public int AllowedInt32ValuesProp { get; set; } + + [AllowedValues(1.2, 3.4)] + public double AllowedDoubleValuesProp { get; set; } + + [DeniedValues("jkl", "mnop")] + public string? DeniedValuesProp { get; set; } + + public DateOnly DateOnlyProp { get; set; } + + public TimeOnly TimeOnlyProp { get; set; } + + [Length(3, 10)] + public string? LengthProp { get; set; } + + [Length(1, 4)] + public int[]? LengthArrayProp { get; set; } +#endif + } + [Fact] public static void AddAIContentType_DerivedAIContent() { @@ -863,9 +1312,10 @@ private class DerivedAIContent : AIContent public int DerivedValue { get; set; } } + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(CreateJsonSchema_IncorporatesTypesAndAnnotations_Type))] [JsonSerializable(typeof(DerivedAIContent))] [JsonSerializable(typeof(MyPoco))] - [JsonSerializable(typeof(PocoWithTypesWithOpenAIUnsupportedKeywords))] [JsonSerializable(typeof(MyEnumValue?))] private partial class JsonContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index ab46a0e0c58..d84d767fd4c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -423,7 +424,13 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict) AIFunctionFactory.Create((long l) => l, createOptions()), AIFunctionFactory.Create((char c) => c, createOptions()), AIFunctionFactory.Create((DateTime dt) => dt, createOptions()), - AIFunctionFactory.Create((DateTime? dt) => dt, createOptions()), + AIFunctionFactory.Create((DateTimeOffset? dt) => dt, createOptions()), + AIFunctionFactory.Create((TimeSpan ts) => ts, createOptions()), +#if NET + AIFunctionFactory.Create((DateOnly d) => d, createOptions()), + AIFunctionFactory.Create((TimeOnly t) => t, createOptions()), +#endif + AIFunctionFactory.Create((Uri uri) => uri, createOptions()), AIFunctionFactory.Create((Guid guid) => guid, createOptions()), AIFunctionFactory.Create((List list) => list, createOptions()), AIFunctionFactory.Create((int[] arr, ComplexObject? co) => arr, createOptions()), @@ -475,11 +482,45 @@ private sealed class CustomAIFunction(string name, string jsonSchema, IReadOnlyD private class ComplexObject { + [DisplayName("Something cool")] +#if NET + [DeniedValues("abc", "def", "default")] +#endif public string? SomeString { get; set; } +#if NET + [AllowedValues("abc", "def", "default")] +#endif public string AnotherString { get; set; } = "default"; +#if NET + [Range(25, 75)] +#endif public int Value { get; set; } + + [EmailAddress] + public string? Email { get; set; } + + [RegularExpression("[abc]")] + public string? RegexString { get; set; } + + [StringLength(42)] + public string MeasuredString { get; set; } = "default"; + +#if NET + [Length(1, 2)] +#endif + public int[]? MeasuredArray1 { get; set; } + +#if NET + [MinLength(1)] +#endif + public int[]? MeasuredArray2 { get; set; } + +#if NET + [MaxLength(10)] +#endif + public int[]? MeasuredArray3 { get; set; } } protected virtual bool SupportsParallelFunctionCalling => true; From f3141737a2c1b7557a1b7bc07073ac72cafc6a93 Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Wed, 25 Jun 2025 20:19:25 +0200 Subject: [PATCH 154/472] Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2737112 (#10028) * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736879 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736911 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736942 From e69d3e959725be4f64faa5a67e815623d6b97b38 Mon Sep 17 00:00:00 2001 From: Krzysztof Cwalina Date: Wed, 25 Jun 2025 21:33:57 -0700 Subject: [PATCH 155/472] Added conversions from AIFunction to various OpenAI tools (#6539) * added conversions from AIFunction to various OpenAI tools * Remove duplication of tool handling across OpenAI impls --------- Co-authored-by: Stephen Toub --- .../OpenAIAssistantChatClient.cs | 42 ++++----- .../OpenAIChatClient.cs | 61 +++---------- .../OpenAIClientExtensions.cs | 86 +++++++++++++++++-- .../OpenAIJsonContext.cs | 19 ++++ .../OpenAIRealtimeConversationClient.cs | 22 +++++ .../OpenAIResponseChatClient.cs | 57 +++--------- .../OpenAIAIFunctionConversionTests.cs | 79 +++++++++++++++++ 7 files changed, 244 insertions(+), 122 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index 7a64a86721d..3547483da10 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -9,7 +9,6 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -30,7 +29,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an Azure.AI.Agents.Persistent . [Experimental("OPENAI001")] -internal sealed partial class OpenAIAssistantChatClient : IChatClient +internal sealed class OpenAIAssistantChatClient : IChatClient { /// The underlying . private readonly AssistantClient _client; @@ -197,9 +196,9 @@ public async IAsyncEnumerable GetStreamingResponseAsync( { ruUpdate.Contents.Add( new FunctionCallContent( - JsonSerializer.Serialize([ru.Value.Id, toolCallId], AssistantJsonContext.Default.StringArray), + JsonSerializer.Serialize([ru.Value.Id, toolCallId], OpenAIJsonContext.Default.StringArray), functionName, - JsonSerializer.Deserialize(rau.FunctionArguments, AssistantJsonContext.Default.IDictionaryStringObject)!)); + JsonSerializer.Deserialize(rau.FunctionArguments, OpenAIJsonContext.Default.IDictionaryStringObject)!)); } yield return ruUpdate; @@ -237,6 +236,19 @@ void IDisposable.Dispose() // nop } + /// Converts an Extensions function to an OpenAI assistants function tool. + internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction) + { + (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + + return new FunctionToolDefinition(aiFunction.Name) + { + Description = aiFunction.Description, + Parameters = parameters, + StrictParameterSchemaEnabled = strict, + }; + } + /// /// Creates the to use for the request and extracts any function result contents /// that need to be submitted as tool results. @@ -284,18 +296,7 @@ void IDisposable.Dispose() switch (tool) { case AIFunction aiFunction: - bool? strict = aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out var strictValue) && strictValue is bool strictBool ? - strictBool : - null; - - JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); - - runOptions.ToolsOverride.Add(new FunctionToolDefinition(aiFunction.Name) - { - Description = aiFunction.Description, - Parameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AssistantJsonContext.Default.JsonElement)), - StrictParameterSchemaEnabled = strict, - }); + runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); break; case HostedCodeInterpreterTool: @@ -340,7 +341,7 @@ void IDisposable.Dispose() case ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema: runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName, - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AssistantJsonContext.Default.JsonElement)), + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), jsonFormat.SchemaDescription); break; @@ -453,7 +454,7 @@ void AppendSystemInstructions(string? toAppend) string[]? runAndCallIDs; try { - runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AssistantJsonContext.Default.StringArray); + runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, OpenAIJsonContext.Default.StringArray); } catch { @@ -476,9 +477,4 @@ void AppendSystemInstructions(string? toAppend) return runId; } - - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(string[]))] - [JsonSerializable(typeof(IDictionary))] - private sealed partial class AssistantJsonContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 40526b708c0..abbcb0ed0ae 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -23,7 +22,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . -internal sealed partial class OpenAIChatClient : IChatClient +internal sealed class OpenAIChatClient : IChatClient { /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -101,6 +100,14 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } + /// Converts an Extensions function to an OpenAI chat tool. + internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction) + { + (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + + return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict); + } + /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, ChatOptions? chatOptions, JsonSerializerOptions jsonOptions) { @@ -547,8 +554,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) result.ResponseFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", - BinaryData.FromBytes( - JsonSerializer.SerializeToUtf8Bytes(jsonSchema, ChatClientJsonContext.Default.JsonElement)), + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), jsonFormat.SchemaDescription) : OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(); } @@ -557,23 +563,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) return result; } - /// Converts an Extensions function to an OpenAI chat tool. - private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) - { - bool? strict = - aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && - strictObj is bool strictValue ? - strictValue : null; - - // Perform transformations making the schema legal per OpenAI restrictions - JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); - - // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(jsonSchema, ChatClientJsonContext.Default.ChatToolJson)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, ChatClientJsonContext.Default.ChatToolJson)); - return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict); - } - private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage) { var destination = new UsageDetails @@ -668,27 +657,11 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) => FunctionCallContent.CreateFromParsedArguments(json, callId, name, - argumentParser: static json => JsonSerializer.Deserialize(json, ChatClientJsonContext.Default.IDictionaryStringObject)!); + argumentParser: static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8Json, string callId, string name) => FunctionCallContent.CreateFromParsedArguments(ut8Json, callId, name, - argumentParser: static json => JsonSerializer.Deserialize(json, ChatClientJsonContext.Default.IDictionaryStringObject)!); - - /// Used to create the JSON payload for an OpenAI chat tool description. - private sealed class ChatToolJson - { - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public HashSet Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; - - [JsonPropertyName("additionalProperties")] - public bool AdditionalProperties { get; set; } - } + argumentParser: static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); /// POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates. private sealed class FunctionCallInfo @@ -697,14 +670,4 @@ private sealed class FunctionCallInfo public string? Name; public StringBuilder? Arguments; } - - /// Source-generated JSON type information. - [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, - UseStringEnumConverter = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = true)] - [JsonSerializable(typeof(ChatToolJson))] - [JsonSerializable(typeof(IDictionary))] - [JsonSerializable(typeof(string[]))] - private sealed partial class ChatClientJsonContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 3b55280f5cd..24fd93ccb65 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -2,15 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Assistants; using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; +using OpenAI.RealtimeConversation; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long @@ -24,7 +28,7 @@ namespace Microsoft.Extensions.AI; public static class OpenAIClientExtensions { /// Key into AdditionalProperties used to store a strict option. - internal const string StrictKey = "strictJsonSchema"; + private const string StrictKey = "strictJsonSchema"; /// Gets the default OpenAI endpoint. internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1"); @@ -106,12 +110,14 @@ static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode /// Gets an for use with this . /// The client. /// An that can be used to converse via the . + /// is . public static IChatClient AsIChatClient(this ChatClient chatClient) => new OpenAIChatClient(chatClient); /// Gets an for use with this . /// The client. /// An that can be used to converse via the . + /// is . public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => new OpenAIResponseChatClient(responseClient); @@ -124,13 +130,17 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request. /// /// An instance configured to interact with the specified agent and thread. - [Experimental("OPENAI001")] + /// is . + /// is . + /// is empty or composed entirely of whitespace. + [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); /// Gets an for use with this . /// The client. /// An that can be used to transcribe audio via the . + /// is . [Experimental("MEAI001")] public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) => new OpenAISpeechToTextClient(audioClient); @@ -139,12 +149,74 @@ public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioCl /// The client. /// The number of dimensions to generate in each embedding. /// An that can be used to generate embeddings via the . + /// is . public static IEmbeddingGenerator> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) => new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions); - /// Gets the JSON schema to use from the function. - internal static JsonElement GetSchema(AIFunction function, bool? strict) => - strict is true ? - StrictSchemaTransformCache.GetOrCreateTransformedSchema(function) : - function.JsonSchema; + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ChatTool AsOpenAIChatTool(this AIFunction function) => + OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function)); + + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID + public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => + OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); + + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => + OpenAIResponseChatClient.ToResponseTool(Throw.IfNull(function)); + + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => + OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); + + /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. + internal static (BinaryData Parameters, bool? Strict) ToOpenAIFunctionParameters(AIFunction aiFunction) + { + // Extract any strict setting from AdditionalProperties. + bool? strict = + aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && + strictObj is bool strictValue ? + strictValue : null; + + // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting. + JsonElement jsonSchema = strict is true ? + StrictSchemaTransformCache.GetOrCreateTransformedSchema(aiFunction) : + aiFunction.JsonSchema; + + // Roundtrip the schema through the ToolJson model type to remove extra properties + // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData. + var tool = JsonSerializer.Deserialize(jsonSchema, OpenAIJsonContext.Default.ToolJson)!; + var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson)); + + return (functionParameters, strict); + } + + /// Used to create the JSON payload for an OpenAI tool description. + internal sealed class ToolJson + { + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("required")] + public HashSet Required { get; set; } = []; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + + [JsonPropertyName("additionalProperties")] + public bool AdditionalProperties { get; set; } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs new file mode 100644 index 00000000000..00b0089ddf7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// Source-generated JSON type information for use by all OpenAI implementations. +[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] +[JsonSerializable(typeof(OpenAIClientExtensions.ToolJson))] +[JsonSerializable(typeof(IDictionary))] +[JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(JsonElement))] +internal sealed partial class OpenAIJsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs new file mode 100644 index 00000000000..892a9e9aa2a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -0,0 +1,22 @@ +// 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 OpenAI.RealtimeConversation; + +namespace Microsoft.Extensions.AI; + +/// Provides helpers for interacting with OpenAI Realtime. +internal sealed class OpenAIRealtimeConversationClient +{ + public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction) + { + (BinaryData parameters, _) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + + return new ConversationFunctionTool(aiFunction.Name) + { + Description = aiFunction.Description, + Parameters = parameters, + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index e722c6c0c9c..a5f68e10365 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -23,7 +22,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an . -internal sealed partial class OpenAIResponseChatClient : IChatClient +internal sealed class OpenAIResponseChatClient : IChatClient { /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -124,7 +123,7 @@ public async Task GetResponseAsync( functionCall.FunctionArguments.ToMemory(), functionCall.CallId, functionCall.FunctionName, - static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!); + static json => JsonSerializer.Deserialize(json.Span, OpenAIJsonContext.Default.IDictionaryStringObject)!); fcc.RawRepresentation = outputItem; message.Contents.Add(fcc); break; @@ -247,7 +246,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( callInfo.Arguments?.ToString() ?? string.Empty, callInfo.ResponseItem.CallId, callInfo.ResponseItem.FunctionName, - static json => JsonSerializer.Deserialize(json, ResponseClientJsonContext.Default.IDictionaryStringObject)!); + static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); lastMessageId = callInfo.ResponseItem.Id; lastRole = ChatRole.Assistant; @@ -324,6 +323,13 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } + internal static ResponseTool ToResponseTool(AIFunction aiFunction) + { + (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + + return ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict ?? false); + } + /// Creates a from a . private static ChatRole ToChatRole(MessageRole? role) => role switch @@ -374,16 +380,8 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt switch (tool) { case AIFunction aiFunction: - bool strict = - aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && - strictObj is bool strictValue && - strictValue; - - JsonElement jsonSchema = OpenAIClientExtensions.GetSchema(aiFunction, strict); - - var oaitool = JsonSerializer.Deserialize(jsonSchema, ResponseClientJsonContext.Default.ResponseToolJson)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(oaitool, ResponseClientJsonContext.Default.ResponseToolJson)); - result.Tools.Add(ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict)); + ResponseTool rtool = ToResponseTool(aiFunction); + result.Tools.Add(rtool); break; case HostedWebSearchTool: @@ -443,7 +441,7 @@ strictObj is bool strictValue && TextFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? ResponseTextFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, ResponseClientJsonContext.Default.JsonElement)), + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), jsonFormat.SchemaDescription) : ResponseTextFormat.CreateJsonObjectFormat(), }; @@ -620,7 +618,7 @@ private static List ToOpenAIResponsesContent(IList ToOpenAIResponsesContent(IListUsed to create the JSON payload for an OpenAI chat tool description. - private sealed class ResponseToolJson - { - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public HashSet Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; - - [JsonPropertyName("additionalProperties")] - public bool AdditionalProperties { get; set; } - } - /// POCO representing function calling info. /// Used to concatenation information for a single function call from across multiple streaming updates. private sealed class FunctionCallInfo(FunctionCallResponseItem item) @@ -664,15 +646,4 @@ private sealed class FunctionCallInfo(FunctionCallResponseItem item) public readonly FunctionCallResponseItem ResponseItem = item; public StringBuilder? Arguments; } - - /// Source-generated JSON type information. - [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, - UseStringEnumConverter = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = true)] - [JsonSerializable(typeof(ResponseToolJson))] - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(IDictionary))] - [JsonSerializable(typeof(string[]))] - private sealed partial class ResponseClientJsonContext : JsonSerializerContext; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs new file mode 100644 index 00000000000..f2f0c9d8a3f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs @@ -0,0 +1,79 @@ +// 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.Text.Json; +using OpenAI.Assistants; +using OpenAI.Chat; +using OpenAI.RealtimeConversation; +using OpenAI.Responses; +using Xunit; + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace Microsoft.Extensions.AI; + +public class OpenAIAIFunctionConversionTests +{ + private static readonly AIFunction _testFunction = AIFunctionFactory.Create( + ([Description("The name parameter")] string name) => name, + "test_function", + "A test function for conversion"); + + [Fact] + public void AsOpenAIChatTool_ProducesValidInstance() + { + ChatTool tool = _testFunction.AsOpenAIChatTool(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.FunctionName); + Assert.Equal("A test function for conversion", tool.FunctionDescription); + ValidateSchemaParameters(tool.FunctionParameters); + } + + [Fact] + public void AsOpenAIResponseTool_ProducesValidInstance() + { + ResponseTool tool = _testFunction.AsOpenAIResponseTool(); + + Assert.NotNull(tool); + } + + [Fact] + public void AsOpenAIConversationFunctionTool_ProducesValidInstance() + { + ConversationFunctionTool tool = _testFunction.AsOpenAIConversationFunctionTool(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.Name); + Assert.Equal("A test function for conversion", tool.Description); + ValidateSchemaParameters(tool.Parameters); + } + + [Fact] + public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() + { + FunctionToolDefinition tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.FunctionName); + Assert.Equal("A test function for conversion", tool.Description); + ValidateSchemaParameters(tool.Parameters); + } + + /// Helper method to validate function parameters match our schema + private static void ValidateSchemaParameters(BinaryData parameters) + { + Assert.NotNull(parameters); + + using var jsonDoc = JsonDocument.Parse(parameters); + var root = jsonDoc.RootElement; + + Assert.Equal("object", root.GetProperty("type").GetString()); + Assert.True(root.TryGetProperty("properties", out var properties)); + Assert.True(properties.TryGetProperty("name", out var nameProperty)); + Assert.Equal("string", nameProperty.GetProperty("type").GetString()); + Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString()); + } +} From 0652a046c4ae01d1167c14717a1c90ea71879f5f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 27 Jun 2025 08:34:16 -0400 Subject: [PATCH 156/472] Add SpeechToTextResponse.Usage (#6546) --- .../SpeechToText/SpeechToTextResponse.cs | 19 +++- .../SpeechToTextResponseUpdateExtensions.cs | 97 ++++++++----------- .../SpeechToText/SpeechToTextResponseTests.cs | 29 +++++- ...eechToTextResponseUpdateExtensionsTests.cs | 24 +++++ 4 files changed, 103 insertions(+), 66 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index 24fa20a11ed..63c6c137411 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -47,10 +47,10 @@ public SpeechToTextResponse(string? content) /// Gets or sets the ID of the speech to text response. public string? ResponseId { get; set; } - /// Gets or sets the model ID used in the creation of the speech to text completion. + /// Gets or sets the model ID used in the creation of the speech to text response. public string? ModelId { get; set; } - /// Gets or sets the raw representation of the speech to text completion from an underlying implementation. + /// Gets or sets the raw representation of the speech to text response from an underlying implementation. /// /// If a is created to represent some underlying object from another object /// model, this property can be used to store that original object. This can be useful for debugging or @@ -59,7 +59,7 @@ public SpeechToTextResponse(string? content) [JsonIgnore] public object? RawRepresentation { get; set; } - /// Gets or sets any additional properties associated with the speech to text completion. + /// Gets or sets any additional properties associated with the speech to text response. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// Gets the text of this speech to text response. @@ -76,9 +76,15 @@ public SpeechToTextResponse(string? content) /// An array of instances that may be used to represent this . public SpeechToTextResponseUpdate[] ToSpeechToTextResponseUpdates() { - SpeechToTextResponseUpdate update = new SpeechToTextResponseUpdate + IList contents = Contents; + if (Usage is { } usage) { - Contents = Contents, + contents = [.. contents, new UsageContent(usage)]; + } + + SpeechToTextResponseUpdate update = new() + { + Contents = contents, AdditionalProperties = AdditionalProperties, RawRepresentation = RawRepresentation, StartTime = StartTime, @@ -98,4 +104,7 @@ public IList Contents get => _contents ??= []; set => _contents = value; } + + /// Gets or sets usage details for the speech to text response. + public UsageDetails? Usage { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index 230ec838ba3..0f83a7a8bee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -1,13 +1,14 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; +#pragma warning disable S1121 // Assignments should not be made from within sub-expressions + namespace Microsoft.Extensions.AI; /// @@ -25,32 +26,13 @@ public static SpeechToTextResponse ToSpeechToTextResponse( _ = Throw.IfNull(updates); SpeechToTextResponse response = new(); - List contents = []; - string? responseId = null; - string? modelId = null; - AdditionalPropertiesDictionary? additionalProperties = null; - TimeSpan? endTime = null; foreach (var update in updates) { - // Track the first start time provided by the updates - response.StartTime ??= update.StartTime; - - // Track the last end time provided by the updates - if (update.EndTime is not null) - { - endTime = update.EndTime; - } - - ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); + ProcessUpdate(update, response); } - ChatResponseExtensions.CoalesceTextContent(contents); - response.EndTime = endTime; - response.Contents = contents; - response.ResponseId = responseId; - response.ModelId = modelId; - response.AdditionalProperties = additionalProperties; + ChatResponseExtensions.CoalesceTextContent((List)response.Contents); return response; } @@ -70,33 +52,13 @@ static async Task ToResponseAsync( IAsyncEnumerable updates, CancellationToken cancellationToken) { SpeechToTextResponse response = new(); - List contents = []; - string? responseId = null; - string? modelId = null; - AdditionalPropertiesDictionary? additionalProperties = null; - TimeSpan? endTime = null; await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) { - // Track the first start time provided by the updates - response.StartTime ??= update.StartTime; - - // Track the last end time provided by the updates - if (update.EndTime is not null) - { - endTime = update.EndTime; - } - - ProcessUpdate(update, contents, ref responseId, ref modelId, ref additionalProperties); + ProcessUpdate(update, response); } - ChatResponseExtensions.CoalesceTextContent(contents); - - response.EndTime = endTime; - response.Contents = contents; - response.ResponseId = responseId; - response.ModelId = modelId; - response.AdditionalProperties = additionalProperties; + ChatResponseExtensions.CoalesceTextContent((List)response.Contents); return response; } @@ -104,40 +66,59 @@ static async Task ToResponseAsync( /// Processes the , incorporating its contents and properties. /// The update to process. - /// The list of content items being accumulated. - /// The response ID to update if the update has one. - /// The model ID to update if the update has one. - /// The additional properties to update if the update has any. + /// The object that should be updated based on . private static void ProcessUpdate( SpeechToTextResponseUpdate update, - List contents, - ref string? responseId, - ref string? modelId, - ref AdditionalPropertiesDictionary? additionalProperties) + SpeechToTextResponse response) { if (update.ResponseId is not null) { - responseId = update.ResponseId; + response.ResponseId = update.ResponseId; } if (update.ModelId is not null) { - modelId = update.ModelId; + response.ModelId = update.ModelId; } - contents.AddRange(update.Contents); + if (response.StartTime is null || (update.StartTime is not null && update.StartTime < response.StartTime)) + { + // Track the first start time provided by the updates + response.StartTime = update.StartTime; + } + + if (response.EndTime is null || (update.EndTime is not null && update.EndTime > response.EndTime)) + { + // Track the last end time provided by the updates + response.EndTime = update.EndTime; + } + + foreach (var content in update.Contents) + { + switch (content) + { + // Usage content is treated specially and propagated to the response's Usage. + case UsageContent usage: + (response.Usage ??= new()).Add(usage.Details); + break; + + default: + response.Contents.Add(content); + break; + } + } if (update.AdditionalProperties is not null) { - if (additionalProperties is null) + if (response.AdditionalProperties is null) { - additionalProperties = new(update.AdditionalProperties); + response.AdditionalProperties = new(update.AdditionalProperties); } else { foreach (var entry in update.AdditionalProperties) { - additionalProperties[entry.Key] = entry.Value; + response.AdditionalProperties[entry.Key] = entry.Value; } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs index 33b27b01291..5c2ff74279e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseTests.cs @@ -31,6 +31,7 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(response.StartTime); Assert.Null(response.EndTime); Assert.Equal(string.Empty, response.ToString()); + Assert.Null(response.Usage); } [Theory] @@ -132,6 +133,11 @@ public void Properties_Roundtrip() List newContents = [new TextContent("text1"), new TextContent("text2")]; response.Contents = newContents; Assert.Same(newContents, response.Contents); + + Assert.Null(response.Usage); + UsageDetails usageDetails = new(); + response.Usage = usageDetails; + Assert.Same(usageDetails, response.Usage); } [Fact] @@ -152,6 +158,7 @@ public void JsonSerialization_Roundtrips() EndTime = TimeSpan.FromSeconds(2), RawRepresentation = new(), AdditionalProperties = new() { ["key"] = "value" }, + Usage = new() { InputTokenCount = 42, OutputTokenCount = 84, TotalTokenCount = 126 }, }; string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.SpeechToTextResponse); @@ -176,6 +183,11 @@ public void JsonSerialization_Roundtrips() Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value)); Assert.IsType(value); Assert.Equal("value", ((JsonElement)value!).GetString()); + + Assert.NotNull(result.Usage); + Assert.Equal(42, result.Usage.InputTokenCount); + Assert.Equal(84, result.Usage.OutputTokenCount); + Assert.Equal(126, result.Usage.TotalTokenCount); } [Fact] @@ -185,8 +197,10 @@ public void ToString_OutputsText() Assert.Equal("This is a test." + Environment.NewLine + "It's multiple lines.", response.ToString()); } - [Fact] - public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate(bool withUsage) { // Arrange: create a response with contents SpeechToTextResponse response = new() @@ -202,6 +216,7 @@ public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate() ResponseId = "12345", ModelId = "someModel", AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, + Usage = withUsage ? new UsageDetails { InputTokenCount = 100, OutputTokenCount = 200, TotalTokenCount = 300 } : null }; // Act: convert to streaming updates @@ -217,7 +232,7 @@ public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate() Assert.Equal(TimeSpan.FromSeconds(1), update.StartTime); Assert.Equal(TimeSpan.FromSeconds(2), update.EndTime); - Assert.Equal(3, update.Contents.Count); + Assert.Equal(withUsage ? 4 : 3, update.Contents.Count); Assert.Equal("Hello, ", Assert.IsType(update.Contents[0]).Text); Assert.Equal("image/png", Assert.IsType(update.Contents[1]).MediaType); Assert.Equal("world!", Assert.IsType(update.Contents[2]).Text); @@ -225,5 +240,13 @@ public void ToSpeechToTextResponseUpdates_ReturnsExpectedUpdate() Assert.NotNull(update.AdditionalProperties); Assert.Equal("value1", update.AdditionalProperties["key1"]); Assert.Equal(42, update.AdditionalProperties["key2"]); + + if (withUsage) + { + var usage = Assert.IsType(update.Contents[3]); + Assert.Equal(100, usage.Details.InputTokenCount); + Assert.Equal(200, usage.Details.OutputTokenCount); + Assert.Equal(300, usage.Details.TotalTokenCount); + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs index f0a2f08ab13..5d5a035bfe8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs @@ -70,6 +70,8 @@ public async Task ToSpeechToTextResponse_SuccessfullyCreatesResponse(bool useAsy Assert.Equal("d", response.AdditionalProperties["c"]); Assert.Equal("Hello human, How are You?", response.Text); + + Assert.Null(response.Usage); } [Theory] @@ -129,6 +131,28 @@ void AddGap() } } + [Fact] + public async Task ToSpeechToTextResponse_UsageContentExtractedFromContents() + { + SpeechToTextResponseUpdate[] updates = + { + new() { Contents = [new TextContent("Hello, ")] }, + new() { Contents = [new UsageContent(new() { TotalTokenCount = 42 })] }, + new() { Contents = [new TextContent("world!")] }, + new() { Contents = [new UsageContent(new() { InputTokenCount = 12, TotalTokenCount = 24 })] }, + }; + + SpeechToTextResponse response = await YieldAsync(updates).ToSpeechToTextResponseAsync(); + + Assert.NotNull(response); + + Assert.NotNull(response.Usage); + Assert.Equal(12, response.Usage.InputTokenCount); + Assert.Equal(66, response.Usage.TotalTokenCount); + + Assert.Equal("Hello, world!", Assert.IsType(Assert.Single(response.Contents)).Text); + } + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) { foreach (SpeechToTextResponseUpdate update in updates) From f99c625e3107c56ed94a5b0855378345e9923cf1 Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Fri, 27 Jun 2025 14:57:01 -0400 Subject: [PATCH 157/472] Implement BLEU score evaluation for NLP tests (#6537) * Implement BLEU score evaluation for NLP tests * Fix style warnings * Support multiple references for a single evaluator * Make some suggested updats. * More review updates * Feedback updates. * Update READMEs * Make word tokenizer internal. * Feedback updates. * More tweaks based on feedback * Remove version from NLP library --- eng/MSBuild/LegacySupport.props | 4 + .../CollectionBuilderAttribute.cs | 17 ++ src/LegacySupport/CollectionBuilder/README.md | 7 + .../README.md | 4 + .../BLEUEvaluator.cs | 96 ++++++++ .../BLEUEvaluatorContext.cs | 62 +++++ .../Common/BLEUAlgorithm.cs | 197 +++++++++++++++ .../Common/MatchCounter.cs | 61 +++++ .../Common/NGram.cs | 55 +++++ .../Common/NGramExtensions.cs | 41 ++++ .../Common/NLPScoreInterpretation.cs | 36 +++ .../Common/RationalNumber.cs | 39 +++ .../Common/SimpleWordTokenizer.cs | 217 +++++++++++++++++ .../Common/SmoothingFunction.cs | 74 ++++++ ...rosoft.Extensions.AI.Evaluation.NLP.csproj | 39 +++ .../README.md | 53 ++++ .../README.md | 4 + .../README.md | 4 + .../README.md | 4 + .../README.md | 4 + .../BLEUAlgorithmTests.cs | 226 ++++++++++++++++++ .../BLEUEvaluatorTests.cs | 113 +++++++++ .../MatchCounterTests.cs | 65 +++++ ....Extensions.AI.Evaluation.NLP.Tests.csproj | 13 + .../NGramTests.cs | 80 +++++++ .../RationalNumberTests.cs | 55 +++++ .../SimpleTokenizerTests.cs | 72 ++++++ 27 files changed, 1642 insertions(+) create mode 100644 src/LegacySupport/CollectionBuilder/CollectionBuilderAttribute.cs create mode 100644 src/LegacySupport/CollectionBuilder/README.md create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGram.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/RationalNumber.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SmoothingFunction.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUEvaluatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/Microsoft.Extensions.AI.Evaluation.NLP.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/RationalNumberTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs diff --git a/eng/MSBuild/LegacySupport.props b/eng/MSBuild/LegacySupport.props index 6b110acaaa1..7bda63a6607 100644 --- a/eng/MSBuild/LegacySupport.props +++ b/eng/MSBuild/LegacySupport.props @@ -74,4 +74,8 @@ + + + + diff --git a/src/LegacySupport/CollectionBuilder/CollectionBuilderAttribute.cs b/src/LegacySupport/CollectionBuilder/CollectionBuilderAttribute.cs new file mode 100644 index 00000000000..569daa70dff --- /dev/null +++ b/src/LegacySupport/CollectionBuilder/CollectionBuilderAttribute.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +internal sealed class CollectionBuilderAttribute : Attribute +{ + public CollectionBuilderAttribute(Type builderType, string methodName) + { + BuilderType = builderType; + MethodName = methodName; + } + + public Type BuilderType { get; } + public string MethodName { get; } +} diff --git a/src/LegacySupport/CollectionBuilder/README.md b/src/LegacySupport/CollectionBuilder/README.md new file mode 100644 index 00000000000..15e9274d433 --- /dev/null +++ b/src/LegacySupport/CollectionBuilder/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md index c21e2a299ad..580facd6294 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs new file mode 100644 index 00000000000..8ce43d48e52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// An that evaluates the quality of a response produced by an AI model by comparing +/// it to a reference response using the BLEU (Bilingual Evaluation Understudy) algorithm. It is often used +/// to evaluate the quality of machine translation or text generation tasks. +/// +/// +/// +/// The computes the BLEU score of a response ("hypothesis") compared to a reference +/// supplied via . The score is returned in a +/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates a perfect match. +/// By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher is +/// passing and a score below 0.5 is failing. +/// +/// +public sealed class BLEUEvaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string BLEUMetricName => "BLEU"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [BLEUMetricName]; + + /// + public ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + + var metric = new NumericMetric(BLEUMetricName); + var result = new EvaluationResult(metric); + + if (string.IsNullOrWhiteSpace(modelResponse.Text)) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error($"The {nameof(modelResponse)} supplied for evaluation was null or empty.")); + + return new ValueTask(result); + } + + if (additionalContext?.OfType().FirstOrDefault() + is not BLEUEvaluatorContext context) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"A value of type '{nameof(BLEUEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + + return new ValueTask(result); + } + + if (context.References.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied '{nameof(BLEUEvaluatorContext)}' did not contain any '{nameof(BLEUEvaluatorContext.References)}'.")); + + return new ValueTask(result); + } + + var (score, duration) = TimingHelper.ExecuteWithTiming(() => + { + var references = context.References.Select(reference => SimpleWordTokenizer.WordTokenize(reference)); + var hypothesis = SimpleWordTokenizer.WordTokenize(modelResponse.Text); + return BLEUAlgorithm.SentenceBLEU(references, hypothesis, BLEUAlgorithm.DefaultBLEUWeights, SmoothingFunction.Method4); + }); + + metric.Value = score; + string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); + metric.AddOrUpdateContext(context); + metric.Interpretation = NLPScoreInterpretation.Interpret(metric); + + return new ValueTask(result); + } + +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs new file mode 100644 index 00000000000..320b20e9116 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S3604 +// S3604: Member initializer values should not be redundant. +// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary +// constructor syntax. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// Contextual information that the uses to compute the BLEU score for a response. +/// +/// +/// measures the BLEU score of a response compared to a reference. BLEU (Bilingual Evaluation Understudy) +/// is a metric used to evaluate the quality of machine-generated text. +/// +public sealed class BLEUEvaluatorContext : EvaluationContext +{ + /// + /// Gets the unique that is used for + /// . + /// + public static string BLEUContextName => "BLEU Context"; + + /// + /// Gets the reference responses against which the provided model response will be scored. + /// + /// + /// The measures the degree to which the response being evaluated is similar to + /// the response supplied via . The metric will be reported as a BLEU score. + /// + public IReadOnlyList References { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The reference responses against which the response that is being evaluated is compared. + /// + public BLEUEvaluatorContext(params string[] references) + : this(references as IEnumerable) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The reference responses against which the response that is being evaluated is compared. + /// + public BLEUEvaluatorContext(IEnumerable references) + : base( + name: BLEUContextName, + contents: [.. references.Select(c => new TextContent(c))]) + { + References = [.. references]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs new file mode 100644 index 00000000000..c7420d0be7a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs @@ -0,0 +1,197 @@ +// 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.Linq; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +/// +/// Helper methods for calculating the BLEU score. +/// See BLEU on Wikipedia or +/// NLTK implementation +/// for more details. +/// +internal static class BLEUAlgorithm +{ + internal static int ClosestRefLength(IEnumerable> references, int hypLength) + { + if (!references.Any()) + { + return 0; + } + + int closestRefLength = 0; + int smallestDiff = int.MaxValue; + foreach (var reference in references) + { + int refLength = reference.Count(); + int diff = Math.Abs(refLength - hypLength); + if (diff < smallestDiff || + (diff == smallestDiff && refLength < closestRefLength)) + { + smallestDiff = diff; + closestRefLength = refLength; + } + } + + return closestRefLength; + } + + internal static double BrevityPenalty(int closestRefLength, int hypLength) + { + if (hypLength <= 0) + { + return 0.0; + } + + if (closestRefLength <= 0 || hypLength > closestRefLength) + { + return 1.0; + } + + return Math.Exp(1 - ((double)closestRefLength / hypLength)); + } + + internal static RationalNumber ModifiedPrecision(IEnumerable> references, IEnumerable hypothesis, int n = 1) + { + if (n <= 0) + { + Throw.ArgumentOutOfRangeException(nameof(n), $"`{nameof(n)}` must be greater than zero."); + } + + if (!references.Any() || !hypothesis.Any()) + { + return RationalNumber.Zero; + } + + var hyp = hypothesis.CreateNGrams(n); + var hypCounts = new MatchCounter>(hyp); + + Dictionary, int> maxCounts = []; + + foreach (var rf in references) + { + IEnumerable> refGrams = rf.CreateNGrams(n); + var refCounts = new MatchCounter>(refGrams); + + foreach (var ct in refCounts) + { + if (maxCounts.TryGetValue(ct.Key, out int val)) + { + maxCounts[ct.Key] = Math.Max(val, ct.Value); + } + else + { + maxCounts[ct.Key] = ct.Value; + } + } + } + + Dictionary, int> clippedCounts = []; + foreach (var h in hypCounts) + { + if (maxCounts.TryGetValue(h.Key, out var v)) + { + clippedCounts[h.Key] = Math.Min(h.Value, v); + } + else + { + // If the hypothesis n-gram is not in any reference, it is clipped to 0. + clippedCounts[h.Key] = 0; + } + } + + int numerator = clippedCounts.Values.Sum(); + int denominator = Math.Max(1, hypCounts.Sum()); + + return new RationalNumber(numerator, denominator); + } + + /// + /// Generate an n-sized array of equal weights that sum to 1.0. + /// + /// Number of weights to return. + /// Array of equal sized values that sum to 1.0. + internal static double[] EqualWeights(int n) + { + if (n <= 0) + { + Throw.ArgumentOutOfRangeException(nameof(n), $"'{nameof(n)}' must be greater than zero."); + } + + double[] weights = new double[n]; + for (int i = 0; i < n; i++) + { + weights[i] = 1.0 / n; + } + + return weights; + } + + internal static readonly double[] DefaultBLEUWeights = EqualWeights(4); + + internal static double SentenceBLEU(IEnumerable> references, IEnumerable hypothesis, + double[]? weights = null, Func? smoothingFunction = null) + { + if (references == null || !references.Any()) + { + Throw.ArgumentNullException(nameof(references), $"'{nameof(references)}' cannot be null or empty."); + } + + if (hypothesis == null || !hypothesis.Any()) + { + Throw.ArgumentNullException(nameof(hypothesis), $"'{nameof(hypothesis)}' cannot be null or empty."); + } + + if (weights is null) + { + weights = DefaultBLEUWeights; + } + + if (weights.Length == 0) + { + Throw.ArgumentNullException(nameof(weights), $"'{nameof(weights)}' cannot be empty."); + } + + var precisionValues = new RationalNumber[weights.Length]; + for (int i = 0; i < weights.Length; i++) + { + int n = i + 1; + RationalNumber prec = ModifiedPrecision(references, hypothesis, n); + + if (i == 0 && prec.Numerator == 0) + { + // If the precision for unigrams (n == 1) is zero, the there can be no higher order matches and BLEU score is zero. + return 0.0; + } + + precisionValues[i] = prec; + } + + int hypLen = hypothesis.Count(); + int closestRefLength = ClosestRefLength(references, hypLen); + double brevityPenalty = BrevityPenalty(closestRefLength, hypLen); + + if (smoothingFunction == null) + { + smoothingFunction = SmoothingFunction.Method0; + } + + double[] smoothedValues = smoothingFunction(precisionValues, hypLen); + + double score = 0.0; + for (int i = 0; i < weights.Length; i++) + { + if (smoothedValues[i] > 0) + { + score += weights[i] * Math.Log(smoothedValues[i]); + } + } + + return brevityPenalty * Math.Exp(score); + } + +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs new file mode 100644 index 00000000000..bbca2252057 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs @@ -0,0 +1,61 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +[DebuggerDisplay("{ToDebugString(),nq}")] +internal readonly struct MatchCounter : IEnumerable> + where T : IEquatable +{ + private readonly Dictionary _counts = []; + + public readonly int Sum() => _counts.Values.Sum(); + + public MatchCounter() + { + } + + public MatchCounter(IEnumerable items) + { + _ = Throw.IfNull(items, nameof(items)); + AddRange(items); + } + + public void Add(T item) + { + if (_counts.TryGetValue(item, out int currentCount)) + { + _counts[item] = currentCount + 1; + } + else + { + _counts[item] = 1; + } + } + + public void AddRange(IEnumerable items) + { + if (items == null) + { + return; + } + + foreach (var item in items) + { + Add(item); + } + } + + public string ToDebugString() => string.Concat(_counts.Select(v => $"{v.Key}: {v.Value}, ")); + + public IEnumerator> GetEnumerator() => _counts.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_counts).GetEnumerator(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGram.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGram.cs new file mode 100644 index 00000000000..5fb66461faf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGram.cs @@ -0,0 +1,55 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +[DebuggerDisplay("{ToDebugString(),nq}")] +[CollectionBuilder(typeof(NGramExtensions), nameof(NGramExtensions.CreateNGram))] +internal readonly struct NGram : IEquatable>, IEnumerable + where T : IEquatable +{ + public NGram(ReadOnlySpan values) + : this(values.ToArray()) + { + } + + public NGram(params T[] values) + { + Values = Throw.IfNull(values, nameof(values)); + _ = Throw.IfLessThan(values.Length, 1, nameof(values)); + } + + public readonly T[] Values { get; } + + public int Length => Values.Length; + + public bool Equals(NGram other) + => Values.SequenceEqual(other.Values); + + public override bool Equals(object? obj) => obj is NGram other && Equals(other); + + public override int GetHashCode() + { + int hashCode = 0; + foreach (var value in Values) + { + hashCode = HashCode.Combine(hashCode, value.GetHashCode()); + } + + return hashCode; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)Values).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public string ToDebugString() => $"[{string.Join(",", Values.Select(v => v.ToString()))}]"; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs new file mode 100644 index 00000000000..149d3820328 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs @@ -0,0 +1,41 @@ +// 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.Linq; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +internal static class NGramExtensions +{ + // Collection builder method. + public static NGram CreateNGram(this ReadOnlySpan values) + where T : IEquatable => new(values); + + /// + /// Create a sequence of n-grams from the input sequence. + /// + /// The input sequence of items. + /// The size of each n-gram. + internal static IEnumerable> CreateNGrams(this IEnumerable input, int n) + where T : IEquatable + { + if (n <= 0) + { + Throw.ArgumentOutOfRangeException(nameof(n), $"'{nameof(n)}' must be greater than zero."); + } + + T[] output = [.. input.Take(n)]; + + while (output.Length == n) + { + yield return new NGram(output); + + input = input.Skip(1); + output = [.. input.Take(n)]; + } + } + +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs new file mode 100644 index 00000000000..4ef1d08b468 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +internal static class NLPScoreInterpretation +{ + internal static EvaluationMetricInterpretation Interpret(NumericMetric metric) + { + // Many NLP scores range from 0.0 to 1.0, where: + // - 0.0 means no match at all, + // - 1.0 means a perfect match. + // 0.5 is considered the minimum passing score for evaluation. + + EvaluationRating rating = metric.Value switch + { + null => EvaluationRating.Inconclusive, + > 1.0 => EvaluationRating.Inconclusive, + > 0.8 and <= 1.0 => EvaluationRating.Exceptional, + > 0.6 and <= 0.8 => EvaluationRating.Good, + > 0.4 and <= 0.6 => EvaluationRating.Average, + > 0.2 and <= 0.4 => EvaluationRating.Poor, + >= 0.0 and <= 0.2 => EvaluationRating.Unacceptable, + < 0.0 => EvaluationRating.Inconclusive, + _ => EvaluationRating.Inconclusive, + }; + + const double MinimumPassingScore = 0.5; + return metric.Value is double value && value < MinimumPassingScore + ? new EvaluationMetricInterpretation( + rating, + failed: true, + reason: $"{metric.Name} is less than {MinimumPassingScore}.") + : new EvaluationMetricInterpretation(rating); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/RationalNumber.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/RationalNumber.cs new file mode 100644 index 00000000000..500b042b17b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/RationalNumber.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +[DebuggerDisplay("{ToDebugString(),nq}")] +internal readonly struct RationalNumber : IEquatable +{ + public static readonly RationalNumber Zero = new(0, 1); + + public RationalNumber(int numerator, int denominator) + { + if (denominator == 0) + { + throw new DivideByZeroException("Denominator cannot be zero."); + } + + Numerator = numerator; + Denominator = denominator; + } + + public int Numerator { get; } + public int Denominator { get; } + + public double ToDouble() => (double)Numerator / Denominator; + + public string ToDebugString() => $"{Numerator}/{Denominator}"; + + public bool Equals(RationalNumber other) + => other.Numerator == Numerator && other.Denominator == Denominator; + + public override bool Equals(object? obj) => obj is RationalNumber other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(Numerator, Denominator); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs new file mode 100644 index 00000000000..4f4717852bd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs @@ -0,0 +1,217 @@ +// 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; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable S109 // Magic numbers should not be used + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +/// +/// Tokenizes a string into segments using the common rules established by the NLTK word tokenizer. +/// +internal static class SimpleWordTokenizer +{ + /// + /// Tokenizes the input text into individual words based on specific rules for text normalization and segmentation. + /// + /// This method applies text normalization steps, such as removing skipped markers, handling line + /// breaks, and replacing common HTML entities. It also ensures consistent tokenization by inserting spaces around + /// punctuation, symbols, and certain character patterns. The tokenization rules are inspired by common BLEU algorithms, + /// such as those used in NLTK, SacreBLEU, and MOSES. + /// The input text to be tokenized. Cannot be . + /// An enumerable collection of strings, where each string represents a tokenized word. The collection will be empty + /// if the input text contains no valid tokens. + public static IEnumerable WordTokenize(string text) + { + _ = Throw.IfNull(text, nameof(text)); + + return WordTokenize(text.AsMemory()); + } + + /// + /// Tokenizes the input text into individual words based on specific rules for text normalization and segmentation. + /// + /// This method applies text normalization steps, such as removing skipped markers, handling line + /// breaks, and replacing common HTML entities. It also ensures consistent tokenization by inserting spaces around + /// punctuation, symbols, and certain character patterns. The tokenization rules are inspired by common BLEU algorithms, + /// such as those used in NLTK, SacreBLEU, and MOSES. + /// The input text to be tokenized. Cannot be . + /// An enumerable collection of strings, where each string represents a tokenized word. The collection will be empty + /// if the input text contains no valid tokens. + public static IEnumerable WordTokenize(ReadOnlyMemory text) + { + StringBuilder sb = new StringBuilder(); + + while (true) + { + if (text.IsEmpty) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + yield break; + } + + var span = text.Span; + char nextChar = span[0]; + + // Skip whitespace as separator + if (char.IsWhiteSpace(nextChar)) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice(1); + continue; + } + + // Join hyphenated words + if (span[0] == '-' && + span.Length > 1 && + span[1] == '\n') + { + text = text.Slice(2); + continue; + } + + if (span[0] == '-' && + span.Length > 2 && + span[1] == '\r' && + span[2] == '\n') + { + text = text.Slice(3); + continue; + } + + // Translate HTML entities + if (nextChar == '&') + { + if (span.StartsWith(""".AsSpan())) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice(""".Length); + yield return "\""; + continue; + } + else if (span.StartsWith("&".AsSpan())) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice("&".Length); + yield return "&"; + continue; + } + else if (span.StartsWith("<".AsSpan())) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice("<".Length); + yield return "<"; + continue; + } + else if (span.StartsWith(">".AsSpan())) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice(">".Length); + yield return ">"; + continue; + } + else if (span.StartsWith("'".AsSpan())) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + text = text.Slice("'".Length); + yield return "'"; + continue; + } + } + + // Each symbol is a separate token + if (char.IsSymbol(nextChar)) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + yield return nextChar.ToString(); + text = text.Slice(1); + continue; + } + + // Return punctuation + if (char.IsPunctuation(nextChar)) + { + if (sb.Length > 0) + { + yield return sb.ToString(); + _ = sb.Clear(); + } + + yield return nextChar.ToString(); + text = text.Slice(1); + continue; + } + + // if we have a number, consume it along with any internal punctuation + if (char.IsNumber(nextChar)) + { + // in this case we are still building a token, then the number + // should be added to the end of it, rather than as a separate number + if (sb.Length > 0) + { + _ = sb.Append(nextChar); + text = text.Slice(1); + continue; + } + + while (!text.IsEmpty && (char.IsNumber(text.Span[0]) || char.IsPunctuation(text.Span[0]))) + { + _ = sb.Append(text.Span[0]); + text = text.Slice(1); + } + + yield return sb.ToString(); + _ = sb.Clear(); + continue; + } + + _ = sb.Append(char.ToUpperInvariant(nextChar)); + text = text.Slice(1); + } + + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SmoothingFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SmoothingFunction.cs new file mode 100644 index 00000000000..0e3071f6bdd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SmoothingFunction.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +/// +/// Implementations of smoothing functions for BLEU scores taken from +/// `A Systematic Comparison of Smoothing Techniques for Sentence-Level BLEU` +/// by Chen and Cherry. http://acl2014.org/acl2014/W14-33/pdf/W14-3346.pdf. +/// +internal static class SmoothingFunction +{ + /// + /// This is the baseline method, which does not apply any smoothing. + /// + /// N precision values to be smoothed. + /// Number of tokens in the hypothesis. + /// Smoothed precision values. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Matches expected signature of SmoothingFunction")] + internal static double[] Method0(RationalNumber[] precisions, int hypLen) + { + double[] smoothed = new double[precisions.Length]; + for (int i = 0; i < precisions.Length; i++) + { + if (precisions[i].Numerator == 0) + { + smoothed[i] = double.Epsilon; + } + else + { + smoothed[i] = precisions[i].ToDouble(); + } + } + + return smoothed; + } + + /// + /// Smoothing method 4: + /// Shorter translations may have inflated precision values due to having + /// smaller denominators; therefore, we give them proportionally + /// smaller smoothed counts. Instead of scaling to 1/(2^k), Chen and Cherry + /// suggests dividing by 1/ln(len(T)), where T is the length of the translation. + /// + /// N precision values to be smoothed. + /// Number of tokens in the hypothesis. + /// Smoothed precision values. + internal static double[] Method4(RationalNumber[] precisions, int hypLen) + { + const double DefaultK = 5.0; + + double[] smoothed = new double[precisions.Length]; + + int inc = 1; + for (int i = 0; i < precisions.Length; i++) + { + RationalNumber p = precisions[i]; + if (p.Numerator == 0 && hypLen > 1) + { + double numerator = 1 / (Math.Pow(2.0, inc) * DefaultK / Math.Log(hypLen)); + smoothed[i] = numerator / p.Denominator; + inc++; + } + else + { + smoothed[i] = p.ToDouble(); + } + } + + return smoothed; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj new file mode 100644 index 00000000000..0bab1cf7fb0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj @@ -0,0 +1,39 @@ + + + + A library that contains a set of evaluators that implement commonly used algorithmic evaluators. + $(TargetFrameworks);netstandard2.0 + Microsoft.Extensions.AI.Evaluation.NLP + + + + AIEval + preview + true + false + 0 + 0 + + + + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md new file mode 100644 index 00000000000..580facd6294 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md @@ -0,0 +1,53 @@ +# The Microsoft.Extensions.AI.Evaluation libraries + +`Microsoft.Extensions.AI.Evaluation` is a set of .NET libraries defined in the following NuGet packages that have been designed to work together to support building processes for evaluating the quality of AI software. + +* [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. +* [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. +* [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. +* [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. +* [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. +* [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. + +## Install the packages + +From the command-line: + +```console +dotnet add package Microsoft.Extensions.AI.Evaluation +dotnet add package Microsoft.Extensions.AI.Evaluation.Quality +dotnet add package Microsoft.Extensions.AI.Evaluation.Safety +dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP +``` + +Or directly in the C# project file: + +```xml + + + + + + + +``` + +You can optionally add the `Microsoft.Extensions.AI.Evaluation.Reporting.Azure` package in either of these places if you need Azure Storage support. + +## Install the command line tool + +```console +dotnet tool install Microsoft.Extensions.AI.Evaluation.Console --create-manifest-if-needed +``` + +## Usage Examples + +For a comprehensive tour of all the functionality, concepts and APIs available in the `Microsoft.Extensions.AI.Evaluation` libraries, check out the [API Usage Examples](https://github.com/dotnet/ai-samples/blob/main/src/microsoft-extensions-ai-evaluation/api/) available in the [dotnet/ai-samples](https://github.com/dotnet/ai-samples) repo. These examples are structured as a collection of unit tests. Each unit test showcases a specific concept or API, and builds on the concepts and APIs showcased in previous unit tests. + + +## Feedback & Contributing + +We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions). diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md index c21e2a299ad..580facd6294 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md index c21e2a299ad..580facd6294 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md index c042da70deb..e135ed24cfe 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md index c21e2a299ad..580facd6294 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU score, with more planned. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs new file mode 100644 index 00000000000..1b029dc4a37 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs @@ -0,0 +1,226 @@ +// 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.Linq; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Xunit; +using static Microsoft.Extensions.AI.Evaluation.NLP.Common.BLEUAlgorithm; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +public class BLEUAlgorithmTests +{ + [Fact] + public void ModifiedPrecisionTests() + { + IEnumerable> references = ["the cat is on the mat".Split(' '), "there is a cat on the mat".Split(' ')]; + IEnumerable hypothesis = "the the the the the the the".Split(' '); + RationalNumber prec = ModifiedPrecision(references, hypothesis, 1); + Assert.Equal(0.2857, prec.ToDouble(), 4); + + references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), + "It is the practical guide for the army always to heed the directions of the party".Split(' '), + ]; + hypothesis = "of the".Split(' '); + prec = ModifiedPrecision(references, hypothesis, 1); + Assert.Equal(1.0, prec.ToDouble(), 4); + prec = ModifiedPrecision(references, hypothesis, 2); + Assert.Equal(1.0, prec.ToDouble(), 4); + + references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), + "It is the practical guide for the army always to heed the directions of the party".Split(' '), + ]; + IEnumerable hypothesis1 = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + IEnumerable hypothesis2 = "It is to insure the troops forever hearing the activity guidebook that party direct".Split(' '); + prec = ModifiedPrecision(references, hypothesis1, 1); + Assert.Equal(0.9444, prec.ToDouble(), 4); + prec = ModifiedPrecision(references, hypothesis2, 1); + Assert.Equal(0.5714, prec.ToDouble(), 4); + prec = ModifiedPrecision(references, hypothesis1, 2); + Assert.Equal(0.5882, prec.ToDouble(), 4); + prec = ModifiedPrecision(references, hypothesis2, 2); + Assert.Equal(0.07692, prec.ToDouble(), 4); + } + + [Theory] + [InlineData(new int[] { 0, 1, 0, 2 }, 10, new[] { 0.2303, 0.0576 })] + [InlineData(new int[] { 4, 5, 2, 4 }, 10, new[] { 0.8000, 0.5 })] + [InlineData(new int[] { 10, 14, 7, 13, 5, 12, 4, 11 }, 20, new[] { 0.7143, 0.5385, 0.4167, 0.3636 })] + [InlineData(new int[] { 10, 14, 7, 13, 0, 12, 0, 11 }, 20, new[] { 0.7143, 0.5385, 0.02496, 0.01362 })] + public void SmoothingMethod4Tests(int[] num_denom, int hypLen, double[] vals) + { + Assert.Equal(num_denom.Length, vals.Length * 2); + + RationalNumber[] prec = new RationalNumber[vals.Length]; + for (int i = 0; i < num_denom.Length - 1; i += 2) + { + prec[i / 2] = new RationalNumber(num_denom[i], num_denom[i + 1]); + } + + double[] smoothed = SmoothingFunction.Method4(prec, hypLen); + + Assert.Equal(vals.Length, smoothed.Length); + + for (int i = 0; i < vals.Length; i++) + { + Assert.Equal(vals[i], smoothed[i], 4); + } + } + + [Fact] + public void TestBrevityPenalty() + { + IEnumerable> references = [ + Enumerable.Repeat("a", 11), + Enumerable.Repeat("a", 8), + ]; + IEnumerable hypothesis = Enumerable.Repeat("a", 7); + int hypLength = hypothesis.Count(); + int closestRefLength = ClosestRefLength(references, hypLength); + double brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(0.8669, brevityPenalty, 4); + + references = [ + Enumerable.Repeat("a", 11), + Enumerable.Repeat("a", 8), + Enumerable.Repeat("a", 6), + Enumerable.Repeat("a", 7), + ]; + hypothesis = Enumerable.Repeat("a", 7); + hypLength = hypothesis.Count(); + closestRefLength = ClosestRefLength(references, hypLength); + brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(1.0, brevityPenalty, 4); + + references = [ + Enumerable.Repeat("a", 28), + Enumerable.Repeat("a", 28), + ]; + hypothesis = Enumerable.Repeat("a", 12); + hypLength = hypothesis.Count(); + closestRefLength = ClosestRefLength(references, hypLength); + brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(0.26359, brevityPenalty, 4); + + references = [ + Enumerable.Repeat("a", 13), + Enumerable.Repeat("a", 2), + ]; + hypothesis = Enumerable.Repeat("a", 12); + hypLength = hypothesis.Count(); + closestRefLength = ClosestRefLength(references, hypLength); + brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(0.9200, brevityPenalty, 4); + + references = [ + Enumerable.Repeat("a", 13), + Enumerable.Repeat("a", 11), + ]; + hypothesis = Enumerable.Repeat("a", 12); + hypLength = hypothesis.Count(); + closestRefLength = ClosestRefLength(references, hypLength); + brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(1.0, brevityPenalty, 4); + + references = [ + Enumerable.Repeat("a", 11), + Enumerable.Repeat("a", 13), + ]; + hypothesis = Enumerable.Repeat("a", 12); + hypLength = hypothesis.Count(); + closestRefLength = ClosestRefLength(references, hypLength); + brevityPenalty = BrevityPenalty(closestRefLength, hypLength); + Assert.Equal(1.0, brevityPenalty, 4); + + } + + [Fact] + public void TestZeroMatches() + { + IEnumerable> references = ["The candidate has no alignment to any of the references".Split(' '),]; + IEnumerable hypothesis = "John loves Mary".Split(' '); + + double score = SentenceBLEU(references, hypothesis, EqualWeights(hypothesis.Count())); + Assert.Equal(0.0, score, 4); + } + + [Fact] + public void TestFullMatches() + { + IEnumerable> references = ["John loves Mary".Split(' '),]; + IEnumerable hypothesis = "John loves Mary".Split(' '); + + double score = SentenceBLEU(references, hypothesis, EqualWeights(hypothesis.Count())); + Assert.Equal(1.0, score, 4); + } + + [Fact] + public void TestPartialMatchesHypothesisLongerThanReference() + { + IEnumerable> references = ["John loves Mary".Split(' '),]; + IEnumerable hypothesis = "John loves Mary who loves Mike".Split(' '); + + double score = SentenceBLEU(references, hypothesis); + Assert.Equal(0, score, 4); + } + + [Fact] + public void TestSentenceBLEUExampleA() + { + IEnumerable> references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), + "It is the practical guide for the army always to heed the directions of the party".Split(' ') + ]; + IEnumerable hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + + double score = SentenceBLEU(references, hypothesis); + Assert.Equal(0.5046, score, 4); + + } + + [Fact] + public void TestSentenceBLEUExampleB() + { + IEnumerable> references = [ + "he was interested in world history because he read the book".Split(' '), + ]; + IEnumerable hypothesis = "he read the book because he was interested in world history".Split(' '); + + double score = SentenceBLEU(references, hypothesis); + Assert.Equal(0.74009, score, 4); + } + + [Fact] + public void TestSentenceBLEUExampleAWithWordTokenizer() + { + IEnumerable> references = [ + SimpleWordTokenizer.WordTokenize("It is a guide to action that ensures that the military will forever heed Party commands"), + SimpleWordTokenizer.WordTokenize("It is the guiding principle which guarantees the military forces always being under the command of the Party"), + SimpleWordTokenizer.WordTokenize("It is the practical guide for the army always to heed the directions of the party") + ]; + IEnumerable hypothesis = SimpleWordTokenizer.WordTokenize("It is a guide to action which ensures that the military always obeys the commands of the party"); + + double score = SentenceBLEU(references, hypothesis); + Assert.Equal(0.5046, score, 4); + + } + + [Fact] + public void TestSentenceBLEUExampleBWithWordTokenizer() + { + IEnumerable> references = [ + SimpleWordTokenizer.WordTokenize("he was interested in world history because he read the book"), + ]; + IEnumerable hypothesis = SimpleWordTokenizer.WordTokenize("he read the book because he was interested in world history"); + + double score = SentenceBLEU(references, hypothesis); + Assert.Equal(0.74009, score, 4); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUEvaluatorTests.cs new file mode 100644 index 00000000000..48fda1357ab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUEvaluatorTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +#pragma warning disable AIEVAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +public class BLEUEvaluatorTests +{ + [Fact] + public async Task ReturnsPerfectScoreForIdenticalText() + { + var evaluator = new BLEUEvaluator(); + var context = new BLEUEvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "The quick brown fox jumps over the lazy dog.")); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.Equal(1.0, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Exceptional, metric.Interpretation.Rating); + Assert.False(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsLowScoreForCompletelyDifferentText() + { + var evaluator = new BLEUEvaluator(); + var context = new BLEUEvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Completely unrelated sentence.")); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.Equal(0.0136, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Unacceptable, metric.Interpretation.Rating); + Assert.True(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfNoContext() + { + var evaluator = new BLEUEvaluator(); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Some text.")); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, additionalContext: null); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + + [Theory] + [InlineData("the cat is on the mat", + "the the the the the the the", 0.0385)] + [InlineData("It is a guide to action that ensures that the military will forever heed Party commands", + "It is a guide to action which ensures that the military always obeys the commands of the party", 0.4209)] + [InlineData("It is the practical guide for the army always to heed the directions of the party", + "It is to insure the troops forever hearing the activity guidebook that party direct", 0.0471)] + public async Task SampleCases(string reference, string hypothesis, double score) + { + var evaluator = new BLEUEvaluator(); + var context = new BLEUEvaluatorContext(reference); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, hypothesis)); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.Equal(score, (double)metric!.Value!, 4); + } + + [Fact] + public async Task MultipleReferences() + { + string[] references = [ + "It is a guide to action that ensures that the military will forever heed Party commands", + "It is the guiding principle which guarantees the military forces always being under the command of the Party", + "It is the practical guide for the army always to heed the directions of the party", + ]; + string hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party"; + + var evaluator = new BLEUEvaluator(); + var context = new BLEUEvaluatorContext(references); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, hypothesis)); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.Equal(0.5046, (double)metric!.Value!, 4); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfEmptyResponse() + { + var evaluator = new BLEUEvaluator(); + var context = new BLEUEvaluatorContext("Reference text."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "")); + var result = await evaluator.EvaluateAsync(response, chatConfiguration: null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(BLEUEvaluator.BLEUMetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs new file mode 100644 index 00000000000..9c2a5b68900 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +public class MatchCounterTests +{ + [Fact] + public void EmptyConstructor_InitializesEmptyCounter() + { + var counter = new MatchCounter(); + Assert.Empty(counter); + Assert.Equal(0, counter.Sum()); + } + + [Fact] + public void ConstructorWithItems_CountsCorrectly() + { + var counter = new MatchCounter(new[] { "a", "b", "a", "c", "b", "a" }); + var dict = counter.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.Equal(3, dict["a"]); + Assert.Equal(2, dict["b"]); + Assert.Equal(1, dict["c"]); + Assert.Equal(6, counter.Sum()); + } + + [Fact] + public void Add_AddsSingleItemCorrectly() + { + var counter = new MatchCounter(); + counter.Add(5); + counter.Add(5); + counter.Add(3); + var dict = counter.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.Equal(2, dict[5]); + Assert.Equal(1, dict[3]); + Assert.Equal(3, counter.Sum()); + } + + [Fact] + public void AddRange_AddsMultipleItemsCorrectly() + { + var counter = new MatchCounter(); + counter.AddRange("hello"); + var dict = counter.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.Equal(1, dict['h']); + Assert.Equal(1, dict['e']); + Assert.Equal(2, dict['l']); + Assert.Equal(1, dict['o']); + Assert.Equal(5, counter.Sum()); + } + + [Fact] + public void ToDebugString_FormatsCorrectly() + { + var counter = new MatchCounter(new[] { "x", "y", "x" }); + var str = counter.ToDebugString(); + Assert.Contains("x: 2", str); + Assert.Contains("y: 1", str); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/Microsoft.Extensions.AI.Evaluation.NLP.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/Microsoft.Extensions.AI.Evaluation.NLP.Tests.csproj new file mode 100644 index 00000000000..6b485136520 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/Microsoft.Extensions.AI.Evaluation.NLP.Tests.csproj @@ -0,0 +1,13 @@ + + + + Microsoft.Extensions.AI.Evaluation.NLP.Tests + Unit tests for Microsoft.Extensions.AI.Evaluation.NLP. + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs new file mode 100644 index 00000000000..d782c3c8f88 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs @@ -0,0 +1,80 @@ +// 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.Linq; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +public class NGramTests +{ + [Fact] + public void Constructor_ValuesAndLength() + { + var ngram = new NGram(1, 2, 3); + Assert.Equal(new[] { 1, 2, 3 }, ngram.Values); + Assert.Equal(3, ngram.Length); + } + + [Fact] + public void Constructor_ThrowsOnEmpty() + { + Assert.Throws(() => new NGram(Array.Empty())); + } + + [Fact] + public void Equals_And_HashCode_WorkCorrectly() + { + var a = new NGram(1, 2, 3); + var b = new NGram(1, 2, 3); + var c = new NGram(3, 2, 1); + Assert.True(a.Equals(b)); + Assert.True(a.Equals((object)b)); + Assert.False(a.Equals(c)); + Assert.NotEqual(a.GetHashCode(), c.GetHashCode()); + } + + [Fact] + public void Enumerator_And_IEnumerable() + { + var ngram = new NGram('a', 'b', 'c'); + var list = ngram.ToList(); + Assert.Equal(new[] { 'a', 'b', 'c' }, list); + } + + [Fact] + public void ToDebugString_FormatsCorrectly() + { + var ngram = new NGram("x", "y"); + Assert.Equal("[x,y]", ngram.ToDebugString()); + } + + [Fact] + public void NGramBuilder_Create_Works() + { + NGram ngram = [1, 2]; + Assert.Equal(new NGram(1, 2), ngram); + } + + [Fact] + public void NGramGenerationNoPadding() + { + int[] input = [1, 2, 3, 4, 5]; + + IEnumerable> result = input.CreateNGrams(1); + List> expected = [[1], [2], [3], [4], [5]]; + Assert.True(result.SequenceEqual(expected)); + + result = input.CreateNGrams(2); + expected = [[1, 2], [2, 3], [3, 4], [4, 5]]; + Assert.True(result.SequenceEqual(expected)); + + result = input.CreateNGrams(3); + expected = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]; + Assert.True(result.SequenceEqual(expected)); + } + +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/RationalNumberTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/RationalNumberTests.cs new file mode 100644 index 00000000000..8776b97811f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/RationalNumberTests.cs @@ -0,0 +1,55 @@ +// 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.Extensions.AI.Evaluation.NLP.Common; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +public class RationalNumberTests +{ + [Fact] + public void Constructor_StoresNumeratorAndDenominator() + { + var r = new RationalNumber(3, 4); + Assert.Equal(3, r.Numerator); + Assert.Equal(4, r.Denominator); + } + + [Fact] + public void Constructor_ThrowsOnZeroDenominator() + { + Assert.Throws(() => new RationalNumber(1, 0)); + } + + [Theory] + [InlineData(1, 2, 0.5)] + [InlineData(-3, 4, -0.75)] + [InlineData(0, 5, 0.0)] + public void ToDouble_ReturnsExpected(int num, int denom, double expected) + { + var r = new RationalNumber(num, denom); + Assert.Equal(expected, r.ToDouble(), 6); + } + + [Fact] + public void ToDebugString_FormatsCorrectly() + { + var r = new RationalNumber(7, 9); + Assert.Equal("7/9", r.ToDebugString()); + } + + [Fact] + public void Equals_And_HashCode_WorkCorrectly() + { + var a = new RationalNumber(2, 3); + var b = new RationalNumber(2, 3); + var c = new RationalNumber(3, 2); + Assert.True(a.Equals(b)); + Assert.True(a.Equals((object)b)); + Assert.False(a.Equals(c)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.NotEqual(a.GetHashCode(), c.GetHashCode()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs new file mode 100644 index 00000000000..3451a6c38c9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +#pragma warning disable AIEVAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +public class SimpleTokenizerTests +{ + [Theory] + [InlineData(" $41.23 ", new[] { "$", "41.23" })] + [InlineData("word", new[] { "WORD" })] + [InlineData("word1 word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1,word2", new[] { "WORD1", ",", "WORD2" })] + [InlineData("word1.word2", new[] { "WORD1", ".", "WORD2" })] + [InlineData("word1!word2?", new[] { "WORD1", "!", "WORD2", "?" })] + [InlineData("word1-word2", new[] { "WORD1", "-", "WORD2" })] + [InlineData("word1 - word2", new[] { "WORD1", "-", "WORD2" })] + [InlineData("word1-\n word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1-\r\n word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1-\r\nword2", new[] { "WORD1WORD2" })] + [InlineData("word1-\nword2", new[] { "WORD1WORD2" })] + [InlineData("word1\nword2", new[] { "WORD1", "WORD2" })] + [InlineData("word1 \n word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1\r\nword2", new[] { "WORD1", "WORD2" })] + [InlineData("word1 \r\n word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1\tword2", new[] { "WORD1", "WORD2" })] + [InlineData("It is a guide to action that ensures that the military will forever heed Party commands.", + new[] { "IT", "IS", "A", "GUIDE", "TO", "ACTION", "THAT", "ENSURES", "THAT", "THE", "MILITARY", "WILL", "FOREVER", "HEED", "PARTY", "COMMANDS", "." })] + [InlineData("Good muffins cost $3.88 (roughly 3,36 euros)\nin New York. Please buy me\ntwo of them.\nThanks.", + new[] { "GOOD", "MUFFINS", "COST", "$", "3.88", "(", "ROUGHLY", "3,36", "EUROS", ")", "IN", "NEW", "YORK", ".", "PLEASE", "BUY", "ME", "TWO", "OF", "THEM", ".", "THANKS", "." })] + [InlineData("", new string[0])] + [InlineData(" This is a test.", new[] { "THIS", "IS", "A", "TEST", "." })] + [InlineData("Hello, world! How's it going?", new[] { "HELLO", ",", "WORLD", "!", "HOW", "'", "S", "IT", "GOING", "?" })] + [InlineData(""Quotes" and & symbols < > '", new[] { "\"", "QUOTES", "\"", "AND", "&", "SYMBOLS", "<", ">", "'" })] + [InlineData("-\nThis is a test.", new[] { "THIS", "IS", "A", "TEST", "." })] + public void Tokenize_Cases(string input, string[] expected) + { + var result = SimpleWordTokenizer.WordTokenize(input); + Assert.Equal(expected, result); + } + + [Fact] + public void HandlesMultipleSpacesAndEmptyEntries() + { + var input = " word1 word2 word3 "; + var expected = new[] { "WORD1", "WORD2", "WORD3" }; + var result = SimpleWordTokenizer.WordTokenize(input); + Assert.Equal(expected, result); + } + + [Fact] + public void HandlesUnicodeSymbolsAndPunctuation() + { + var input = "word1 © word2 ™ word3 — word4"; + var expected = new[] { "WORD1", "©", "WORD2", "™", "WORD3", "—", "WORD4" }; + var result = SimpleWordTokenizer.WordTokenize(input); + Assert.Equal(expected, result); + } + + [Fact] + public void HandlesHtmlEntities() + { + var input = ""Hello" & Goodbye <test> '"; + var expected = new[] { "\"", "HELLO", "\"", "&", "GOODBYE", "<", "TEST", ">", "'" }; + var result = SimpleWordTokenizer.WordTokenize(input); + Assert.Equal(expected, result); + } +} From 9b94d9d4c494d4b9e89729814121ae91111525a6 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 05:47:29 +0000 Subject: [PATCH 158/472] Update dependencies from https://github.com/dotnet/arcade build 20250625.4 (#6553) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- eng/common/core-templates/job/job.yml | 4 ++++ eng/common/internal/NuGet.config | 3 +++ global.json | 8 ++++---- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7d902490fb5..3a746194f1b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - 0d52a8b262d35fa2fde84e398cb2e791b8454bd2 + 13b20849f8294593bf150a801cab639397e6c29d - + https://github.com/dotnet/arcade - 0d52a8b262d35fa2fde84e398cb2e791b8454bd2 + 13b20849f8294593bf150a801cab639397e6c29d - + https://github.com/dotnet/arcade - 0d52a8b262d35fa2fde84e398cb2e791b8454bd2 + 13b20849f8294593bf150a801cab639397e6c29d diff --git a/eng/Versions.props b/eng/Versions.props index 8667691bdd6..03fa0e24fd4 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,7 +84,7 @@ 9.0.6 - 9.0.0-beta.25302.2 + 9.0.0-beta.25325.4 diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index ba53ebfbd51..abe80a2a0e0 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -134,6 +134,10 @@ jobs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' diff --git a/eng/common/internal/NuGet.config b/eng/common/internal/NuGet.config index 19d3d311b16..f70261ed689 100644 --- a/eng/common/internal/NuGet.config +++ b/eng/common/internal/NuGet.config @@ -4,4 +4,7 @@ + + + diff --git a/global.json b/global.json index 84536c55f54..70681bc9744 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "9.0.106" + "version": "9.0.107" }, "tools": { - "dotnet": "9.0.106", + "dotnet": "9.0.107", "runtimes": { "dotnet": [ "8.0.0", @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25302.2", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25302.2" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25325.4", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25325.4" } } From 8148872125e9135808ed32561fa733354144694e Mon Sep 17 00:00:00 2001 From: Julian Mathias Kock <119596298+juliankock@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:53:30 +0200 Subject: [PATCH 159/472] #5962 Change to early return of OS instead of throwing (#5963) --- .../README.md | 6 ++- ...ceMonitoringServiceCollectionExtensions.cs | 16 +++--- .../ResourceMonitoringExtensionsTests.cs | 51 ++++++++++++++++++- .../XUnit/OSSkipConditionAttribute.cs | 2 +- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/README.md b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/README.md index ae20ab0297e..41f91b7df44 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/README.md +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/README.md @@ -1,6 +1,10 @@ # Microsoft.Extensions.Diagnostics.ResourceMonitoring -Measures and reports processor and memory usage. This library utilizes control groups (cgroups) in Linux to monitor system resources. Both cgroups v1 and v2 are supported. +Measures and reports processor and memory usage. To monitor system resources, this library: + +- Utilizes control groups (cgroups) in Linux. Both cgroups v1 and v2 are supported. +- Utilized Job Objects in Windows. +- Mac OS is not supported. ## Install the package diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 541984db78d..c368e2bff91 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -60,19 +60,21 @@ public static IServiceCollection AddResourceMonitoring( return services.AddResourceMonitoringInternal(configure); } - // can't easily test the exception throwing case - [ExcludeFromCodeCoverage] private static IServiceCollection AddResourceMonitoringInternal( this IServiceCollection services, Action configure) { - var builder = new ResourceMonitorBuilder(services); - _ = services.AddMetrics(); - + var builder = new ResourceMonitorBuilder(services); #if NETFRAMEWORK _ = builder.AddWindowsProvider(); #else + bool isSupportedOs = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); + if (!isSupportedOs) + { + return services; + } + if (OperatingSystem.IsWindows()) { _ = builder.AddWindowsProvider(); @@ -81,10 +83,6 @@ private static IServiceCollection AddResourceMonitoringInternal( { _ = builder.AddLinuxProvider(); } - else - { - throw new PlatformNotSupportedException(); - } #endif configure.Invoke(builder); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringExtensionsTests.cs index 995afb65c62..875fbb67158 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringExtensionsTests.cs @@ -16,9 +16,9 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; -[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] public sealed class ResourceMonitoringExtensionsTests { + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void Throw_Null_When_Registration_Ingredients_Null() { @@ -30,6 +30,7 @@ public void Throw_Null_When_Registration_Ingredients_Null() Assert.Throws(() => services.AddResourceMonitoring((b) => b.ConfigureMonitor((Action)null!))); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void AddsResourceMonitoringService_ToServicesCollection() { @@ -50,6 +51,7 @@ public void AddsResourceMonitoringService_ToServicesCollection() Assert.IsAssignableFrom(trackerService); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void AddsResourceMonitoringService_ToServicesCollection_NoArgs() { @@ -66,6 +68,7 @@ public void AddsResourceMonitoringService_ToServicesCollection_NoArgs() Assert.IsAssignableFrom(trackerService); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void AddsResourceMonitoringService_AsHostedService() { @@ -87,6 +90,7 @@ public void AddsResourceMonitoringService_AsHostedService() Assert.IsAssignableFrom(trackerService); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void ConfigureResourceUtilization_InitializeTrackerProperly() { @@ -113,6 +117,7 @@ public void ConfigureResourceUtilization_InitializeTrackerProperly() Assert.NotNull(publisher); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void ConfigureMonitor_GivenOptionsDelegate_InitializeTrackerWithOptionsProperly() { @@ -141,6 +146,7 @@ public void ConfigureMonitor_GivenOptionsDelegate_InitializeTrackerWithOptionsPr Assert.Equal(TimeSpan.FromSeconds(CalculationPeriodValue), options!.Value.PublishingWindow); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void ConfigureMonitor_GivenIConfigurationSection_InitializeTrackerWithOptionsProperly() { @@ -182,6 +188,7 @@ public void ConfigureMonitor_GivenIConfigurationSection_InitializeTrackerWithOpt Assert.Equal(TimeSpan.FromSeconds(CalculationPeriod), options!.Value.PublishingWindow); } + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] [ConditionalFact] public void Registering_Resource_Utilization_Adds_Only_One_Object_Of_Type_ResourceUtilizationService_To_DI_Container() { @@ -204,4 +211,46 @@ public void Registering_Resource_Utilization_Adds_Only_One_Object_Of_Type_Resour Assert.IsAssignableFrom(background); Assert.Same(tracker as ResourceMonitorService, background as ResourceMonitorService); } + + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.Windows, SkipReason = "For MacOs only.")] + [ConditionalFact] + public void AddResourceMonitoringInternal_WhenMacOs_ReturnsSameServiceCollection() + { + var services = new ServiceCollection(); + + // Act + IServiceCollection result = services.AddResourceMonitoring(); + + // Assert + Assert.Same(services, result); + Assert.DoesNotContain(services, s => s.ServiceType == typeof(ISnapshotProvider)); + } + + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] + [ConditionalFact] + public void AddResourceMonitoring_AddsISnapshotProvider() + { + var services = new ServiceCollection(); + + // Act + IServiceCollection result = services.AddResourceMonitoring(); + + // Assert + Assert.Same(services, result); + Assert.Contains(services, s => s.ServiceType == typeof(ISnapshotProvider)); + } + + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Not supported on MacOs.")] + [ConditionalFact] + public void AddResourceMonitoringInternal_CallsConfigureDelegate() + { + var services = new ServiceCollection(); + bool delegateCalled = false; + + // Act + services.AddResourceMonitoring(_ => delegateCalled = true); + + // Assert + Assert.True(delegateCalled); + } } diff --git a/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs b/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs index 143cd7005ad..586b53d3fcb 100644 --- a/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs +++ b/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs @@ -60,7 +60,7 @@ private static OperatingSystems GetCurrentOS() throw new PlatformNotSupportedException(); #else - // RuntimeInformation API is only avaialble in .NET Framework 4.7.1+ + // RuntimeInformation API is only available in .NET Framework 4.7.1+ // .NET Framework 4.7 and below can only run on Windows. return OperatingSystems.Windows; #endif From bb47e1388e0e624be35cb8af7e7de2b5d65b143d Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:23:41 +0200 Subject: [PATCH 160/472] Mark log sampling & buffering API as stable (#6534) --- ...IncomingRequestLoggingBuilderExtensions.cs | 3 - .../PerRequestLogBufferingOptions.cs | 3 - ...t.AspNetCore.Diagnostics.Middleware.csproj | 3 +- ...oft.AspNetCore.Diagnostics.Middleware.json | 52 +++++- .../Buffering/LogBuffer.cs | 4 - ...t.Extensions.Telemetry.Abstractions.csproj | 2 + ...oft.Extensions.Telemetry.Abstractions.json | 58 ++++++- .../Sampling/LoggingSampler.cs | 3 - .../GlobalBufferLoggingBuilderExtensions.cs | 3 - .../Buffering/GlobalLogBufferingOptions.cs | 3 - .../Buffering/LogBufferingFilterRule.cs | 3 - .../Microsoft.Extensions.Telemetry.csproj | 2 + .../Microsoft.Extensions.Telemetry.json | 158 +++++++++++++++++- .../RandomProbabilisticSamplerFilterRule.cs | 3 - .../RandomProbabilisticSamplerOptions.cs | 3 - .../SamplingLoggerBuilderExtensions.cs | 2 - 16 files changed, 271 insertions(+), 34 deletions(-) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs index 41664d466ce..840fc80c6cd 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs @@ -3,7 +3,6 @@ #if NET9_0_OR_GREATER using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Diagnostics.Buffering; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -11,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Options; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging; @@ -19,7 +17,6 @@ namespace Microsoft.Extensions.Logging; /// /// Lets you register per incoming request log buffering in a dependency injection container. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public static class PerIncomingRequestLoggingBuilderExtensions { /// diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs index 0b281f06edb..f675466ebe5 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs @@ -5,17 +5,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Shared.Data.Validation; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.AspNetCore.Diagnostics.Buffering; /// /// The options for log buffering per each incoming request. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public class PerRequestLogBufferingOptions { private const int DefaultPerRequestBufferSizeInBytes = 500 * 1024 * 1024; // 500 MB. diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj index 11f769c026a..15e0e9d9225 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj @@ -4,13 +4,14 @@ ASP.NET Core middleware for collecting high-quality telemetry. $(PackageTags);aspnetcore Telemetry + + $(NoWarn);LA0006 $(NetCoreTargetFrameworks) true true - true true false false diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.json b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.json index e76e745c5f1..f445bb581f5 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.json +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.AspNetCore.Diagnostics.Middleware, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.AspNetCore.Diagnostics.Middleware, Version=9.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "static class Microsoft.Extensions.DependencyInjection.HttpLoggingServiceCollectionExtensions", @@ -122,6 +122,10 @@ "Member": "System.Collections.Generic.ISet Microsoft.AspNetCore.Diagnostics.Logging.LoggingRedactionOptions.ExcludePathStartsWith { get; set; }", "Stage": "Experimental" }, + { + "Member": "bool Microsoft.AspNetCore.Diagnostics.Logging.LoggingRedactionOptions.IncludeUnmatchedRoutes { get; set; }", + "Stage": "Experimental" + }, { "Member": "System.Collections.Generic.IDictionary Microsoft.AspNetCore.Diagnostics.Logging.LoggingRedactionOptions.RequestHeadersDataClasses { get; set; }", "Stage": "Experimental" @@ -144,6 +148,52 @@ } ] }, + { + "Type": "static class Microsoft.Extensions.Logging.PerIncomingRequestLoggingBuilderExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.PerIncomingRequestLoggingBuilderExtensions.AddPerIncomingRequestBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.PerIncomingRequestLoggingBuilderExtensions.AddPerIncomingRequestBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.PerIncomingRequestLoggingBuilderExtensions.AddPerIncomingRequestBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.LogLevel? logLevel = null);", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions.PerRequestLogBufferingOptions();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.TimeSpan Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions.AutoFlushDuration { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions.MaxLogRecordSizeInBytes { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions.MaxPerRequestBufferSizeInBytes { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList Microsoft.AspNetCore.Diagnostics.Buffering.PerRequestLogBufferingOptions.Rules { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "static class Microsoft.AspNetCore.Diagnostics.Latency.RequestCheckpointConstants", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Buffering/LogBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Buffering/LogBuffer.cs index 4853615c228..4259c90f645 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Buffering/LogBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Buffering/LogBuffer.cs @@ -1,17 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if NET9_0_OR_GREATER - -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Buffers logs into circular buffers and drops them after some time if not flushed. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] #pragma warning disable S1694 // An abstract class should have both abstract and concrete methods public abstract class LogBuffer #pragma warning restore S1694 // An abstract class should have both abstract and concrete methods diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj index 08a379be0e6..816ad679585 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj @@ -3,6 +3,8 @@ Microsoft.Extensions.Telemetry Common abstractions for high-level telemetry primitives. Telemetry + + $(NoWarn);LA0006 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.json b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.json index 8f52b86fcd1..1ddd511257a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.Telemetry.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.Telemetry.Abstractions, Version=9.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "readonly struct Microsoft.Extensions.Diagnostics.Latency.Checkpoint : System.IEquatable", @@ -179,6 +179,16 @@ } ] }, + { + "Type": "abstract class Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBuffer : Microsoft.Extensions.Diagnostics.Buffering.LogBuffer", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBuffer.GlobalLogBuffer();", + "Stage": "Stable" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.Diagnostics.Metrics.HistogramAttribute : System.Attribute", "Stage": "Stable", @@ -488,6 +498,24 @@ } ] }, + { + "Type": "abstract class Microsoft.Extensions.Diagnostics.Buffering.LogBuffer", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Buffering.LogBuffer.LogBuffer();", + "Stage": "Stable" + }, + { + "Member": "abstract void Microsoft.Extensions.Diagnostics.Buffering.LogBuffer.Flush();", + "Stage": "Stable" + }, + { + "Member": "abstract bool Microsoft.Extensions.Diagnostics.Buffering.LogBuffer.TryEnqueue(Microsoft.Extensions.Logging.Abstractions.IBufferedLogger bufferedLogger, in Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry);", + "Stage": "Stable" + } + ] + }, { "Type": "static class Microsoft.Extensions.Logging.LoggerMessageHelper", "Stage": "Stable", @@ -600,6 +628,20 @@ } ] }, + { + "Type": "abstract class Microsoft.Extensions.Logging.LoggingSampler", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Logging.LoggingSampler.LoggingSampler();", + "Stage": "Stable" + }, + { + "Member": "abstract bool Microsoft.Extensions.Logging.LoggingSampler.ShouldSample(in Microsoft.Extensions.Logging.Abstractions.LogEntry logEntry);", + "Stage": "Stable" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.Logging.LogPropertiesAttribute : System.Attribute", "Stage": "Stable", @@ -617,6 +659,10 @@ { "Member": "bool Microsoft.Extensions.Logging.LogPropertiesAttribute.SkipNullProperties { get; set; }", "Stage": "Stable" + }, + { + "Member": "bool Microsoft.Extensions.Logging.LogPropertiesAttribute.Transitive { get; set; }", + "Stage": "Experimental" } ] }, @@ -708,6 +754,16 @@ } ] }, + { + "Type": "abstract class Microsoft.Extensions.Diagnostics.Buffering.PerRequestLogBuffer : Microsoft.Extensions.Diagnostics.Buffering.LogBuffer", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Buffering.PerRequestLogBuffer.PerRequestLogBuffer();", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.Http.Diagnostics.RequestMetadata", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Sampling/LoggingSampler.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Sampling/LoggingSampler.cs index 3dcd0b4cd12..3e227f9c17d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Sampling/LoggingSampler.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Sampling/LoggingSampler.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Logging; @@ -11,7 +9,6 @@ namespace Microsoft.Extensions.Logging; /// Controls the number of samples of log records collected and sent to the backend. /// #pragma warning disable S1694 // An abstract class should have both abstract and concrete methods -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public abstract class LoggingSampler #pragma warning restore S1694 // An abstract class should have both abstract and concrete methods { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs index 150f82b7414..d97d8fa5f0a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs @@ -3,13 +3,11 @@ #if NET9_0_OR_GREATER using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Options; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging; @@ -17,7 +15,6 @@ namespace Microsoft.Extensions.Logging; /// /// Lets you register log buffering in a dependency injection container. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public static class GlobalBufferLoggingBuilderExtensions { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs index 060d823b35d..b12f9ae33cf 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs @@ -5,16 +5,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Data.Validation; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// The options for global log buffering. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public class GlobalLogBufferingOptions { private const int DefaultMaxBufferSizeInBytes = 500 * 1024 * 1024; // 500 MB. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs index 6aa0a0109fa..b6725141bfe 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs @@ -3,9 +3,7 @@ #if NET9_0_OR_GREATER using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -17,7 +15,6 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// If a log entry does not match any rule, it will be emitted normally. /// If the buffer size limit is reached, the oldest buffered log entries will be dropped (not emitted!) to make room for new ones. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public class LogBufferingFilterRule { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index 81d379cd381..9dc4a11ca6c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -3,6 +3,8 @@ Microsoft.Extensions.Diagnostics Provides canonical implementations of telemetry abstractions. Telemetry + + $(NoWarn);LA0006 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.json b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.json index 89b08ce9676..a9e74aa0c2b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.json +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.Telemetry, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.Telemetry, Version=9.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "static class Microsoft.Extensions.DependencyInjection.ApplicationEnricherServiceCollectionExtensions", @@ -79,6 +79,52 @@ } ] }, + { + "Type": "static class Microsoft.Extensions.Logging.GlobalBufferLoggingBuilderExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.GlobalBufferLoggingBuilderExtensions.AddGlobalBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.GlobalBufferLoggingBuilderExtensions.AddGlobalBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.GlobalBufferLoggingBuilderExtensions.AddGlobalBuffer(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.LogLevel? logLevel = null);", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions.GlobalLogBufferingOptions();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.TimeSpan Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions.AutoFlushDuration { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions.MaxBufferSizeInBytes { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions.MaxLogRecordSizeInBytes { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.Diagnostics.Buffering.GlobalLogBufferingOptions.Rules { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "static class Microsoft.Extensions.DependencyInjection.LatencyConsoleExtensions", "Stage": "Stable", @@ -155,6 +201,38 @@ } ] }, + { + "Type": "class Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.LogBufferingFilterRule(string? categoryName = null, Microsoft.Extensions.Logging.LogLevel? logLevel = null, int? eventId = null, string? eventName = null, System.Collections.Generic.IReadOnlyList>? attributes = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyList>? Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.Attributes { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.CategoryName { get; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.EventId { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.EventName { get; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.Logging.LogLevel? Microsoft.Extensions.Diagnostics.Buffering.LogBufferingFilterRule.LogLevel { get; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.Logging.LoggerEnrichmentOptions", "Stage": "Stable", @@ -294,6 +372,84 @@ "Stage": "Stable" } ] + }, + { + "Type": "class Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule : Microsoft.Extensions.Diagnostics.Sampling.ILogSamplingFilterRule", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.RandomProbabilisticSamplerFilterRule(double probability, string? categoryName = null, Microsoft.Extensions.Logging.LogLevel? logLevel = null, int? eventId = null, string? eventName = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string? Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.CategoryName { get; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.EventId { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.EventName { get; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.Logging.LogLevel? Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.LogLevel { get; }", + "Stage": "Stable" + }, + { + "Member": "double Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerFilterRule.Probability { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerOptions", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerOptions.RandomProbabilisticSamplerOptions();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.Diagnostics.Sampling.RandomProbabilisticSamplerOptions.Rules { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddRandomProbabilisticSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddRandomProbabilisticSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action configure);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddRandomProbabilisticSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder, double probability, Microsoft.Extensions.Logging.LogLevel? level = null);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder, Microsoft.Extensions.Logging.LoggingSampler sampler);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.Logging.ILoggingBuilder Microsoft.Extensions.Logging.SamplingLoggerBuilderExtensions.AddTraceBasedSampler(this Microsoft.Extensions.Logging.ILoggingBuilder builder);", + "Stage": "Stable" + } + ] } ] } \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerFilterRule.cs index aa0454ff739..b55b34f0da5 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerFilterRule.cs @@ -2,16 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Sampling; /// /// Defines a rule used to filter log messages for purposes of sampling. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public class RandomProbabilisticSamplerFilterRule : ILogSamplingFilterRule { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerOptions.cs index 5c0cd89c675..4808eab9fc7 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSamplerOptions.cs @@ -3,16 +3,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Options; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Sampling; /// /// The options for the Random Probabilistic sampler. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public class RandomProbabilisticSamplerOptions { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/SamplingLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/SamplingLoggerBuilderExtensions.cs index 489b4462482..dfd9ad2b997 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/SamplingLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/SamplingLoggerBuilderExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.Sampling; using Microsoft.Extensions.Options; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging; @@ -16,7 +15,6 @@ namespace Microsoft.Extensions.Logging; /// /// Extensions for configuring logging sampling. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public static class SamplingLoggerBuilderExtensions { /// From 5658886dd69fd539fe2b4f9ab44abd5dcd23259d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 1 Jul 2025 07:10:13 -0400 Subject: [PATCH 161/472] Update GenAI otel impl for v1.35 (#6557) --- .../AzureAIInferenceChatClient.cs | 2 +- .../AzureAIInferenceEmbeddingGenerator.cs | 2 +- .../AzureAIInferenceImageEmbeddingGenerator.cs | 2 +- .../ChatCompletion/OpenTelemetryChatClient.cs | 2 +- .../Embeddings/OpenTelemetryEmbeddingGenerator.cs | 2 +- .../AzureAIInferenceChatClientTests.cs | 2 +- .../AzureAIInferenceEmbeddingGeneratorTests.cs | 2 +- .../AzureAIInferenceImageEmbeddingGeneratorTests.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 0f8a8a90008..bb23e1de489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -65,7 +65,7 @@ public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, s var providerUrl = typeof(ChatCompletionsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(chatCompletionsClient) as Uri; - _metadata = new ChatClientMetadata("az.ai.inference", providerUrl, defaultModelId); + _metadata = new ChatClientMetadata("azure.ai.inference", providerUrl, defaultModelId); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs index 46a8a204e80..95cea4e2a3b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs @@ -69,7 +69,7 @@ public AzureAIInferenceEmbeddingGenerator( var providerUrl = typeof(EmbeddingsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(embeddingsClient) as Uri; - _metadata = new EmbeddingGeneratorMetadata("az.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); + _metadata = new EmbeddingGeneratorMetadata("azure.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs index 1604509a410..91222722b2a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs @@ -65,7 +65,7 @@ public AzureAIInferenceImageEmbeddingGenerator( var providerUrl = typeof(ImageEmbeddingsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(imageEmbeddingsClient) as Uri; - _metadata = new EmbeddingGeneratorMetadata("az.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); + _metadata = new EmbeddingGeneratorMetadata("azure.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 569b8b40d02..d66266c39bc 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.34, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.35, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed partial class OpenTelemetryChatClient : DelegatingChatClient diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 82f617d5bf6..2eeb32891ea 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.34, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.35, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// /// The type of input used to produce embeddings. diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 489ee13f987..8b9f3d50cc2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -56,7 +56,7 @@ public void AsIChatClient_ProducesExpectedMetadata() IChatClient chatClient = client.AsIChatClient(model); var metadata = chatClient.GetService(); - Assert.Equal("az.ai.inference", metadata?.ProviderName); + Assert.Equal("azure.ai.inference", metadata?.ProviderName); Assert.Equal(endpoint, metadata?.ProviderUri); Assert.Equal(model, metadata?.DefaultModelId); } diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs index 3b1a7dda9d1..31e9980a330 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs @@ -38,7 +38,7 @@ public void AsIEmbeddingGenerator_AzureAIClient_ProducesExpectedMetadata() IEmbeddingGenerator> embeddingGenerator = client.AsIEmbeddingGenerator(model); var metadata = embeddingGenerator.GetService(); - Assert.Equal("az.ai.inference", metadata?.ProviderName); + Assert.Equal("azure.ai.inference", metadata?.ProviderName); Assert.Equal(endpoint, metadata?.ProviderUri); Assert.Equal(model, metadata?.DefaultModelId); } diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs index 0e2a3b685af..7ceefe947f3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs @@ -38,7 +38,7 @@ public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata() IEmbeddingGenerator> embeddingGenerator = client.AsIEmbeddingGenerator(model); var metadata = embeddingGenerator.GetService(); - Assert.Equal("az.ai.inference", metadata?.ProviderName); + Assert.Equal("azure.ai.inference", metadata?.ProviderName); Assert.Equal(endpoint, metadata?.ProviderUri); Assert.Equal(model, metadata?.DefaultModelId); } From a8dde6b13ddb3f212e38439c75f3ca9ddeaff6ae Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Tue, 1 Jul 2025 12:03:34 -0400 Subject: [PATCH 162/472] Implement GLEU and F1 NLP evaluators (#6555) * Add GLEU and F1 evaluators. * Update READMEs * Use arrays in place of IEnumerable for internal processing. * Review updates --- .../README.md | 2 +- .../BLEUEvaluator.cs | 9 +- .../BLEUEvaluatorContext.cs | 14 +- .../Common/BLEUAlgorithm.cs | 29 +++-- .../Common/F1Algorithm.cs | 44 +++++++ .../Common/GLEUAlgorithm.cs | 66 ++++++++++ .../Common/MatchCounter.cs | 21 ++- .../Common/NGramExtensions.cs | 58 ++++++++- ...on.cs => ScoreInterpretationExtensions.cs} | 4 +- .../F1Evaluator.cs | 88 +++++++++++++ .../F1EvaluatorContext.cs | 49 +++++++ .../GLEUEvaluator.cs | 97 ++++++++++++++ .../GLEUEvaluatorContext.cs | 62 +++++++++ ...rosoft.Extensions.AI.Evaluation.NLP.csproj | 1 + .../README.md | 2 +- .../IntentResolutionEvaluatorContext.cs | 7 +- .../README.md | 2 +- .../RetrievalEvaluatorContext.cs | 8 +- .../TaskAdherenceEvaluatorContext.cs | 7 +- .../ToolCallAccuracyEvaluatorContext.cs | 7 +- .../README.md | 2 +- .../CSharp/README.md | 4 + .../README.md | 2 +- .../README.md | 2 +- .../BLEUAlgorithmTests.cs | 87 +++++++------ .../F1EvaluatorTests.cs | 93 ++++++++++++++ .../GLEUAlgorithmTests.cs | 120 ++++++++++++++++++ .../GLEUEvaluatorTests.cs | 113 +++++++++++++++++ .../MatchCounterTests.cs | 18 +++ .../NGramTests.cs | 57 +++++++-- 30 files changed, 966 insertions(+), 109 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/F1Algorithm.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/GLEUAlgorithm.cs rename src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/{NLPScoreInterpretation.cs => ScoreInterpretationExtensions.cs} (90%) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/F1EvaluatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUAlgorithmTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUEvaluatorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md index 580facd6294..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs index 8ce43d48e52..bfe1b9af589 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -1,6 +1,7 @@ // 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.Globalization; using System.Linq; @@ -77,10 +78,10 @@ public ValueTask EvaluateAsync( return new ValueTask(result); } - var (score, duration) = TimingHelper.ExecuteWithTiming(() => + (double score, TimeSpan duration) = TimingHelper.ExecuteWithTiming(() => { - var references = context.References.Select(reference => SimpleWordTokenizer.WordTokenize(reference)); - var hypothesis = SimpleWordTokenizer.WordTokenize(modelResponse.Text); + string[][] references = context.References.Select(reference => SimpleWordTokenizer.WordTokenize(reference).ToArray()).ToArray(); + string[] hypothesis = SimpleWordTokenizer.WordTokenize(modelResponse.Text).ToArray(); return BLEUAlgorithm.SentenceBLEU(references, hypothesis, BLEUAlgorithm.DefaultBLEUWeights, SmoothingFunction.Method4); }); @@ -88,7 +89,7 @@ public ValueTask EvaluateAsync( string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); - metric.Interpretation = NLPScoreInterpretation.Interpret(metric); + metric.Interpretation = metric.Interpret(); return new ValueTask(result); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs index 320b20e9116..4085355db92 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs @@ -24,10 +24,10 @@ public sealed class BLEUEvaluatorContext : EvaluationContext /// Gets the unique that is used for /// . /// - public static string BLEUContextName => "BLEU Context"; + public static string ReferencesContextName => "References (BLEU)"; /// - /// Gets the reference responses against which the provided model response will be scored. + /// Gets the references against which the provided response will be scored. /// /// /// The measures the degree to which the response being evaluated is similar to @@ -41,8 +41,8 @@ public sealed class BLEUEvaluatorContext : EvaluationContext /// /// The reference responses against which the response that is being evaluated is compared. /// - public BLEUEvaluatorContext(params string[] references) - : this(references as IEnumerable) + public BLEUEvaluatorContext(IEnumerable references) + : this(references.ToArray()) { } @@ -52,11 +52,11 @@ public BLEUEvaluatorContext(params string[] references) /// /// The reference responses against which the response that is being evaluated is compared. /// - public BLEUEvaluatorContext(IEnumerable references) + public BLEUEvaluatorContext(params string[] references) : base( - name: BLEUContextName, + name: ReferencesContextName, contents: [.. references.Select(c => new TextContent(c))]) { - References = [.. references]; + References = references; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs index c7420d0be7a..b5ffb0ba3d2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/BLEUAlgorithm.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; /// internal static class BLEUAlgorithm { - internal static int ClosestRefLength(IEnumerable> references, int hypLength) + internal static int ClosestRefLength(string[][] references, int hypLength) { if (!references.Any()) { @@ -27,7 +27,7 @@ internal static int ClosestRefLength(IEnumerable> references int smallestDiff = int.MaxValue; foreach (var reference in references) { - int refLength = reference.Count(); + int refLength = reference.Length; int diff = Math.Abs(refLength - hypLength); if (diff < smallestDiff || (diff == smallestDiff && refLength < closestRefLength)) @@ -55,27 +55,27 @@ internal static double BrevityPenalty(int closestRefLength, int hypLength) return Math.Exp(1 - ((double)closestRefLength / hypLength)); } - internal static RationalNumber ModifiedPrecision(IEnumerable> references, IEnumerable hypothesis, int n = 1) + internal static RationalNumber ModifiedPrecision(string[][] references, string[] hypothesis, int n = 1) { if (n <= 0) { Throw.ArgumentOutOfRangeException(nameof(n), $"`{nameof(n)}` must be greater than zero."); } - if (!references.Any() || !hypothesis.Any()) + if (references.Length == 0 || hypothesis.Length == 0) { return RationalNumber.Zero; } - var hyp = hypothesis.CreateNGrams(n); - var hypCounts = new MatchCounter>(hyp); + List> hypGrams = hypothesis.CreateNGrams(n); + MatchCounter> hypCounts = new(hypGrams); Dictionary, int> maxCounts = []; foreach (var rf in references) { - IEnumerable> refGrams = rf.CreateNGrams(n); - var refCounts = new MatchCounter>(refGrams); + List> refGrams = rf.CreateNGrams(n); + MatchCounter> refCounts = new(refGrams); foreach (var ct in refCounts) { @@ -123,25 +123,28 @@ internal static double[] EqualWeights(int n) } double[] weights = new double[n]; +#if NET8_0_OR_GREATER + Array.Fill(weights, 1.0 / n); +#else for (int i = 0; i < n; i++) { weights[i] = 1.0 / n; } - +#endif return weights; } internal static readonly double[] DefaultBLEUWeights = EqualWeights(4); - internal static double SentenceBLEU(IEnumerable> references, IEnumerable hypothesis, + internal static double SentenceBLEU(string[][] references, string[] hypothesis, double[]? weights = null, Func? smoothingFunction = null) { - if (references == null || !references.Any()) + if (references == null || references.Length == 0) { Throw.ArgumentNullException(nameof(references), $"'{nameof(references)}' cannot be null or empty."); } - if (hypothesis == null || !hypothesis.Any()) + if (hypothesis == null || hypothesis.Length == 0) { Throw.ArgumentNullException(nameof(hypothesis), $"'{nameof(hypothesis)}' cannot be null or empty."); } @@ -171,7 +174,7 @@ internal static double SentenceBLEU(IEnumerable> references, precisionValues[i] = prec; } - int hypLen = hypothesis.Count(); + int hypLen = hypothesis.Length; int closestRefLength = ClosestRefLength(references, hypLen); double brevityPenalty = BrevityPenalty(closestRefLength, hypLen); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/F1Algorithm.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/F1Algorithm.cs new file mode 100644 index 00000000000..cfc077e11a0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/F1Algorithm.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +/// +/// F1 score for a response is the ratio of the number of shared words between the generated response +/// and the reference response. Python implementation reference +/// https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py. +/// +internal static class F1Algorithm +{ + public static double CalculateF1Score(string[] groundTruth, string[] response) + { + if (groundTruth == null || groundTruth.Length == 0) + { + Throw.ArgumentNullException(nameof(groundTruth), $"'{nameof(groundTruth)}' cannot be null or empty."); + } + + if (response == null || response.Length == 0) + { + Throw.ArgumentNullException(nameof(response), $"'{nameof(response)}' cannot be null or empty."); + } + + MatchCounter referenceTokens = new(groundTruth); + MatchCounter predictionTokens = new(response); + MatchCounter commonTokens = referenceTokens.Intersect(predictionTokens); + int numCommonTokens = commonTokens.Sum(); + + if (numCommonTokens == 0) + { + return 0.0; // F1 score is 0 if there are no common tokens + } + else + { + double precision = (double)numCommonTokens / response.Length; + double recall = (double)numCommonTokens / groundTruth.Length; + double f1 = (2.0 * precision * recall) / (precision + recall); + return f1; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/GLEUAlgorithm.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/GLEUAlgorithm.cs new file mode 100644 index 00000000000..cd25b9beb5d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/GLEUAlgorithm.cs @@ -0,0 +1,66 @@ +// 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 Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; + +/// +/// Google-BLEU (GLEU) algorithm implementation for evaluating the quality of a response. +/// Python implementation reference: https://www.nltk.org/api/nltk.translate.gleu_score.html. +/// +internal static class GLEUAlgorithm +{ + internal static double SentenceGLEU(string[][] references, string[] hypothesis, int minN = 1, int maxN = 4) + { + if (references == null || references.Length == 0) + { + Throw.ArgumentNullException(nameof(references), $"'{nameof(references)}' cannot be null or empty."); + } + + if (hypothesis == null || hypothesis.Length == 0) + { + Throw.ArgumentNullException(nameof(hypothesis), $"'{nameof(hypothesis)}' cannot be null or empty."); + } + + MatchCounter> hypNGrams = new(hypothesis.CreateAllNGrams(minN, maxN)); + int truePosFalsePos = hypNGrams.Sum(); + + List<(int, int)> hypCounts = []; + foreach (var reference in references) + { + MatchCounter> refNGrams = new(reference.CreateAllNGrams(minN, maxN)); + int truePosFalseNeg = refNGrams.Sum(); + + MatchCounter> overlapNGrams = hypNGrams.Intersect(refNGrams); + int truePos = overlapNGrams.Sum(); + + int nAll = Math.Max(truePosFalsePos, truePosFalseNeg); + + if (nAll > 0) + { + hypCounts.Add((truePos, nAll)); + } + } + + int corpusNMatch = 0; + int corpusNAll = 0; + + foreach (var (truePos, nAll) in hypCounts) + { + corpusNMatch += truePos; + corpusNAll += nAll; + } + + if (corpusNAll == 0) + { + return 0.0; + } + else + { + return (double)corpusNMatch / corpusNAll; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs index bbca2252057..b54c67d14a2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/MatchCounter.cs @@ -53,7 +53,26 @@ public void AddRange(IEnumerable items) } } - public string ToDebugString() => string.Concat(_counts.Select(v => $"{v.Key}: {v.Value}, ")); + public MatchCounter Intersect(MatchCounter other) + { + _ = Throw.IfNull(other, nameof(other)); + var intersection = new MatchCounter(); + + (Dictionary smaller, Dictionary larger) = + _counts.Count < other._counts.Count ? (_counts, other._counts) : (other._counts, _counts); + + foreach (var kvp in smaller) + { + if (larger.TryGetValue(kvp.Key, out int otherCount)) + { + intersection._counts[kvp.Key] = Math.Min(kvp.Value, otherCount); + } + } + + return intersection; + } + + public string ToDebugString() => string.Join(",", _counts.Select(v => $"{v.Key}: {v.Value}")); public IEnumerator> GetEnumerator() => _counts.GetEnumerator(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs index 149d3820328..bde63f74c73 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NGramExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; @@ -14,12 +13,16 @@ internal static class NGramExtensions public static NGram CreateNGram(this ReadOnlySpan values) where T : IEquatable => new(values); + internal static List> CreateNGrams(this T[] input, int n) + where T : IEquatable + => CreateNGrams((ReadOnlySpan)input, n); + /// /// Create a sequence of n-grams from the input sequence. /// /// The input sequence of items. /// The size of each n-gram. - internal static IEnumerable> CreateNGrams(this IEnumerable input, int n) + internal static List> CreateNGrams(this ReadOnlySpan input, int n) where T : IEquatable { if (n <= 0) @@ -27,15 +30,56 @@ internal static IEnumerable> CreateNGrams(this IEnumerable input, Throw.ArgumentOutOfRangeException(nameof(n), $"'{nameof(n)}' must be greater than zero."); } - T[] output = [.. input.Take(n)]; + List> nGrams = []; + + ReadOnlySpan next = input.Slice(0, Math.Min(n, input.Length)); - while (output.Length == n) + while (next.Length == n) { - yield return new NGram(output); + nGrams.Add(new NGram(next)); - input = input.Skip(1); - output = [.. input.Take(n)]; + input = input.Slice(1); + next = input.Slice(0, Math.Min(n, input.Length)); } + + return nGrams; } + internal static List> CreateAllNGrams(this T[] input, int minN, int maxN = -1) + where T : IEquatable + => CreateAllNGrams((ReadOnlySpan)input, minN, maxN); + + /// + /// Create a sequence of all n-grams from the input sequence from minN to maxN. + /// + /// The input sequence of items. + /// The minimum size of n-gram. + /// The maximum size of n-gram. If not specified, the default is to include up to length of the input. + internal static List> CreateAllNGrams(this ReadOnlySpan input, int minN, int maxN = -1) + where T : IEquatable + { + _ = Throw.IfLessThanOrEqual(minN, 0, nameof(minN)); + + if (maxN < 0) + { + maxN = input.Length; // Update to use Length instead of Count() + } + else if (maxN < minN) + { + Throw.ArgumentOutOfRangeException(nameof(maxN), $"'{nameof(maxN)}' must be greater than or equal to '{nameof(minN)}'."); + } + + List> nGrams = []; + + for (int i = 0; i <= input.Length - minN; i++) + { + for (int s = minN; s <= maxN && s <= input.Length - i; s++) + { + nGrams.Add(new NGram(input.Slice(i, s))); + } + } + + return nGrams; + } } + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/ScoreInterpretationExtensions.cs similarity index 90% rename from src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs rename to src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/ScoreInterpretationExtensions.cs index 4ef1d08b468..9fe6df64452 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/NLPScoreInterpretation.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/ScoreInterpretationExtensions.cs @@ -3,9 +3,9 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; -internal static class NLPScoreInterpretation +internal static class ScoreInterpretationExtensions { - internal static EvaluationMetricInterpretation Interpret(NumericMetric metric) + internal static EvaluationMetricInterpretation Interpret(this NumericMetric metric) { // Many NLP scores range from 0.0 to 1.0, where: // - 0.0 means no match at all, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs new file mode 100644 index 00000000000..b0806be6d66 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs @@ -0,0 +1,88 @@ +// 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.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// An that evaluates the quality of a response produced by an AI model by comparing +/// it to a reference response using the F1 scoring algorithm. F1 score is the ratio of the number of shared +/// words between the generated response and the reference response. +/// +/// +/// +/// The computes the F1 score of a response ("hypothesis") in relation to a ground-truth reference +/// supplied by . The score is returned in a +/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates a perfect match. +/// By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher is +/// passing and a score below 0.5 is failing. +/// +/// +public sealed class F1Evaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string F1MetricName => "F1"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [F1MetricName]; + + /// + public ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + + var metric = new NumericMetric(F1MetricName); + var result = new EvaluationResult(metric); + + if (string.IsNullOrWhiteSpace(modelResponse.Text)) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error($"The {nameof(modelResponse)} supplied for evaluation was null or empty.")); + + return new ValueTask(result); + } + + if (additionalContext?.OfType().FirstOrDefault() + is not F1EvaluatorContext context) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"A value of type '{nameof(F1EvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + + return new ValueTask(result); + } + + (double score, TimeSpan duration) = TimingHelper.ExecuteWithTiming(() => + { + string[] reference = SimpleWordTokenizer.WordTokenize(context.GroundTruth).ToArray(); + string[] hypothesis = SimpleWordTokenizer.WordTokenize(modelResponse.Text).ToArray(); + return F1Algorithm.CalculateF1Score(reference, hypothesis); + }); + + metric.Value = score; + string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); + metric.AddOrUpdateContext(context); + metric.Interpretation = metric.Interpret(); + + return new ValueTask(result); + } + +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs new file mode 100644 index 00000000000..d6dafcc3c6a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S3604 +// S3604: Member initializer values should not be redundant. +// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary +// constructor syntax. + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// Contextual information that the uses to compute the F1 score for a response. +/// +/// +/// measures the F1 score of a response compared to a reference response that is supplied via +/// . F1 is a metric used to valuate the quality of machine-generated text. It is the ratio +/// of the number of shared words between the generated response and the reference response. +/// +public sealed class F1EvaluatorContext : EvaluationContext +{ + /// + /// Gets the unique that is used for + /// . + /// + public static string GroundTruthContextName => "Ground Truth (F1)"; + + /// + /// Gets the reference response against which the provided response will be scored. + /// + /// + /// The measures the degree to which the response being evaluated is similar to + /// the response supplied via . The metric will be reported as an F1 score. + /// + public string GroundTruth { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The reference response against which the provided response will be scored. + /// + public F1EvaluatorContext(string groundTruth) + : base( + name: GroundTruthContextName, + content: groundTruth) + { + GroundTruth = groundTruth; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs new file mode 100644 index 00000000000..d33ed07f5cb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs @@ -0,0 +1,97 @@ +// 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.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Microsoft.Extensions.AI.Evaluation.Utilities; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// An that evaluates the quality of a response produced by an AI model by comparing +/// it to a reference response using the GLEU (Google-BLEU) algorithm. The GLEU evaluator measures the similarity +/// between the generated response and one or more reference responses using n-gram overlap. +/// +/// +/// +/// The computes the GLEU score of a response ("hypothesis") compared to a reference +/// supplied via . The score is returned in a +/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates a perfect match. +/// By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher is +/// passing and a score below 0.5 is failing. +/// +/// +public sealed class GLEUEvaluator : IEvaluator +{ + /// + /// Gets the of the returned by + /// . + /// + public static string GLEUMetricName => "GLEU"; + + /// + public IReadOnlyCollection EvaluationMetricNames { get; } = [GLEUMetricName]; + + /// + public ValueTask EvaluateAsync( + IEnumerable messages, + ChatResponse modelResponse, + ChatConfiguration? chatConfiguration = null, + IEnumerable? additionalContext = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(modelResponse); + + var metric = new NumericMetric(GLEUMetricName); + var result = new EvaluationResult(metric); + + if (string.IsNullOrWhiteSpace(modelResponse.Text)) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error($"The {nameof(modelResponse)} supplied for evaluation was null or empty.")); + + return new ValueTask(result); + } + + if (additionalContext?.OfType().FirstOrDefault() + is not GLEUEvaluatorContext context) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"A value of type '{nameof(GLEUEvaluatorContext)}' was not found in the '{nameof(additionalContext)}' collection.")); + + return new ValueTask(result); + } + + if (context.References.Count is 0) + { + metric.AddDiagnostics( + EvaluationDiagnostic.Error( + $"Supplied '{nameof(GLEUEvaluatorContext)}' did not contain any '{nameof(GLEUEvaluatorContext.References)}'.")); + + return new ValueTask(result); + } + + (double score, TimeSpan duration) = TimingHelper.ExecuteWithTiming(() => + { + string[][] references = context.References.Select(reference => SimpleWordTokenizer.WordTokenize(reference).ToArray()).ToArray(); + string[] hypothesis = SimpleWordTokenizer.WordTokenize(modelResponse.Text).ToArray(); + return GLEUAlgorithm.SentenceGLEU(references, hypothesis); + }); + + metric.Value = score; + string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); + metric.AddOrUpdateContext(context); + metric.Interpretation = metric.Interpret(); + + return new ValueTask(result); + } + +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs new file mode 100644 index 00000000000..b41b1f80f42 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S3604 +// S3604: Member initializer values should not be redundant. +// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary +// constructor syntax. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.AI.Evaluation.NLP; + +/// +/// Contextual information that the uses to compute the GLEU score for a response. +/// +/// +/// measures the GLEU score of a response compared to one or more reference responses +/// supplied via . GLEU (Google-BLEU) is a metric used to evaluate the quality of machine-generated text. +/// +public sealed class GLEUEvaluatorContext : EvaluationContext +{ + /// + /// Gets the unique that is used for + /// . + /// + public static string ReferencesContextName => "References (GLEU)"; + + /// + /// Gets the reference against which the provided response will be scored. + /// + /// + /// The measures the degree to which the response being evaluated is similar to + /// the response supplied via . The metric will be reported as a GLEU score. + /// + public IReadOnlyList References { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The reference responses against which the response that is being evaluated is compared. + /// + public GLEUEvaluatorContext(IEnumerable references) + : this(references.ToArray()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The reference responses against which the response that is being evaluated is compared. + /// + public GLEUEvaluatorContext(params string[] references) + : base( + name: ReferencesContextName, + contents: [.. references.Select(c => new TextContent(c))]) + { + References = references; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj index 0bab1cf7fb0..12e7cebb957 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj @@ -17,6 +17,7 @@ true + true diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md index 580facd6294..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs index c8dcbc996b7..cf4a9b17004 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Microsoft.Extensions.AI.Evaluation.Quality; @@ -38,7 +39,7 @@ public sealed class IntentResolutionEvaluatorContext : EvaluationContext /// are defined as s. Any other definitions will be ignored. /// /// - public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) + public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) : base(name: IntentResolutionContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; @@ -57,8 +58,8 @@ public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) /// are defined as s. Any other definitions will be ignored. /// /// - public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) - : this(toolDefinitions as IEnumerable) + public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) + : this(toolDefinitions.ToArray()) { } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md index 580facd6294..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluatorContext.cs index 1b3f94bcdf9..50c80f42fa6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluatorContext.cs @@ -41,12 +41,12 @@ public sealed class RetrievalEvaluatorContext : EvaluationContext /// /// The context chunks that were retrieved in response to the user request being evaluated. /// - public RetrievalEvaluatorContext(IEnumerable retrievedContextChunks) + public RetrievalEvaluatorContext(params string[] retrievedContextChunks) : base( name: RetrievedContextChunksContextName, contents: [.. retrievedContextChunks.Select(c => new TextContent(c))]) { - RetrievedContextChunks = [.. retrievedContextChunks]; + RetrievedContextChunks = retrievedContextChunks; } /// @@ -55,8 +55,8 @@ public RetrievalEvaluatorContext(IEnumerable retrievedContextChunks) /// /// The context chunks that were retrieved in response to the user request being evaluated. /// - public RetrievalEvaluatorContext(params string[] retrievedContextChunks) - : this(retrievedContextChunks as IEnumerable) + public RetrievalEvaluatorContext(IEnumerable retrievedContextChunks) + : this(retrievedContextChunks.ToArray()) { } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs index 3d54ed74dab..4557f2536d2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Microsoft.Extensions.AI.Evaluation.Quality; @@ -39,7 +40,7 @@ public sealed class TaskAdherenceEvaluatorContext : EvaluationContext /// are defined as s. Any other definitions will be ignored. /// /// - public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) + public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) : base(name: TaskAdherenceContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; @@ -58,8 +59,8 @@ public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) /// are defined as s. Any other definitions will be ignored. /// /// - public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) - : this(toolDefinitions as IEnumerable) + public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) + : this(toolDefinitions.ToArray()) { } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs index d25e586163a..79ebc923d6c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Microsoft.Extensions.AI.Evaluation.Quality; @@ -40,7 +41,7 @@ public sealed class ToolCallAccuracyEvaluatorContext : EvaluationContext /// are defined as s. Any other definitions will be ignored. /// /// - public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) + public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) : base(name: ToolCallAccuracyContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; @@ -59,8 +60,8 @@ public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) /// are defined as s. Any other definitions will be ignored. /// /// - public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) - : this(toolDefinitions as IEnumerable) + public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) + : this(toolDefinitions.ToArray()) { } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md index 580facd6294..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/README.md index c21e2a299ad..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/README.md @@ -5,6 +5,8 @@ * [`Microsoft.Extensions.AI.Evaluation`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation) - Defines core abstractions and types for supporting evaluation. * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. +* [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. @@ -18,6 +20,7 @@ dotnet add package Microsoft.Extensions.AI.Evaluation dotnet add package Microsoft.Extensions.AI.Evaluation.Quality dotnet add package Microsoft.Extensions.AI.Evaluation.Safety dotnet add package Microsoft.Extensions.AI.Evaluation.Reporting +dotnet add package Microsoft.Extensions.AI.Evaluation.NLP ``` Or directly in the C# project file: @@ -28,6 +31,7 @@ Or directly in the C# project file: + ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md index e135ed24cfe..9bf406ba052 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md b/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md index 580facd6294..dfc15311489 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/README.md @@ -6,7 +6,7 @@ * [`Microsoft.Extensions.AI.Evaluation.Quality`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Quality) - Contains evaluators that can be used to evaluate the quality of AI responses in your projects including Relevance, Truth, Completeness, Fluency, Coherence, Retrieval, Equivalence and Groundedness. * [`Microsoft.Extensions.AI.Evaluation.Safety`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Safety) - Contains a set of evaluators that are built atop the Azure AI Foundry Evaluation service that can be used to evaluate the content safety of AI responses in your projects including Protected Material, Groundedness Pro, Ungrounded Attributes, Hate and Unfairness, Self Harm, Violence, Sexual, Code Vulnerability and Indirect Attack. * [`Microsoft.Extensions.AI.Evaluation.NLP`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.NLP) - Contains a set of evaluators that implement common algorithms for evaluating machine translation and natural -language processing tasks. Evaluators currently include BLEU score, with more planned. +language processing tasks. Evaluators currently include BLEU, GLEU and F1 scores. * [`Microsoft.Extensions.AI.Evaluation.Reporting`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting) - Contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. * [`Microsoft.Extensions.AI.Evaluation.Reporting.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Reporting.Azure) - Supports the `Microsoft.Extensions.AI.Evaluation.Reporting` library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. * [`Microsoft.Extensions.AI.Evaluation.Console`](https://www.nuget.org/packages/Microsoft.Extensions.AI.Evaluation.Console) - A command line dotnet tool for generating reports and managing evaluation data. diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs index 1b029dc4a37..9260a688cc4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/BLEUAlgorithmTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.AI.Evaluation.NLP.Common; using Xunit; @@ -15,8 +14,8 @@ public class BLEUAlgorithmTests [Fact] public void ModifiedPrecisionTests() { - IEnumerable> references = ["the cat is on the mat".Split(' '), "there is a cat on the mat".Split(' ')]; - IEnumerable hypothesis = "the the the the the the the".Split(' '); + string[][] references = ["the cat is on the mat".Split(' '), "there is a cat on the mat".Split(' ')]; + string[] hypothesis = "the the the the the the the".Split(' '); RationalNumber prec = ModifiedPrecision(references, hypothesis, 1); Assert.Equal(0.2857, prec.ToDouble(), 4); @@ -36,8 +35,8 @@ public void ModifiedPrecisionTests() "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), "It is the practical guide for the army always to heed the directions of the party".Split(' '), ]; - IEnumerable hypothesis1 = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); - IEnumerable hypothesis2 = "It is to insure the troops forever hearing the activity guidebook that party direct".Split(' '); + string[] hypothesis1 = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + string[] hypothesis2 = "It is to insure the troops forever hearing the activity guidebook that party direct".Split(' '); prec = ModifiedPrecision(references, hypothesis1, 1); Assert.Equal(0.9444, prec.ToDouble(), 4); prec = ModifiedPrecision(references, hypothesis2, 1); @@ -76,63 +75,63 @@ public void SmoothingMethod4Tests(int[] num_denom, int hypLen, double[] vals) [Fact] public void TestBrevityPenalty() { - IEnumerable> references = [ - Enumerable.Repeat("a", 11), - Enumerable.Repeat("a", 8), + string[][] references = [ + [.. Enumerable.Repeat("a", 11)], + [.. Enumerable.Repeat("a", 8)], ]; - IEnumerable hypothesis = Enumerable.Repeat("a", 7); + string[] hypothesis = [.. Enumerable.Repeat("a", 7)]; int hypLength = hypothesis.Count(); int closestRefLength = ClosestRefLength(references, hypLength); double brevityPenalty = BrevityPenalty(closestRefLength, hypLength); Assert.Equal(0.8669, brevityPenalty, 4); references = [ - Enumerable.Repeat("a", 11), - Enumerable.Repeat("a", 8), - Enumerable.Repeat("a", 6), - Enumerable.Repeat("a", 7), + [.. Enumerable.Repeat("a", 11)], + [.. Enumerable.Repeat("a", 8)], + [.. Enumerable.Repeat("a", 6)], + [.. Enumerable.Repeat("a", 7)], ]; - hypothesis = Enumerable.Repeat("a", 7); + hypothesis = [.. Enumerable.Repeat("a", 7)]; hypLength = hypothesis.Count(); closestRefLength = ClosestRefLength(references, hypLength); brevityPenalty = BrevityPenalty(closestRefLength, hypLength); Assert.Equal(1.0, brevityPenalty, 4); references = [ - Enumerable.Repeat("a", 28), - Enumerable.Repeat("a", 28), + [.. Enumerable.Repeat("a", 28)], + [.. Enumerable.Repeat("a", 28)], ]; - hypothesis = Enumerable.Repeat("a", 12); + hypothesis = [.. Enumerable.Repeat("a", 12)]; hypLength = hypothesis.Count(); closestRefLength = ClosestRefLength(references, hypLength); brevityPenalty = BrevityPenalty(closestRefLength, hypLength); Assert.Equal(0.26359, brevityPenalty, 4); references = [ - Enumerable.Repeat("a", 13), - Enumerable.Repeat("a", 2), + [.. Enumerable.Repeat("a", 13)], + [.. Enumerable.Repeat("a", 2)], ]; - hypothesis = Enumerable.Repeat("a", 12); + hypothesis = [.. Enumerable.Repeat("a", 12)]; hypLength = hypothesis.Count(); closestRefLength = ClosestRefLength(references, hypLength); brevityPenalty = BrevityPenalty(closestRefLength, hypLength); Assert.Equal(0.9200, brevityPenalty, 4); references = [ - Enumerable.Repeat("a", 13), - Enumerable.Repeat("a", 11), + [.. Enumerable.Repeat("a", 13)], + [.. Enumerable.Repeat("a", 11)], ]; - hypothesis = Enumerable.Repeat("a", 12); + hypothesis = [.. Enumerable.Repeat("a", 12)]; hypLength = hypothesis.Count(); closestRefLength = ClosestRefLength(references, hypLength); brevityPenalty = BrevityPenalty(closestRefLength, hypLength); Assert.Equal(1.0, brevityPenalty, 4); references = [ - Enumerable.Repeat("a", 11), - Enumerable.Repeat("a", 13), + [.. Enumerable.Repeat("a", 11)], + [.. Enumerable.Repeat("a", 13)], ]; - hypothesis = Enumerable.Repeat("a", 12); + hypothesis = [.. Enumerable.Repeat("a", 12)]; hypLength = hypothesis.Count(); closestRefLength = ClosestRefLength(references, hypLength); brevityPenalty = BrevityPenalty(closestRefLength, hypLength); @@ -143,8 +142,8 @@ public void TestBrevityPenalty() [Fact] public void TestZeroMatches() { - IEnumerable> references = ["The candidate has no alignment to any of the references".Split(' '),]; - IEnumerable hypothesis = "John loves Mary".Split(' '); + string[][] references = ["The candidate has no alignment to any of the references".Split(' '),]; + string[] hypothesis = "John loves Mary".Split(' '); double score = SentenceBLEU(references, hypothesis, EqualWeights(hypothesis.Count())); Assert.Equal(0.0, score, 4); @@ -153,8 +152,8 @@ public void TestZeroMatches() [Fact] public void TestFullMatches() { - IEnumerable> references = ["John loves Mary".Split(' '),]; - IEnumerable hypothesis = "John loves Mary".Split(' '); + string[][] references = ["John loves Mary".Split(' '),]; + string[] hypothesis = "John loves Mary".Split(' '); double score = SentenceBLEU(references, hypothesis, EqualWeights(hypothesis.Count())); Assert.Equal(1.0, score, 4); @@ -163,8 +162,8 @@ public void TestFullMatches() [Fact] public void TestPartialMatchesHypothesisLongerThanReference() { - IEnumerable> references = ["John loves Mary".Split(' '),]; - IEnumerable hypothesis = "John loves Mary who loves Mike".Split(' '); + string[][] references = ["John loves Mary".Split(' '),]; + string[] hypothesis = "John loves Mary who loves Mike".Split(' '); double score = SentenceBLEU(references, hypothesis); Assert.Equal(0, score, 4); @@ -173,12 +172,12 @@ public void TestPartialMatchesHypothesisLongerThanReference() [Fact] public void TestSentenceBLEUExampleA() { - IEnumerable> references = [ + string[][] references = [ "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), "It is the practical guide for the army always to heed the directions of the party".Split(' ') ]; - IEnumerable hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + string[] hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); double score = SentenceBLEU(references, hypothesis); Assert.Equal(0.5046, score, 4); @@ -188,10 +187,10 @@ public void TestSentenceBLEUExampleA() [Fact] public void TestSentenceBLEUExampleB() { - IEnumerable> references = [ + string[][] references = [ "he was interested in world history because he read the book".Split(' '), ]; - IEnumerable hypothesis = "he read the book because he was interested in world history".Split(' '); + string[] hypothesis = "he read the book because he was interested in world history".Split(' '); double score = SentenceBLEU(references, hypothesis); Assert.Equal(0.74009, score, 4); @@ -200,12 +199,12 @@ public void TestSentenceBLEUExampleB() [Fact] public void TestSentenceBLEUExampleAWithWordTokenizer() { - IEnumerable> references = [ - SimpleWordTokenizer.WordTokenize("It is a guide to action that ensures that the military will forever heed Party commands"), - SimpleWordTokenizer.WordTokenize("It is the guiding principle which guarantees the military forces always being under the command of the Party"), - SimpleWordTokenizer.WordTokenize("It is the practical guide for the army always to heed the directions of the party") + string[][] references = [ + SimpleWordTokenizer.WordTokenize("It is a guide to action that ensures that the military will forever heed Party commands").ToArray(), + SimpleWordTokenizer.WordTokenize("It is the guiding principle which guarantees the military forces always being under the command of the Party").ToArray(), + SimpleWordTokenizer.WordTokenize("It is the practical guide for the army always to heed the directions of the party").ToArray(), ]; - IEnumerable hypothesis = SimpleWordTokenizer.WordTokenize("It is a guide to action which ensures that the military always obeys the commands of the party"); + string[] hypothesis = SimpleWordTokenizer.WordTokenize("It is a guide to action which ensures that the military always obeys the commands of the party").ToArray(); double score = SentenceBLEU(references, hypothesis); Assert.Equal(0.5046, score, 4); @@ -215,10 +214,10 @@ public void TestSentenceBLEUExampleAWithWordTokenizer() [Fact] public void TestSentenceBLEUExampleBWithWordTokenizer() { - IEnumerable> references = [ - SimpleWordTokenizer.WordTokenize("he was interested in world history because he read the book"), + string[][] references = [ + SimpleWordTokenizer.WordTokenize("he was interested in world history because he read the book").ToArray(), ]; - IEnumerable hypothesis = SimpleWordTokenizer.WordTokenize("he read the book because he was interested in world history"); + string[] hypothesis = SimpleWordTokenizer.WordTokenize("he read the book because he was interested in world history").ToArray(); double score = SentenceBLEU(references, hypothesis); Assert.Equal(0.74009, score, 4); diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/F1EvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/F1EvaluatorTests.cs new file mode 100644 index 00000000000..52a87badf41 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/F1EvaluatorTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +#pragma warning disable AIEVAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +public class F1EvaluatorTests +{ + [Fact] + public async Task ReturnsPerfectScoreForIdenticalText() + { + var evaluator = new F1Evaluator(); + var context = new F1EvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "The quick brown fox jumps over the lazy dog.")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(F1Evaluator.F1MetricName, metric.Name); + Assert.Equal(1.0, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Exceptional, metric.Interpretation.Rating); + Assert.False(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsLowScoreForCompletelyDifferentText() + { + var evaluator = new F1Evaluator(); + var context = new F1EvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Completely unrelated sentence.")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(F1Evaluator.F1MetricName, metric.Name); + Assert.Equal(0.1429, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Unacceptable, metric.Interpretation.Rating); + Assert.True(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfNoContext() + { + var evaluator = new F1Evaluator(); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Some text.")); + var result = await evaluator.EvaluateAsync(response, null, null); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(F1Evaluator.F1MetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + + [Theory] + [InlineData("the cat is on the mat", + "the the the the the the the", 0.30769)] + [InlineData("It is a guide to action that ensures that the military will forever heed Party commands", + "It is a guide to action which ensures that the military always obeys the commands of the party", 0.70589)] + [InlineData("It is the practical guide for the army always to heed the directions of the party", + "It is to insure the troops forever hearing the activity guidebook that party direct", 0.4000)] + public async Task SampleCases(string reference, string hypothesis, double score) + { + var evaluator = new F1Evaluator(); + var context = new F1EvaluatorContext(reference); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, hypothesis)); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(F1Evaluator.F1MetricName, metric.Name); + Assert.Equal(score, (double)metric!.Value!, 4); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfEmptyResponse() + { + var evaluator = new F1Evaluator(); + var context = new F1EvaluatorContext("Reference text."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(F1Evaluator.F1MetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUAlgorithmTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUAlgorithmTests.cs new file mode 100644 index 00000000000..794b85c6595 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUAlgorithmTests.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.AI.Evaluation.NLP.Common; +using Xunit; +using static Microsoft.Extensions.AI.Evaluation.NLP.Common.GLEUAlgorithm; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +public class GLEUAlgorithmTests +{ + [Fact] + public void TestZeroMatches() + { + string[][] references = ["The candidate has no alignment to any of the references".Split(' '),]; + string[] hypothesis = "John loves Mary".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.0, score, 4); + } + + [Fact] + public void TestFullMatches() + { + string[][] references = ["John loves Mary".Split(' '),]; + string[] hypothesis = "John loves Mary".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(1.0, score, 4); + } + + [Fact] + public void TestSentenceGLEUExampleA() + { + string[][] references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + "It is the guiding principle which guarantees the military forces always being under the command of the Party".Split(' '), + "It is the practical guide for the army always to heed the directions of the party".Split(' ') + ]; + string[] hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.2778, score, 4); + } + + [Fact] + public void TestSentenceGLEUMilitaryExampleA() + { + string[][] references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + ]; + string[] hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.43939, score, 4); + } + + [Fact] + public void TestSentenceGLEUMilitaryExampleB() + { + string[][] references = [ + "It is a guide to action that ensures that the military will forever heed Party commands".Split(' '), + ]; + string[] hypothesis = "It is to insure the troops forever hearing the activity guidebook that party direct".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.12069, score, 4); + } + + [Fact] + public void TestSentenceGLEUExampleB() + { + string[][] references = [ + "he was interested in world history because he read the book".Split(' '), + ]; + string[] hypothesis = "he read the book because he was interested in world history".Split(' '); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.7895, score, 4); + } + + [Fact] + public void TestSentenceGLEUExampleAWithWordTokenizer() + { + string[][] references = [ + SimpleWordTokenizer.WordTokenize("It is a guide to action that ensures that the military will forever heed Party commands").ToArray(), + SimpleWordTokenizer.WordTokenize("It is the guiding principle which guarantees the military forces always being under the command of the Party").ToArray(), + SimpleWordTokenizer.WordTokenize("It is the practical guide for the army always to heed the directions of the party").ToArray(), + ]; + string[] hypothesis = SimpleWordTokenizer.WordTokenize("It is a guide to action which ensures that the military always obeys the commands of the party").ToArray(); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.2980, score, 4); + + } + + [Fact] + public void TestSentenceGLEUExampleBWithWordTokenizer() + { + string[][] references = [ + SimpleWordTokenizer.WordTokenize("he was interested in world history because he read the book").ToArray(), + ]; + string[] hypothesis = SimpleWordTokenizer.WordTokenize("he read the book because he was interested in world history").ToArray(); + + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.7895, score, 4); + } + + [Fact] + public void TestSentenceGLEUCatExample() + { + string[][] references = [ + "the cat is on the mat".Split(' '), + ]; + string[] hypothesis = "the the the the the the the".Split(' '); + double score = SentenceGLEU(references, hypothesis); + Assert.Equal(0.0909, score, 4); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUEvaluatorTests.cs new file mode 100644 index 00000000000..f27d11e4e2e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/GLEUEvaluatorTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.NLP.Tests; + +#pragma warning disable AIEVAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +public class GLEUEvaluatorTests +{ + [Fact] + public async Task ReturnsPerfectScoreForIdenticalText() + { + var evaluator = new GLEUEvaluator(); + var context = new GLEUEvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "The quick brown fox jumps over the lazy dog.")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.Equal(1.0, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Exceptional, metric.Interpretation.Rating); + Assert.False(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsLowScoreForCompletelyDifferentText() + { + var evaluator = new GLEUEvaluator(); + var context = new GLEUEvaluatorContext("The quick brown fox jumps over the lazy dog."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Completely unrelated sentence.")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.Equal(0.02939, (double)metric!.Value!, 4); + Assert.NotNull(metric.Interpretation); + Assert.Equal(EvaluationRating.Unacceptable, metric.Interpretation.Rating); + Assert.True(metric.Interpretation.Failed); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfNoContext() + { + var evaluator = new GLEUEvaluator(); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Some text.")); + var result = await evaluator.EvaluateAsync(response, null, null); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + + [Theory] + [InlineData("the cat is on the mat", + "the the the the the the the", 0.0909)] + [InlineData("It is a guide to action that ensures that the military will forever heed Party commands", + "It is a guide to action which ensures that the military always obeys the commands of the party", 0.4545)] + [InlineData("It is the practical guide for the army always to heed the directions of the party", + "It is to insure the troops forever hearing the activity guidebook that party direct", 0.12069)] + public async Task SampleCases(string reference, string hypothesis, double score) + { + var evaluator = new GLEUEvaluator(); + var context = new GLEUEvaluatorContext(reference); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, hypothesis)); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.Equal(score, (double)metric!.Value!, 4); + } + + [Fact] + public async Task MultipleReferences() + { + string[] references = [ + "It is a guide to action that ensures that the military will forever heed Party commands", + "It is the guiding principle which guarantees the military forces always being under the command of the Party", + "It is the practical guide for the army always to heed the directions of the party", + ]; + string hypothesis = "It is a guide to action which ensures that the military always obeys the commands of the party"; + + var evaluator = new GLEUEvaluator(); + var context = new GLEUEvaluatorContext(references); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, hypothesis)); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.Equal(0.29799, (double)metric!.Value!, 4); + } + + [Fact] + public async Task ReturnsErrorDiagnosticIfEmptyResponse() + { + var evaluator = new GLEUEvaluator(); + var context = new GLEUEvaluatorContext("Reference text."); + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "")); + var result = await evaluator.EvaluateAsync(response, null, [context]); + var metric = Assert.Single(result.Metrics.Values) as NumericMetric; + Assert.NotNull(metric); + Assert.Equal(GLEUEvaluator.GLEUMetricName, metric.Name); + Assert.NotNull(metric.Diagnostics); + Assert.Contains(metric.Diagnostics, d => d.Severity == EvaluationDiagnosticSeverity.Error); + } + +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs index 9c2a5b68900..71765ca3eff 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/MatchCounterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.AI.Evaluation.NLP.Common; using Xunit; @@ -62,4 +63,21 @@ public void ToDebugString_FormatsCorrectly() Assert.Contains("x: 2", str); Assert.Contains("y: 1", str); } + + [Fact] + public void Intersect_ReturnsCorrectIntersection() + { + MatchCounter counter1 = new(new[] { 1, 2, 2, 3 }); + MatchCounter counter2 = new(new[] { 2, 2, 4 }); + + MatchCounter intersection = counter1.Intersect(counter2); + Dictionary dict = intersection.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.Equal(2, dict[2]); + Assert.Equal(2, intersection.Sum()); + + intersection = counter2.Intersect(counter1); + dict = intersection.ToDictionary(kv => kv.Key, kv => kv.Value); + Assert.Equal(2, dict[2]); + Assert.Equal(2, intersection.Sum()); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs index d782c3c8f88..6c0aefbb02a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/NGramTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.AI.Evaluation.NLP.Common; using Xunit; @@ -60,21 +59,55 @@ public void NGramBuilder_Create_Works() } [Fact] - public void NGramGenerationNoPadding() + public void CreateNGrams() { - int[] input = [1, 2, 3, 4, 5]; + Assert.Throws(() => new int[0].CreateNGrams(-1).ToList()); - IEnumerable> result = input.CreateNGrams(1); - List> expected = [[1], [2], [3], [4], [5]]; - Assert.True(result.SequenceEqual(expected)); + ReadOnlySpan data = [1, 2, 3]; - result = input.CreateNGrams(2); - expected = [[1, 2], [2, 3], [3, 4], [4, 5]]; - Assert.True(result.SequenceEqual(expected)); + var nGram = data.CreateNGrams(1); + Assert.Equal([[1], [2], [3]], nGram); - result = input.CreateNGrams(3); - expected = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]; - Assert.True(result.SequenceEqual(expected)); + nGram = data.CreateNGrams(2); + Assert.Equal([[1, 2], [2, 3]], nGram); + + nGram = data.CreateNGrams(3); + Assert.Equal([[1, 2, 3]], nGram); + + nGram = data.CreateNGrams(4); + Assert.Equal([], nGram); } + [Fact] + public void CreateAllNGrams() + { + Assert.Throws(() => new int[0].CreateAllNGrams(-1).ToList()); + + Assert.Throws(() => new int[0].CreateAllNGrams(0).ToList()); + + Assert.Throws(() => new int[0].CreateAllNGrams(1, 0).ToList()); + + ReadOnlySpan arr = [1, 2, 3]; + + var nGram = arr.CreateAllNGrams(1).ToList(); + Assert.Equal([[1], [1, 2], [1, 2, 3], [2], [2, 3], [3]], nGram); + + nGram = arr.CreateAllNGrams(2).ToList(); + Assert.Equal([[1, 2], [1, 2, 3], [2, 3]], nGram); + + nGram = arr.CreateAllNGrams(3).ToList(); + Assert.Equal([[1, 2, 3]], nGram); + + nGram = arr.CreateAllNGrams(3, 5).ToList(); + Assert.Equal([[1, 2, 3]], nGram); + + nGram = arr.CreateAllNGrams(1, 2).ToList(); + Assert.Equal([[1], [1, 2], [2], [2, 3], [3]], nGram); + + nGram = arr.CreateAllNGrams(1, 1).ToList(); + Assert.Equal([[1], [2], [3]], nGram); + + nGram = arr.CreateAllNGrams(4).ToList(); + Assert.Equal([], nGram); + } } From 952c751ca2ec535c8cf4d24ad9943834ccfb78f3 Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Tue, 1 Jul 2025 14:42:26 -0400 Subject: [PATCH 163/472] Update tokenizer alg to use is [.] (#6561) --- .../Common/SimpleWordTokenizer.cs | 9 ++------- .../SimpleTokenizerTests.cs | 2 ++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs index 4f4717852bd..322ea4cedd6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs @@ -76,18 +76,13 @@ public static IEnumerable WordTokenize(ReadOnlyMemory text) } // Join hyphenated words - if (span[0] == '-' && - span.Length > 1 && - span[1] == '\n') + if (span is ['-', '\n', ..]) { text = text.Slice(2); continue; } - if (span[0] == '-' && - span.Length > 2 && - span[1] == '\r' && - span[2] == '\n') + if (span is ['-', '\r', '\n', ..]) { text = text.Slice(3); continue; diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs index 3451a6c38c9..5766c2e7fc0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.NLP.Tests/SimpleTokenizerTests.cs @@ -20,6 +20,8 @@ public class SimpleTokenizerTests [InlineData("word1-word2", new[] { "WORD1", "-", "WORD2" })] [InlineData("word1 - word2", new[] { "WORD1", "-", "WORD2" })] [InlineData("word1-\n word2", new[] { "WORD1", "WORD2" })] + [InlineData("word1-", new[] { "WORD1", "-" })] + [InlineData("word1&", new[] { "WORD1", "&" })] [InlineData("word1-\r\n word2", new[] { "WORD1", "WORD2" })] [InlineData("word1-\r\nword2", new[] { "WORD1WORD2" })] [InlineData("word1-\nword2", new[] { "WORD1WORD2" })] From fd8a1f00e2ac091d8d25483dd779996f6a783792 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Tue, 1 Jul 2025 12:47:33 -0700 Subject: [PATCH 164/472] Rename some constants to match convention. (#6562) --- .../BLEUEvaluator.cs | 10 +++++----- .../BLEUEvaluatorContext.cs | 7 ++++--- .../GLEUEvaluator.cs | 10 +++++----- .../GLEUEvaluatorContext.cs | 7 ++++--- .../IntentResolutionEvaluatorContext.cs | 4 ++-- .../TaskAdherenceEvaluatorContext.cs | 4 ++-- .../ToolCallAccuracyEvaluatorContext.cs | 4 ++-- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs index bfe1b9af589..e1419bd630e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -20,11 +20,11 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP; /// /// /// -/// The computes the BLEU score of a response ("hypothesis") compared to a reference -/// supplied via . The score is returned in a -/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates a perfect match. -/// By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher is -/// passing and a score below 0.5 is failing. +/// The computes the BLEU score of a response ("hypothesis") compared to one or more +/// reference responses supplied via . The score is returned in a +/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates +/// a perfect match. By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher +/// is passing and a score below 0.5 is failing. /// /// public sealed class BLEUEvaluator : IEvaluator diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs index 4085355db92..c1eaf699565 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs @@ -15,8 +15,9 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP; /// Contextual information that the uses to compute the BLEU score for a response. /// /// -/// measures the BLEU score of a response compared to a reference. BLEU (Bilingual Evaluation Understudy) -/// is a metric used to evaluate the quality of machine-generated text. +/// measures the BLEU score of a response compared to one or more reference responses +/// supplied via . BLEU (Bilingual Evaluation Understudy) is a metric used to evaluate the +/// quality of machine-generated text. /// public sealed class BLEUEvaluatorContext : EvaluationContext { @@ -31,7 +32,7 @@ public sealed class BLEUEvaluatorContext : EvaluationContext /// /// /// The measures the degree to which the response being evaluated is similar to - /// the response supplied via . The metric will be reported as a BLEU score. + /// the responses supplied via . The metric will be reported as a BLEU score. /// public IReadOnlyList References { get; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs index d33ed07f5cb..0c9805ee108 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs @@ -20,11 +20,11 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP; /// /// /// -/// The computes the GLEU score of a response ("hypothesis") compared to a reference -/// supplied via . The score is returned in a -/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates a perfect match. -/// By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher is -/// passing and a score below 0.5 is failing. +/// The computes the GLEU score of a response ("hypothesis") compared to one or more +/// reference responses supplied via . The score is returned in a +/// with a value between 0.0 and 1.0 where 0.0 represents no match at all and 1.0 indicates +/// a perfect match. By default, the score is interpreted with a pass/fail cutoff of 0.5. So a score of 0.5 or higher +/// is passing and a score below 0.5 is failing. /// /// public sealed class GLEUEvaluator : IEvaluator diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs index b41b1f80f42..d98aac6811d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs @@ -16,7 +16,8 @@ namespace Microsoft.Extensions.AI.Evaluation.NLP; /// /// /// measures the GLEU score of a response compared to one or more reference responses -/// supplied via . GLEU (Google-BLEU) is a metric used to evaluate the quality of machine-generated text. +/// supplied via . GLEU (Google-BLEU) is a metric used to evaluate the quality of +/// machine-generated text. /// public sealed class GLEUEvaluatorContext : EvaluationContext { @@ -27,11 +28,11 @@ public sealed class GLEUEvaluatorContext : EvaluationContext public static string ReferencesContextName => "References (GLEU)"; /// - /// Gets the reference against which the provided response will be scored. + /// Gets the references against which the provided response will be scored. /// /// /// The measures the degree to which the response being evaluated is similar to - /// the response supplied via . The metric will be reported as a GLEU score. + /// the responses supplied via . The metric will be reported as a GLEU score. /// public IReadOnlyList References { get; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs index cf4a9b17004..c19cb5dcd71 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs @@ -40,7 +40,7 @@ public sealed class IntentResolutionEvaluatorContext : EvaluationContext /// /// public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) - : base(name: IntentResolutionContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + : base(name: ToolDefinitionsContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; } @@ -67,7 +67,7 @@ public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) /// Gets the unique that is used for /// . ///
    - public static string IntentResolutionContextName => "Tool Definitions (Intent Resolution)"; + public static string ToolDefinitionsContextName => "Tool Definitions (Intent Resolution)"; /// /// Gets set of tool definitions (see ) that were used when generating the model diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs index 4557f2536d2..c8e94d03b26 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs @@ -41,7 +41,7 @@ public sealed class TaskAdherenceEvaluatorContext : EvaluationContext /// /// public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) - : base(name: TaskAdherenceContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + : base(name: ToolDefinitionsContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; } @@ -68,7 +68,7 @@ public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) /// Gets the unique that is used for /// . /// - public static string TaskAdherenceContextName => "Tool Definitions (Task Adherence)"; + public static string ToolDefinitionsContextName => "Tool Definitions (Task Adherence)"; /// /// Gets set of tool definitions (see ) that were used when generating the model diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs index 79ebc923d6c..037d811e0f4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs @@ -42,7 +42,7 @@ public sealed class ToolCallAccuracyEvaluatorContext : EvaluationContext /// /// public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) - : base(name: ToolCallAccuracyContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) + : base(name: ToolDefinitionsContextName, contents: [new TextContent(toolDefinitions.RenderAsJson())]) { ToolDefinitions = [.. toolDefinitions]; } @@ -69,7 +69,7 @@ public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) /// Gets the unique that is used for /// . /// - public static string ToolCallAccuracyContextName => "Tool Definitions (Tool Call Accuracy)"; + public static string ToolDefinitionsContextName => "Tool Definitions (Tool Call Accuracy)"; /// /// Gets set of tool definitions (see ) that were used when generating the model From f78d2870a47dc1a54ed62de29b24e7828e228991 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 1 Jul 2025 16:42:02 -0400 Subject: [PATCH 165/472] Add DistributedCachingChatClient/EmbeddingGenerator.AdditionalCacheKeyValues (#6558) * Add DistributedCachingChatClient.AdditionalCacheKeyValues GetCacheKey already enabled augmenting the key list, but doing so required deriving a custom caching client. It appears to be a reasonably common thing to want to configure, so exposing it as a property as well. While doing this, I also removed additional per-call allocation coming from GetCacheKey. * Address feedback --- .../CSharp/ResponseCachingChatClient.cs | 7 +-- .../DistributedCachingChatClient.cs | 40 +++++++++++- .../Embeddings/CachingEmbeddingGenerator.cs | 5 ++ .../DistributedCachingEmbeddingGenerator.cs | 45 +++++++++++++- .../Microsoft.Extensions.AI.json | 8 +++ .../DistributedCachingChatClientTest.cs | 46 ++++++++++++++ ...istributedCachingEmbeddingGeneratorTest.cs | 61 +++++++++++++++++++ 7 files changed, 201 insertions(+), 11 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs index 79baf3be88c..bc49d76e1be 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs @@ -1,7 +1,6 @@ // 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.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -13,7 +12,6 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; internal sealed class ResponseCachingChatClient : DistributedCachingChatClient { - private readonly IReadOnlyList _cachingKeys; private readonly ChatDetails _chatDetails; private readonly ConcurrentDictionary _stopWatches; @@ -24,7 +22,7 @@ internal ResponseCachingChatClient( ChatDetails chatDetails) : base(originalChatClient, cache) { - _cachingKeys = [.. cachingKeys]; + CacheKeyAdditionalValues = [.. cachingKeys]; _chatDetails = chatDetails; _stopWatches = new ConcurrentDictionary(); } @@ -124,7 +122,4 @@ protected override async Task WriteCacheStreamingAsync( cacheHit: false)); } } - - protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) - => base.GetCacheKey(messages, options, [.. additionalValues, .. _cachingKeys]); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs index 9fb586f5b79..afaa12235ec 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -34,9 +36,16 @@ namespace Microsoft.Extensions.AI; /// public class DistributedCachingChatClient : CachingChatClient { + /// Boxed cache version. + /// Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way. + private static readonly object _cacheVersion = 2; + /// The instance that will be used as the backing store for the cache. private readonly IDistributedCache _storage; + /// Additional values used to inform the cache key employed for storing state. + private object[]? _cacheKeyAdditionalValues; + /// The to use when serializing cache data. private JsonSerializerOptions _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; @@ -56,6 +65,14 @@ public JsonSerializerOptions JsonSerializerOptions set => _jsonSerializerOptions = Throw.IfNull(value); } + /// Gets or sets additional values used to inform the cache key employed for storing state. + /// Any values set in this list will augment the other values used to inform the cache key. + public IReadOnlyList? CacheKeyAdditionalValues + { + get => _cacheKeyAdditionalValues; + set => _cacheKeyAdditionalValues = value?.ToArray(); + } + /// protected override async Task ReadCacheAsync(string key, CancellationToken cancellationToken) { @@ -122,9 +139,26 @@ protected override async Task WriteCacheStreamingAsync(string key, IReadOnlyList /// protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) { - // Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way. - const int CacheVersion = 2; + const int FixedValuesCount = 3; + + object[] clientValues = _cacheKeyAdditionalValues ?? Array.Empty(); + int length = FixedValuesCount + additionalValues.Length + clientValues.Length; - return AIJsonUtilities.HashDataToString([CacheVersion, messages, options, .. additionalValues], _jsonSerializerOptions); + object?[] arr = ArrayPool.Shared.Rent(length); + try + { + arr[0] = _cacheVersion; + arr[1] = messages; + arr[2] = options; + additionalValues.CopyTo(arr.AsSpan(FixedValuesCount)); + clientValues.CopyTo(arr, FixedValuesCount + additionalValues.Length); + + return AIJsonUtilities.HashDataToString(arr.AsSpan(0, length), _jsonSerializerOptions); + } + finally + { + Array.Clear(arr, 0, length); + ArrayPool.Shared.Return(arr); + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/CachingEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/CachingEmbeddingGenerator.cs index 2c880d7a22c..926378ad517 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/CachingEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/CachingEmbeddingGenerator.cs @@ -54,6 +54,11 @@ public override async Task> GenerateAsync( Throw.InvalidOperationException($"Expected exactly one embedding to be generated, but received {generated.Count}."); } + if (generated[0] is null) + { + Throw.InvalidOperationException("Generator produced null embedding."); + } + await WriteCacheAsync(cacheKey, generated[0], cancellationToken); return generated; } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/DistributedCachingEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/DistributedCachingEmbeddingGenerator.cs index cd26879d040..7da9671554b 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/DistributedCachingEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/DistributedCachingEmbeddingGenerator.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading; @@ -24,7 +27,17 @@ namespace Microsoft.Extensions.AI; public class DistributedCachingEmbeddingGenerator : CachingEmbeddingGenerator where TEmbedding : Embedding { + /// Boxed cache version. + /// Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way. + private static readonly object _cacheVersion = 2; + + /// The instance that will be used as the backing store for the cache. private readonly IDistributedCache _storage; + + /// Additional values used to inform the cache key employed for storing state. + private object[]? _cacheKeyAdditionalValues; + + /// Additional cache key values used to inform the key employed for storing state. private JsonSerializerOptions _jsonSerializerOptions; /// Initializes a new instance of the class. @@ -51,6 +64,14 @@ public JsonSerializerOptions JsonSerializerOptions } } + /// Gets or sets additional values used to inform the cache key employed for storing state. + /// Any values set in this list will augment the other values used to inform the cache key. + public IReadOnlyList? CacheKeyAdditionalValues + { + get => _cacheKeyAdditionalValues; + set => _cacheKeyAdditionalValues = value?.ToArray(); + } + /// protected override async Task ReadCacheAsync(string key, CancellationToken cancellationToken) { @@ -87,6 +108,26 @@ protected override async Task WriteCacheAsync(string key, TEmbedding value, Canc /// The generated cache key is not guaranteed to be stable across releases of the library. /// /// - protected override string GetCacheKey(params ReadOnlySpan values) => - AIJsonUtilities.HashDataToString(values, _jsonSerializerOptions); + protected override string GetCacheKey(params ReadOnlySpan values) + { + const int FixedValuesCount = 1; + + object[] clientValues = _cacheKeyAdditionalValues ?? Array.Empty(); + int length = FixedValuesCount + clientValues.Length + values.Length; + + object?[] arr = ArrayPool.Shared.Rent(length); + try + { + arr[0] = _cacheVersion; + values.CopyTo(arr.AsSpan(FixedValuesCount)); + clientValues.CopyTo(arr, FixedValuesCount + values.Length); + + return AIJsonUtilities.HashDataToString(arr.AsSpan(0, length), _jsonSerializerOptions); + } + finally + { + Array.Clear(arr, 0, length); + ArrayPool.Shared.Return(arr); + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index f7f246eb35c..4f4317c9978 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -310,6 +310,10 @@ } ], "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyList? Microsoft.Extensions.AI.DistributedCachingChatClient.CacheKeyAdditionalValues { get; set; }", + "Stage": "Stable" + }, { "Member": "System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DistributedCachingChatClient.JsonSerializerOptions { get; set; }", "Stage": "Stable" @@ -351,6 +355,10 @@ } ], "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyList? Microsoft.Extensions.AI.DistributedCachingEmbeddingGenerator.CacheKeyAdditionalValues { get; set; }", + "Stage": "Stable" + }, { "Member": "System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DistributedCachingEmbeddingGenerator.JsonSerializerOptions { get; set; }", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs index 2c755da7be9..4b6f9bc87e6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs @@ -595,6 +595,52 @@ public async Task CacheKeyVariesByChatOptionsAsync() Assert.Equal("value 2", result4.Text); } + [Fact] + public async Task CacheKeyVariesByAdditionalKeyValuesAsync() + { + // Arrange + var innerCallCount = 0; + var completionTcs = new TaskCompletionSource(); + using var testClient = new TestChatClient + { + GetResponseAsyncCallback = async (_, options, _) => + { + innerCallCount++; + await Task.Yield(); + return new(new ChatMessage(ChatRole.Assistant, innerCallCount.ToString())); + } + }; + using var outer = new DistributedCachingChatClient(testClient, _storage) + { + JsonSerializerOptions = TestJsonSerializerContext.Default.Options + }; + + var result1 = await outer.GetResponseAsync([]); + var result2 = await outer.GetResponseAsync([]); + + Assert.Equal(1, innerCallCount); + Assert.Equal("1", result1.Text); + Assert.Equal("1", result2.Text); + + // Change key + outer.CacheKeyAdditionalValues = ["extraKey"]; + + var result3 = await outer.GetResponseAsync([]); + var result4 = await outer.GetResponseAsync([]); + + Assert.Equal(2, innerCallCount); + Assert.Equal("2", result3.Text); + Assert.Equal("2", result4.Text); + + // Remove key + outer.CacheKeyAdditionalValues = []; + + var result5 = await outer.GetResponseAsync([]); + + Assert.Equal(2, innerCallCount); + Assert.Equal("1", result5.Text); + } + [Fact] public async Task SubclassCanOverrideCacheKeyToVaryByChatOptionsAsync() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/DistributedCachingEmbeddingGeneratorTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/DistributedCachingEmbeddingGeneratorTest.cs index 6153ec8ab45..b14d3de83a9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/DistributedCachingEmbeddingGeneratorTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/DistributedCachingEmbeddingGeneratorTest.cs @@ -21,6 +21,24 @@ public class DistributedCachingEmbeddingGeneratorTest AdditionalProperties = new() { ["a"] = "b" }, }; + [Fact] + public void Properties_Roundtrip() + { + using var innerGenerator = new TestEmbeddingGenerator(); + using DistributedCachingEmbeddingGenerator> generator = new(innerGenerator, _storage); + + Assert.Same(AIJsonUtilities.DefaultOptions, generator.JsonSerializerOptions); + var jso = new JsonSerializerOptions(); + generator.JsonSerializerOptions = jso; + Assert.Same(jso, generator.JsonSerializerOptions); + + Assert.Null(generator.CacheKeyAdditionalValues); + var additionalValues = new[] { "value1", "value2" }; + generator.CacheKeyAdditionalValues = additionalValues; + Assert.NotSame(additionalValues, generator.CacheKeyAdditionalValues); + Assert.Equal(additionalValues, generator.CacheKeyAdditionalValues); + } + [Fact] public async Task CachesSuccessResultsAsync() { @@ -271,6 +289,49 @@ public async Task CacheKeyVariesByEmbeddingOptionsAsync() AssertEmbeddingsEqual(new("value 2".Select(c => (float)c).ToArray()), result4); } + [Fact] + public async Task CacheKeyVariesByAdditionalKeyValuesAsync() + { + // Arrange + var innerCallCount = 0; + var completionTcs = new TaskCompletionSource(); + using var innerGenerator = new TestEmbeddingGenerator + { + GenerateAsyncCallback = async (value, options, cancellationToken) => + { + innerCallCount++; + await Task.Yield(); + return new(new Embedding[] { new Embedding(new float[] { innerCallCount }) }); + } + }; + using var outer = new DistributedCachingEmbeddingGenerator>(innerGenerator, _storage) + { + JsonSerializerOptions = TestJsonSerializerContext.Default.Options, + }; + + var result1 = await outer.GenerateAsync("abc"); + var result2 = await outer.GenerateAsync("abc"); + AssertEmbeddingsEqual(result1, result2); + + var result3 = await outer.GenerateAsync("abc"); + AssertEmbeddingsEqual(result1, result3); + + // Change key + outer.CacheKeyAdditionalValues = ["extraKey"]; + + var result4 = await outer.GenerateAsync("abc"); + Assert.NotEqual(result1.Vector.ToArray(), result4.Vector.ToArray()); + + var result5 = await outer.GenerateAsync("abc"); + AssertEmbeddingsEqual(result4, result5); + + // Remove key + outer.CacheKeyAdditionalValues = []; + + var result6 = await outer.GenerateAsync("abc"); + AssertEmbeddingsEqual(result1, result6); + } + [Fact] public async Task SubclassCanOverrideCacheKeyToVaryByOptionsAsync() { From 78f99eee8c02c599cd7eea2182abb4e0e5ca4482 Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Wed, 2 Jul 2025 16:29:25 +0700 Subject: [PATCH 166/472] Align EventId generation with M.E.Logging source-gen (#6566) Co-authored-by: Nikita --- .../Emission/Emitter.Method.cs | 4 ++- .../Generated/LogMethodTests.cs | 31 +++++++++++++++++++ .../TestClasses/EventNameTestExtensions.cs | 8 +++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs index 68549c11540..c970caf330f 100644 --- a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs +++ b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs @@ -91,7 +91,9 @@ private void GenLogMethod(LoggingMethod lm) } else { - OutLn($"new({GetNonRandomizedHashCode(eventName)}, {eventName}),"); + var eventNameToCalcId = string.IsNullOrWhiteSpace(lm.EventName) ? lm.Name : lm.EventName!; + var calculatedEventId = GetNonRandomizedHashCode(eventNameToCalcId); + OutLn($"new({calculatedEventId}, {eventName}),"); } OutLn($"{stateName},"); diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/LogMethodTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/LogMethodTests.cs index 1d74dc68713..12771c42767 100644 --- a/test/Generators/Microsoft.Gen.Logging/Generated/LogMethodTests.cs +++ b/test/Generators/Microsoft.Gen.Logging/Generated/LogMethodTests.cs @@ -580,6 +580,37 @@ public void EventNameTests() Assert.Equal("M1_Event", logRecord.Id.Name); } + [Fact] + public void EventIdTests() + { + using var logger = Utils.GetLogger(); + var collector = logger.FakeLogCollector; + + collector.Clear(); + EventNameTestExtensions.M1(LogLevel.Warning, logger, "Eight"); + Assert.Equal(1, collector.Count); + + var firstEventId = collector.LatestRecord.Id; + Assert.NotEqual(0, firstEventId.Id); + Assert.Equal("M1_Event", firstEventId.Name); + + collector.Clear(); + EventNameTestExtensions.M1_Event(logger, "Nine"); + Assert.Equal(1, collector.Count); + + var secondEventId = collector.LatestRecord.Id; + Assert.Equal(firstEventId.Id, secondEventId.Id); // Same EventName means same generated EventId + Assert.Equal(nameof(EventNameTestExtensions.M1_Event), secondEventId.Name); + + collector.Clear(); + EventNameTestExtensions.M2(logger, "Ten"); + Assert.Equal(1, collector.Count); + + var thirdEventId = collector.LatestRecord.Id; + Assert.NotEqual(thirdEventId.Id, secondEventId.Id); // Different EventName means different generated EventId + Assert.Equal(nameof(EventNameTestExtensions.M2), thirdEventId.Name); + } + [Fact] public void NestedClassTests() { diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs index 48385c1fd0a..83c239499e1 100644 --- a/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs @@ -12,5 +12,13 @@ internal static partial class EventNameTestExtensions [LoggerMessage(EventName = "M1_Event")] public static partial void M1(LogLevel level, ILogger logger, string p0); + + // This one should have the same generated EventId as the method above + [LoggerMessage(LogLevel.Debug)] + public static partial void M1_Event(ILogger logger, string p0); + + // This one should have different generated EventId as the methods above + [LoggerMessage(LogLevel.Error)] + public static partial void M2(ILogger logger, string p0); } } From 0a55b139e52b732faea8da8e078dc1b69b8b96b9 Mon Sep 17 00:00:00 2001 From: Amadeusz Lechniak Date: Wed, 2 Jul 2025 14:41:38 +0200 Subject: [PATCH 167/472] Add resiliency mechanism to CPU and memory utilization checks (#6528) * Add RetryingLinuxUtilizationParser * Switch approach to use retries in Provider class * Refactor and add test * Refactor --- .../Linux/LinuxUtilizationProvider.cs | 77 ++++++++++- .../Linux/LinuxUtilizationProviderTests.cs | 130 ++++++++++++++++++ 2 files changed, 200 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 4090bbb5619..af13b8100ba 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.Metrics; +using System.Linq; +using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -33,6 +36,10 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly double _scaleRelativeToCpuRequest; private readonly double _scaleRelativeToCpuRequestForTrackerApi; + private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(5); + private DateTimeOffset _lastFailure = DateTimeOffset.MinValue; + private int _measurementsUnavailable; + private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; @@ -94,18 +101,44 @@ public LinuxUtilizationProvider(IOptions options, ILi // Initialize the counters _cpuUtilizationLimit100PercentExceededCounter = meter.CreateCounter("cpu_utilization_limit_100_percent_exceeded"); _cpuUtilizationLimit110PercentExceededCounter = meter.CreateCounter("cpu_utilization_limit_110_percent_exceeded"); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: () => CpuUtilizationLimit(cpuLimit), unit: "1"); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuUtilizationWithoutHostDelta() / cpuRequest, unit: "1"); + + _ = meter.CreateObservableGauge( + ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), + "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationWithoutHostDelta() / cpuRequest), + unit: "1"); } else { - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuLimit, unit: "1"); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuRequest, unit: "1"); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessCpuUtilization, observeValue: () => CpuUtilization() * _scaleRelativeToCpuRequest, unit: "1"); + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuLimit), + unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuRequest), + unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessCpuUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuRequest), + unit: "1"); } - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: MemoryUtilization, unit: "1"); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessMemoryUtilization, observeValue: MemoryUtilization, unit: "1"); + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, + observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessMemoryUtilization, + observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + unit: "1"); // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core // cpuLimit is a CPU limit (aka max CPU units available) for a pod or for a host. @@ -288,4 +321,34 @@ public Snapshot GetSnapshot() userTimeSinceStart: TimeSpan.FromTicks((long)(cgroupTime / Hundred * _scaleRelativeToCpuRequestForTrackerApi)), memoryUsageInBytes: memoryUsed); } + + private IEnumerable> GetMeasurementWithRetry(Func func) + { + if (Volatile.Read(ref _measurementsUnavailable) == 1 && + _timeProvider.GetUtcNow() - _lastFailure < _retryInterval) + { + return Enumerable.Empty>(); + } + + try + { + double result = func(); + if (Volatile.Read(ref _measurementsUnavailable) == 1) + { + _ = Interlocked.Exchange(ref _measurementsUnavailable, 0); + } + + return new[] { new Measurement(result) }; + } + catch (Exception ex) when ( + ex is System.IO.FileNotFoundException || + ex is System.IO.DirectoryNotFoundException || + ex is System.UnauthorizedAccessException) + { + _lastFailure = _timeProvider.GetUtcNow(); + _ = Interlocked.Exchange(ref _measurementsUnavailable, 1); + + return Enumerable.Empty>(); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index e6e9a282eca..6ee3c40d44d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; using Microsoft.Shared.Instruments; using Microsoft.TestUtilities; using Moq; @@ -272,4 +273,133 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); } + + [Fact] + public void Provider_GetMeasurementWithRetry_HandlesExceptionAndRecovers() + { + var meterName = Guid.NewGuid().ToString(); + var logger = new FakeLogger(); + var options = Options.Options.Create(new ResourceMonitoringOptions()); + using var meter = new Meter(nameof(Provider_GetMeasurementWithRetry_HandlesExceptionAndRecovers)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var callCount = 0; + var parserMock = new Mock(); + parserMock.Setup(p => p.GetMemoryUsageInBytes()).Returns(() => + { + callCount++; + if (callCount <= 1) + { + throw new FileNotFoundException("Simulated failure to read file"); + } + + return 420UL; + }); + parserMock.Setup(p => p.GetAvailableMemoryInBytes()).Returns(1000UL); + parserMock.Setup(p => p.GetCgroupRequestCpu()).Returns(10f); + parserMock.Setup(p => p.GetCgroupLimitedCpus()).Returns(12f); + + var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow); + var provider = new LinuxUtilizationProvider(options, parserMock.Object, meterFactoryMock.Object, logger, fakeTime); + + using var listener = new MeterListener + { + InstrumentPublished = (instrument, listener) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + listener.EnableMeasurementEvents(instrument); + } + } + }; + + var samples = new List<(Instrument instrument, double value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + samples.Add((instrument, value)); + } + }); + + listener.Start(); + listener.RecordObservableInstruments(); + Assert.DoesNotContain(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + + fakeTime.Advance(TimeSpan.FromMinutes(1)); + listener.RecordObservableInstruments(); + Assert.DoesNotContain(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + + fakeTime.Advance(TimeSpan.FromMinutes(5)); + listener.RecordObservableInstruments(); + var metric = samples.SingleOrDefault(x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + Assert.Equal(0.42, metric.value); + + parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(2)); + } + + [Fact] + public void Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutureReads() + { + var meterName = Guid.NewGuid().ToString(); + var logger = new FakeLogger(); + var options = Options.Options.Create(new ResourceMonitoringOptions()); + using var meter = new Meter(nameof(Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutureReads)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var callCount = 0; + var parserMock = new Mock(); + parserMock.Setup(p => p.GetMemoryUsageInBytes()).Returns(() => + { + callCount++; + if (callCount <= 2) + { + throw new InvalidOperationException("Simulated unhandled exception"); + } + + return 1234UL; + }); + parserMock.Setup(p => p.GetAvailableMemoryInBytes()).Returns(2000UL); + parserMock.Setup(p => p.GetCgroupRequestCpu()).Returns(10f); + parserMock.Setup(p => p.GetCgroupLimitedCpus()).Returns(12f); + + var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow); + var provider = new LinuxUtilizationProvider(options, parserMock.Object, meterFactoryMock.Object, logger, fakeTime); + + using var listener = new MeterListener + { + InstrumentPublished = (instrument, listener) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + listener.EnableMeasurementEvents(instrument); + } + } + }; + + var samples = new List<(Instrument instrument, double value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + samples.Add((instrument, value)); + } + }); + + listener.Start(); + + Assert.Throws(() => listener.RecordObservableInstruments()); + Assert.DoesNotContain(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + + fakeTime.Advance(TimeSpan.FromMinutes(1)); + listener.RecordObservableInstruments(); + var metric = samples.SingleOrDefault(x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + Assert.Equal(1234f / 2000f, metric.value, 0.01f); + + parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(3)); + } } From c3b85f80ce12f6d3b33949f191c86c6cd23e5d74 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 2 Jul 2025 11:16:01 -0400 Subject: [PATCH 168/472] Add DelegatingAIFunction (#6565) * Add DelegatingAIFunction To simplify scenarios where someone wants to augment an existing AIFunction's behavior, tweak what one of its properties returns, etc. * Address PR feedback --- .../Functions/DelegatingAIFunction.cs | 61 ++++++++++++ .../Microsoft.Extensions.AI.Abstractions.json | 52 +++++++++++ .../Functions/DelegatingAIFunctionTests.cs | 92 +++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs new file mode 100644 index 00000000000..a52c5acd959 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs @@ -0,0 +1,61 @@ +// 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.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1202 // Elements should be ordered by access + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +public class DelegatingAIFunction : AIFunction +{ + /// + /// Initializes a new instance of the class as a wrapper around . + /// + /// The inner AI function to which all calls are delegated by default. + /// is . + protected DelegatingAIFunction(AIFunction innerFunction) + { + InnerFunction = Throw.IfNull(innerFunction); + } + + /// Gets the inner . + protected AIFunction InnerFunction { get; } + + /// + public override string Name => InnerFunction.Name; + + /// + public override string Description => InnerFunction.Description; + + /// + public override JsonElement JsonSchema => InnerFunction.JsonSchema; + + /// + public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema; + + /// + public override JsonSerializerOptions JsonSerializerOptions => InnerFunction.JsonSerializerOptions; + + /// + public override MethodInfo? UnderlyingMethod => InnerFunction.UnderlyingMethod; + + /// + public override IReadOnlyDictionary AdditionalProperties => InnerFunction.AdditionalProperties; + + /// + public override string ToString() => InnerFunction.ToString(); + + /// + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => + InnerFunction.InvokeAsync(arguments, cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 79776b0ecb4..5e87edc01f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1355,6 +1355,58 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.DelegatingAIFunction : Microsoft.Extensions.AI.AIFunction", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", + "Stage": "Stable" + }, + { + "Member": "override System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonElement Microsoft.Extensions.AI.DelegatingAIFunction.JsonSchema { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DelegatingAIFunction.JsonSerializerOptions { get; }", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Name { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonElement? Microsoft.Extensions.AI.DelegatingAIFunction.ReturnJsonSchema { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Reflection.MethodInfo? Microsoft.Extensions.AI.DelegatingAIFunction.UnderlyingMethod { get; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.DelegatingChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable", "Stage": "Stable", diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs new file mode 100644 index 00000000000..cfad15efdc0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs @@ -0,0 +1,92 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DelegatingAIFunctionTests +{ + [Fact] + public void Constructor_NullInnerFunction_ThrowsArgumentNullException() + { + Assert.Throws("innerFunction", () => new DerivedFunction(null!)); + } + + [Fact] + public void DefaultOverrides_DelegateToInnerFunction() + { + AIFunction expected = AIFunctionFactory.Create(() => 42); + DerivedFunction actual = new(expected); + + Assert.Same(expected, actual.InnerFunction); + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.Description, actual.Description); + Assert.Equal(expected.JsonSchema, actual.JsonSchema); + Assert.Equal(expected.ReturnJsonSchema, actual.ReturnJsonSchema); + Assert.Same(expected.JsonSerializerOptions, actual.JsonSerializerOptions); + Assert.Same(expected.UnderlyingMethod, actual.UnderlyingMethod); + Assert.Same(expected.AdditionalProperties, actual.AdditionalProperties); + Assert.Equal(expected.ToString(), actual.ToString()); + } + + private sealed class DerivedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction) + { + public new AIFunction InnerFunction => base.InnerFunction; + } + + [Fact] + public void Virtuals_AllOverridden() + { + Assert.All(typeof(DelegatingAIFunction).GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), m => + { + switch (m) + { + case MethodInfo methodInfo when methodInfo.IsVirtual && methodInfo.Name is not ("Finalize" or "Equals" or "GetHashCode"): + Assert.True(methodInfo.DeclaringType == typeof(DelegatingAIFunction), $"{methodInfo.Name} not overridden"); + break; + + case PropertyInfo propertyInfo when propertyInfo.GetMethod?.IsVirtual is true: + Assert.True(propertyInfo.DeclaringType == typeof(DelegatingAIFunction), $"{propertyInfo.Name} not overridden"); + break; + } + }); + } + + [Fact] + public async Task OverriddenInvocation_SuccessfullyInvoked() + { + bool innerInvoked = false; + AIFunction inner = AIFunctionFactory.Create(int () => + { + innerInvoked = true; + throw new Exception("uh oh"); + }, "TestFunction", "A test function for DelegatingAIFunction"); + + AIFunction actual = new OverridesInvocation(inner, (args, ct) => new ValueTask(84)); + + Assert.Equal(inner.Name, actual.Name); + Assert.Equal(inner.Description, actual.Description); + Assert.Equal(inner.JsonSchema, actual.JsonSchema); + Assert.Equal(inner.ReturnJsonSchema, actual.ReturnJsonSchema); + Assert.Same(inner.JsonSerializerOptions, actual.JsonSerializerOptions); + Assert.Same(inner.UnderlyingMethod, actual.UnderlyingMethod); + Assert.Same(inner.AdditionalProperties, actual.AdditionalProperties); + Assert.Equal(inner.ToString(), actual.ToString()); + + object? result = await actual.InvokeAsync(new(), CancellationToken.None); + Assert.Contains("84", result?.ToString()); + + Assert.False(innerInvoked); + } + + private sealed class OverridesInvocation(AIFunction innerFunction, Func> invokeAsync) : DelegatingAIFunction(innerFunction) + { + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => + invokeAsync(arguments, cancellationToken); + } +} From 7854bf3b937373693869da5513e5447ff925c6a4 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 2 Jul 2025 11:55:01 -0400 Subject: [PATCH 169/472] Suppress flaky test until fixed (#6568) Found during another PR. Recurred 2 or 3 times. Worked around by retrying the CI build. Fixed tracked by https://github.com/dotnet/extensions/issues/6567. --- .../TimerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/TimerTests.cs b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/TimerTests.cs index d0db83d943a..18d45449388 100644 --- a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/TimerTests.cs +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/TimerTests.cs @@ -178,7 +178,7 @@ public async Task Change_WhenCalledAfterDisposeAsync_ReturnsFalse() Assert.False(t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1))); } - [Fact] + [Fact(Skip = "Flaky, https://github.com/dotnet/extensions/issues/6567")] public void CreateTimer_WhenDisposed_RemovesWaiterFromQueue() { var timer1Counter = 0; From c49b57b42701303d0b4fc8c6d692c6d8ab97ce37 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 2 Jul 2025 12:06:38 -0400 Subject: [PATCH 170/472] Create a project template for an MCP server (#6547) The conventions we're pushing for on NuGet are: 1. `PackAsTool` 2. `McpServer` package type (in addition to the default `DotnetTool`) 3. Embed server.json This provides an `mcpserver` template as part of `Microsoft.Extensions.AI.Templates`. This currently only covers a local MCP server, with stdio transport. --- src/ProjectTemplates/GeneratedContent.targets | 8 ++ .../Microsoft.Extensions.AI.Templates.csproj | 10 ++ .../McpServer-CSharp/.mcp/server.json | 20 ++++ .../.template.config/dotnetcli.host.json | 7 ++ .../.template.config/ide.host.json | 6 ++ .../.template.config/ide/icon.ico | Bin 0 -> 38045 bytes .../.template.config/template.json | 46 +++++++++ .../McpServer-CSharp.csproj.in | 32 ++++++ .../src/McpServer/McpServer-CSharp/Program.cs | 16 +++ .../src/McpServer/McpServer-CSharp/README.md | 82 +++++++++++++++ .../Tools/RandomNumberTools.cs | 18 ++++ .../McpServerSnapshotTests.cs | 95 ++++++++++++++++++ .../mcpserver/.mcp/server.json | 20 ++++ .../mcpserver/Program.cs | 16 +++ .../mcpserver/README.md | 82 +++++++++++++++ .../mcpserver/Tools/RandomNumberTools.cs | 18 ++++ .../mcpserver/mcpserver.csproj | 32 ++++++ 17 files changed, 508 insertions(+) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/template.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index c3287408034..ce6ba2bc502 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -10,6 +10,7 @@ <_LocalChatTemplateVariant>aspire <_ChatWithCustomDataContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\ChatWithCustomData\ + <_McpServerContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\McpServer\ @@ -34,9 +35,11 @@ 1.14.0 11.6.0 9.4.1-beta.291 + 10.0.0-preview.5.25277.114 9.3.0 1.53.0 1.53.0-preview + 0.3.0-preview.1 5.1.18 1.12.0 0.1.10 @@ -64,9 +67,11 @@ TemplatePackageVersion_AzureIdentity=$(TemplatePackageVersion_AzureIdentity); TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire); + TemplatePackageVersion_MicrosoftExtensionsHosting=$(TemplatePackageVersion_MicrosoftExtensionsHosting); TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery); TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel); TemplatePackageVersion_MicrosoftSemanticKernel_Preview=$(TemplatePackageVersion_MicrosoftSemanticKernel_Preview); + TemplatePackageVersion_ModelContextProtocol=$(TemplatePackageVersion_ModelContextProtocol); TemplatePackageVersion_OllamaSharp=$(TemplatePackageVersion_OllamaSharp); TemplatePackageVersion_OpenTelemetry=$(TemplatePackageVersion_OpenTelemetry); TemplatePackageVersion_PdfPig=$(TemplatePackageVersion_PdfPig); @@ -100,6 +105,9 @@ + <_GeneratedContentEnablingJustBuiltPackages diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 2827734c794..7784747028e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -61,6 +61,16 @@ **\NuGet.config; **\Directory.Build.targets; **\Directory.Build.props;" /> + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json new file mode 100644 index 00000000000..d4b9d0edf5b --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 00000000000..5be51dd6357 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host", + "symbolInfo": {}, + "usageExamples": [ + "" + ] +} \ No newline at end of file diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json new file mode 100644 index 00000000000..5edf447bbd4 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/ide.host", + "order": 0, + "icon": "ide/icon.ico", + "symbolInfo": [] +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..954709ffd6b9d360fbc0b121a63a29657e0fe768 GIT binary patch literal 38045 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 + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs new file mode 100644 index 00000000000..f320c93fd88 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md new file mode 100644 index 00000000000..dc6f5038b61 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..4542f8505a5 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs new file mode 100644 index 00000000000..a3f3dedd1b5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs @@ -0,0 +1,95 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Templates.Tests; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.AI.Templates.Tests; + +public class McpServerSnapshotTests +{ + // Keep the exclude patterns below in sync with those in Microsoft.Extensions.AI.Templates.csproj. + private static readonly string[] _verificationExcludePatterns = [ + "**/bin/**", + "**/obj/**", + "**/.vs/**", + "**/*.sln", + "**/*.in", + ]; + + private readonly ILogger _log; + + public McpServerSnapshotTests(ITestOutputHelper log) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + [Fact] + public async Task BasicTest() + { + await TestTemplateCoreAsync(scenarioName: "Basic"); + } + + private async Task TestTemplateCoreAsync(string scenarioName, IEnumerable? templateArgs = null) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "mcpserver"; + + // Get the template location + string templateLocation = Path.Combine(WellKnownPaths.TemplateFeedLocation, "Microsoft.Extensions.AI.Templates", "src", "McpServer"); + + var verificationExcludePatterns = Path.DirectorySeparatorChar is '/' + ? _verificationExcludePatterns + : _verificationExcludePatterns.Select(p => p.Replace('/', Path.DirectorySeparatorChar)).ToArray(); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + TemplateSpecificArgs = templateArgs, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = scenarioName, + VerificationExcludePatterns = verificationExcludePatterns, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty.AddScrubber((path, content) => + { + string filePath = path.UnixifyDirSeparators(); + + if (filePath.EndsWith(".csproj")) + { + // Scrub references to just-built packages and remove the suffix, if it exists. + // This allows the snapshots to remain the same regardless of where the repo is built (e.g., locally, public CI, internal CI). + var pattern = @"(?<=)"; + content.ScrubByRegex(pattern, replacement: "$1"); + } + })); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + Directory.Delete(workingDir, recursive: true); + } + catch + { + /* don't care */ + } +#pragma warning restore CA1031 // Do not catch general exception types + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..ab997541e52 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md new file mode 100644 index 00000000000..25704e5d135 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..72af767e320 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..a71ac148e6f --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + From 9344962e3acf2c690c4b43a1a3a56351365296a0 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 2 Jul 2025 13:27:21 -0400 Subject: [PATCH 171/472] Add FunctionInvokingChatClient.FunctionInvoker delegate (#6564) We've had a bunch of requests to be able to customize how function invocation is handled, and while it's already possible today by deriving from FunctionInvokingChatClient and overriding its InvokeFunctionAsync, there's a lot of ceremony involved in that. By having a property on the client instance, that behavior can instead be configured as part of a UseFunctionInvocation call. --- .../FunctionInvokingChatClient.cs | 13 +++- .../Microsoft.Extensions.AI.json | 4 + .../FunctionInvokingChatClientTests.cs | 73 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 293ff1d98f1..6b1d3b3e905 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -205,6 +205,15 @@ public int MaximumConsecutiveErrorsPerRequest set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0); } + /// Gets or sets a delegate used to invoke instances. + /// + /// By default, the protected method is called for each to be invoked, + /// invoking the instance and returning its result. If this delegate is set to a non- value, + /// will replace its normal invocation with a call to this delegate, enabling + /// this delegate to assume all invocation handling of the function. + /// + public Func>? FunctionInvoker { get; set; } + /// public override async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) @@ -872,7 +881,9 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul { _ = Throw.IfNull(context); - return context.Function.InvokeAsync(context.Arguments, cancellationToken); + return FunctionInvoker is { } invoker ? + invoker(context, cancellationToken) : + context.Function.InvokeAsync(context.Arguments, cancellationToken); } private static TimeSpan GetElapsedTime(long startingTimestamp) => diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index 4f4317c9978..59ed3d32fab 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -527,6 +527,10 @@ "Member": "System.IServiceProvider? Microsoft.Extensions.AI.FunctionInvokingChatClient.FunctionInvocationServices { get; }", "Stage": "Stable" }, + { + "Member": "System.Func>? Microsoft.Extensions.AI.FunctionInvokingChatClient.FunctionInvoker { get; set; }", + "Stage": "Stable" + }, { "Member": "bool Microsoft.Extensions.AI.FunctionInvokingChatClient.IncludeDetailedErrors { get; set; }", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 26554946dca..1379cef8bf0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -37,6 +38,35 @@ public void Ctor_HasExpectedDefaults() Assert.False(client.IncludeDetailedErrors); Assert.Equal(10, client.MaximumIterationsPerRequest); Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); + Assert.Null(client.FunctionInvoker); + } + + [Fact] + public void Properties_Roundtrip() + { + using TestChatClient innerClient = new(); + using FunctionInvokingChatClient client = new(innerClient); + + Assert.False(client.AllowConcurrentInvocation); + client.AllowConcurrentInvocation = true; + Assert.True(client.AllowConcurrentInvocation); + + Assert.False(client.IncludeDetailedErrors); + client.IncludeDetailedErrors = true; + Assert.True(client.IncludeDetailedErrors); + + Assert.Equal(10, client.MaximumIterationsPerRequest); + client.MaximumIterationsPerRequest = 5; + Assert.Equal(5, client.MaximumIterationsPerRequest); + + Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); + client.MaximumConsecutiveErrorsPerRequest = 1; + Assert.Equal(1, client.MaximumConsecutiveErrorsPerRequest); + + Assert.Null(client.FunctionInvoker); + Func> invoker = (ctx, ct) => new ValueTask("test"); + client.FunctionInvoker = invoker; + Assert.Same(invoker, client.FunctionInvoker); } [Fact] @@ -208,6 +238,49 @@ public async Task ConcurrentInvocationOfParallelCallsDisabledByDefaultAsync() await InvokeAndAssertStreamingAsync(options, plan); } + [Fact] + public async Task FunctionInvokerDelegateOverridesHandlingAsync() + { + var options = new ChatOptions + { + Tools = + [ + AIFunctionFactory.Create(() => "Result 1", "Func1"), + AIFunctionFactory.Create((int i) => $"Result 2: {i}", "Func2"), + AIFunctionFactory.Create((int i) => { }, "VoidReturn"), + ] + }; + + List plan = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1 from delegate")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42 from delegate")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId3", "VoidReturn", arguments: new Dictionary { { "i", 43 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Success: Function completed.")]), + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + Func configure = b => b.Use( + s => new FunctionInvokingChatClient(s) + { + FunctionInvoker = async (ctx, cancellationToken) => + { + Assert.NotNull(ctx); + var result = await ctx.Function.InvokeAsync(ctx.Arguments, cancellationToken); + return result is JsonElement e ? + JsonSerializer.SerializeToElement($"{e.GetString()} from delegate", AIJsonUtilities.DefaultOptions) : + result; + } + }); + + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); + + await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure); + } + [Fact] public async Task ContinuesWithSuccessfulCallsUntilMaximumIterations() { From 0ba068956a6ac3cf85c887db3e5bbe085d94bbf6 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 2 Jul 2025 13:28:50 -0400 Subject: [PATCH 172/472] Use dnx instead of dotnet tool exec in template README (#6571) --- .../src/McpServer/McpServer-CSharp/README.md | 4 +--- .../Snapshots/mcpserver.Basic.verified/mcpserver/README.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md index dc6f5038b61..50091888ad8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -24,10 +24,8 @@ Once the MCP server package is published to NuGet.org, you can use the following "servers": { "McpServer-CSharp": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "tool", - "exec", "", "--version", "", diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md index 25704e5d135..5c00a3bf669 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -24,10 +24,8 @@ Once the MCP server package is published to NuGet.org, you can use the following "servers": { "mcpserver": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "tool", - "exec", "", "--version", "", From 6e2f719cb11ad82593f451a73168edd53b4a523d Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Wed, 2 Jul 2025 17:15:40 -0400 Subject: [PATCH 173/472] Add reporting tests that show NLP results. (#6574) * Add reporting tests that show NLP results. * Cleanup analyzer errors. * Add global tags for NLP * Add more precision to the evaluator timing * More tags * Add another partial match test --- .../BLEUEvaluator.cs | 2 +- .../F1Evaluator.cs | 2 +- .../GLEUEvaluator.cs | 2 +- .../EvaluationMetricExtensions.cs | 2 +- ...ons.AI.Evaluation.Integration.Tests.csproj | 1 + .../NLPEvaluatorTests.cs | 162 ++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs index e1419bd630e..f3030ec7cfb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -86,7 +86,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs index b0806be6d66..e070577c448 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs @@ -77,7 +77,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs index 0c9805ee108..60df30879a4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs @@ -86,7 +86,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs index d3012030cec..534f5e300f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs @@ -177,7 +177,7 @@ public static void AddOrUpdateChatMetadata( if (duration is not null) { - string durationText = $"{duration.Value.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.Value.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj index c08667ff421..6e3332ebca6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs new file mode 100644 index 00000000000..a4f3b75045a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take it. +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP; +using Microsoft.Extensions.AI.Evaluation.Reporting; +using Microsoft.Extensions.AI.Evaluation.Reporting.Storage; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Integration.Tests; + +[Experimental("AIEVAL001")] +public class NLPEvaluatorTests +{ + private static readonly ReportingConfiguration? _nlpReportingConfiguration; + + static NLPEvaluatorTests() + { + if (Settings.Current.Configured) + { + string version = $"Product Version: {Constants.Version}"; + string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; + string projectName = $"Project: Integration Tests"; + string testClass = $"Test Class: {nameof(NLPEvaluatorTests)}"; + string usesContext = $"Feature: Context"; + + IEvaluator bleuEvaluator = new BLEUEvaluator(); + IEvaluator gleuEvaluator = new GLEUEvaluator(); + IEvaluator f1Evaluator = new F1Evaluator(); + + _nlpReportingConfiguration = + DiskBasedReportingConfiguration.Create( + storageRootPath: Settings.Current.StorageRootPath, + evaluators: [bleuEvaluator, gleuEvaluator, f1Evaluator], + executionName: Constants.Version, + tags: [version, date, projectName, testClass, usesContext]); + } + } + + [ConditionalFact] + public async Task ExactMatch() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(ExactMatch)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + EvaluationResult result = await scenarioRun.EvaluateAsync(referenceText, [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task PartialMatch() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(PartialMatch)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + var similarText = "The brown fox quickly jumps over a lazy dog."; + EvaluationResult result = await scenarioRun.EvaluateAsync(similarText, [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task Unmatched() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(Unmatched)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + EvaluationResult result = await scenarioRun.EvaluateAsync("What is life's meaning?", [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task AdditionalContextIsNotPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(AdditionalContextIsNotPassed)}"); + + EvaluationResult result = await scenarioRun.EvaluateAsync("What is the meaning of life?"); + + Assert.True( + result.Metrics.Values.All(m => m.ContainsDiagnostics(d => d.Severity is EvaluationDiagnosticSeverity.Error)), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? bleu)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? gleu)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? f1)); + + Assert.Null(bleu.Context); + Assert.Null(gleu.Context); + Assert.Null(f1.Context); + + } + + [MemberNotNull(nameof(_nlpReportingConfiguration))] + private static void SkipIfNotConfigured() + { + if (!Settings.Current.Configured) + { + throw new SkipTestException("Test is not configured"); + } + + Assert.NotNull(_nlpReportingConfiguration); + } +} From a71873af3643fd0bede1201d2076b175d417315f Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 2 Jul 2025 15:55:40 -0700 Subject: [PATCH 174/472] Branding updates for 9.8.0 (#6573) * Branding updates for 9.8.0 * Remove unused suppressions --- eng/Versions.props | 4 +- .../CompatibilitySuppressions.xml | 46 ------------------- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 4 +- .../aichatweb/aichatweb.csproj | 4 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 4 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 2 +- .../aichatweb/aichatweb.csproj | 4 +- 10 files changed, 14 insertions(+), 60 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml diff --git a/eng/Versions.props b/eng/Versions.props index 03fa0e24fd4..5786b4478ec 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,12 +1,12 @@ 9 - 7 + 8 0 preview 1 $(MajorVersion).$(MinorVersion).$(PatchVersion) - 9.5.0 + 9.6.0 $(MajorVersion).$(MinorVersion).0.0 - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_EnableDiskIoMetrics - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_EnableDiskIoMetrics(System.Boolean) - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - \ No newline at end of file diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 8f588a6bef4..b54a7690839 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index e76ef5b53f6..e3e384f86da 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index a00ffe716f9..fd2138900b0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 8f588a6bef4..b54a7690839 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 13d362a5b15..9a1b1da5279 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 8f588a6bef4..b54a7690839 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index a69db54eabc..74fed505bde 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 01d4728712a..66360fa60a0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,9 +8,9 @@ - + - + From bbd9db7adb83df7ad5489bc028dfce553068f702 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 2 Jul 2025 23:30:25 +0000 Subject: [PATCH 175/472] Merged PR 51321: Flow .NET Servicing versions Flow .NET Servicing versions ---- #### AI description (iteration 1) #### PR Classification This PR performs a servicing update by bumping .NET dependency versions and refining CI pipeline configurations for stable releases. #### PR Summary The changes update dependency versions from 9.0.6 to 9.0.7 (and corresponding LTS versions) and adjust build settings to support release stability while streamlining internal feed configurations. - `eng/Version.Details.xml`: Updated multiple dependency version numbers and SHA values from 9.0.6 to 9.0.7. - `eng/Versions.props`: Bumped version properties (including LTS versions from 8.0.17 to 8.0.18) and enabled stable release settings by setting package stabilization to true and DotNetFinalVersionKind to release. - `NuGet.config`: Modified package source settings by adding new internal feed mappings and removing preexisting package source mapping blocks. - `azure-pipelines.yml` & `eng/pipelines/templates/BuildAndTest.yml`: Removed the CodeCoverage stage and added tasks for setting up private feed credentials, with integration tests commented out due to authentication requirements. - `Directory.Build.props`: Suppressed NU1507 warnings to accommodate internal feeds without package source mapping. --- Directory.Build.props | 5 + NuGet.config | 76 ++++++--- azure-pipelines.yml | 46 ------ eng/Version.Details.xml | 188 +++++++++++------------ eng/Versions.props | 124 +++++++-------- eng/pipelines/templates/BuildAndTest.yml | 32 +++- 6 files changed, 240 insertions(+), 231 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0af806af628..0c0fcf22bfd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,11 @@ $(NetCoreTargetFrameworks) + + + $(NoWarn);NU1507 + + false latest diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..5080151679a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -18,35 +45,40 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ec5e3d1cdb..0052dc9f706 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -239,51 +239,6 @@ extends: isWindows: false warnAsError: 0 - # ---------------------------------------------------------------- - # This stage performs quality gates enforcements - # ---------------------------------------------------------------- - - stage: codecoverage - displayName: CodeCoverage - dependsOn: - - build - condition: and(succeeded('build'), ne(variables['SkipQualityGates'], 'true')) - variables: - - template: /eng/common/templates-official/variables/pool-providers.yml@self - jobs: - - template: /eng/common/templates-official/jobs/jobs.yml@self - parameters: - enableMicrobuild: true - enableTelemetry: true - runAsPublic: ${{ variables['runAsPublic'] }} - workspace: - clean: all - - # ---------------------------------------------------------------- - # This stage downloads the code coverage reports from the build jobs, - # merges those and validates the combined test coverage. - # ---------------------------------------------------------------- - jobs: - - job: CodeCoverageReport - timeoutInMinutes: 180 - - pool: - name: NetCore1ESPool-Internal - image: 1es-mariner-2 - os: linux - - preSteps: - - checkout: self - clean: true - persistCredentials: true - fetchDepth: 1 - - steps: - - script: $(Build.SourcesDirectory)/build.sh --ci --restore - displayName: Init toolset - - - template: /eng/pipelines/templates/VerifyCoverageReport.yml - - # ---------------------------------------------------------------- # This stage only performs a build treating warnings as errors # to detect any kind of code style violations @@ -339,7 +294,6 @@ extends: parameters: validateDependsOn: - build - - codecoverage - correctness publishingInfraVersion: 3 enableSymbolValidation: false diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3a746194f1b..cb6ead9fa88 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,196 +1,196 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3875b54e7b10b10606b105340199946d0b877754 + 3c298d9f00936d651cc47d221762474e25277672 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 379bfc7b2559e7cc9f42f997a497b2f2dd8e12d2 + f6b3a5da75eb405046889a5447ec9b14cc29d285 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 8751e6d519fda94d5154187358765311ed4a4e84 + 67d253c17619e6ba325e5390905ea2a13cc7f532 diff --git a/eng/Versions.props b/eng/Versions.props index 03fa0e24fd4..ac0cc576af8 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,14 +11,14 @@ - false + true - + release true @@ -34,55 +34,55 @@ --> - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 - 9.0.6 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 + 9.0.7 - 9.0.6 + 9.0.7 9.0.0-beta.25325.4 @@ -108,8 +108,8 @@ 8.0.1 8.0.0 8.0.2 - 8.0.17 - 8.0.17 + 8.0.18 + 8.0.18 8.0.0 8.0.1 8.0.1 @@ -123,20 +123,20 @@ 8.0.2 8.0.0 8.0.0 - 8.0.5 + 8.0.6 8.0.0 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 - 8.0.17 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 + 8.0.18 - 8.0.17 + 8.0.18 + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + + CP0002 + M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll + true + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index af13b8100ba..5fb2f0a189e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -17,23 +17,16 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider { private const double One = 1.0; private const long Hundred = 100L; - private const double CpuLimitThreshold110Percent = 1.1; - // Meters to track CPU utilization threshold exceedances - private readonly Counter? _cpuUtilizationLimit100PercentExceededCounter; - private readonly Counter? _cpuUtilizationLimit110PercentExceededCounter; - - private readonly bool _useDeltaNrPeriods; private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); private readonly ILogger _logger; private readonly ILinuxUtilizationParser _parser; private readonly ulong _memoryLimit; + private readonly long _cpuPeriodsInterval; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; private readonly TimeProvider _timeProvider; - private readonly double _scaleRelativeToCpuLimit; - private readonly double _scaleRelativeToCpuRequest; private readonly double _scaleRelativeToCpuRequestForTrackerApi; private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(5); @@ -42,18 +35,11 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; - - // Track the actual timestamp when we read CPU values - private DateTimeOffset _lastCpuMeasurementTime; - private double _cpuPercentage = double.NaN; private double _lastCpuCoresUsed = double.NaN; private double _memoryPercentage; private long _previousCgroupCpuTime; private long _previousHostCpuTime; - private long _cpuUtilizationLimit100PercentExceeded; - private long _cpuUtilizationLimit110PercentExceeded; - private long _cpuPeriodsInterval; private long _previousCgroupCpuPeriodCounter; public SystemResources Resources { get; } @@ -66,7 +52,6 @@ public LinuxUtilizationProvider(IOptions options, ILi DateTimeOffset now = _timeProvider.GetUtcNow(); _cpuRefreshInterval = options.Value.CpuConsumptionRefreshInterval; _memoryRefreshInterval = options.Value.MemoryConsumptionRefreshInterval; - _useDeltaNrPeriods = options.Value.UseDeltaNrPeriodsForCpuCalculation; _refreshAfterCpu = now; _refreshAfterMemory = now; _memoryLimit = _parser.GetAvailableMemoryInBytes(); @@ -76,8 +61,8 @@ public LinuxUtilizationProvider(IOptions options, ILi float hostCpus = _parser.GetHostCpuCount(); float cpuLimit = _parser.GetCgroupLimitedCpus(); float cpuRequest = _parser.GetCgroupRequestCpu(); - _scaleRelativeToCpuLimit = hostCpus / cpuLimit; - _scaleRelativeToCpuRequest = hostCpus / cpuRequest; + float scaleRelativeToCpuLimit = hostCpus / cpuLimit; + float scaleRelativeToCpuRequest = hostCpus / cpuRequest; _scaleRelativeToCpuRequestForTrackerApi = hostCpus; // the division by cpuRequest is performed later on in the ResourceUtilization class #pragma warning disable CA2000 // Dispose objects before losing scope @@ -87,21 +72,15 @@ public LinuxUtilizationProvider(IOptions options, ILi var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope - if (options.Value.CalculateCpuUsageWithoutHostDelta) + if (options.Value.UseLinuxCalculationV2) { cpuLimit = _parser.GetCgroupLimitV2(); - - // Try to get the CPU request from cgroup cpuRequest = _parser.GetCgroupRequestCpuV2(); // Get Cpu periods interval from cgroup _cpuPeriodsInterval = _parser.GetCgroupPeriodsIntervalInMicroSecondsV2(); (_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); - // Initialize the counters - _cpuUtilizationLimit100PercentExceededCounter = meter.CreateCounter("cpu_utilization_limit_100_percent_exceeded"); - _cpuUtilizationLimit110PercentExceededCounter = meter.CreateCounter("cpu_utilization_limit_110_percent_exceeded"); - _ = meter.CreateObservableGauge( ResourceUtilizationInstruments.ContainerCpuLimitUtilization, () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), @@ -109,24 +88,24 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationWithoutHostDelta() / cpuRequest), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)), unit: "1"); } else { _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuLimit), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * scaleRelativeToCpuLimit), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuRequest), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * scaleRelativeToCpuRequest), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessCpuUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * _scaleRelativeToCpuRequest), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilization() * scaleRelativeToCpuRequest), unit: "1"); } @@ -148,10 +127,9 @@ public LinuxUtilizationProvider(IOptions options, ILi _logger.SystemResourcesInfo(cpuLimit, cpuRequest, _memoryLimit, _memoryLimit); } - public double CpuUtilizationWithoutHostDelta() + public double CpuUtilizationV2() { DateTimeOffset now = _timeProvider.GetUtcNow(); - double actualElapsedNanoseconds = (now - _lastCpuMeasurementTime).TotalNanoseconds; lock (_cpuLocker) { if (now < _refreshAfterCpu) @@ -160,79 +138,34 @@ public double CpuUtilizationWithoutHostDelta() } } - var (cpuUsageTime, cpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); + (long cpuUsageTime, long cpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); lock (_cpuLocker) { - if (now >= _refreshAfterCpu) + if (now < _refreshAfterCpu) { - long deltaCgroup = cpuUsageTime - _previousCgroupCpuTime; - double coresUsed; - - if (_useDeltaNrPeriods) - { - long deltaPeriodCount = cpuPeriodCounter - _previousCgroupCpuPeriodCounter; - long deltaCpuPeriodInNanoseconds = deltaPeriodCount * _cpuPeriodsInterval * 1000; - - if (deltaCgroup > 0 && deltaPeriodCount > 0) - { - coresUsed = deltaCgroup / (double)deltaCpuPeriodInNanoseconds; - - _logger.CpuUsageDataV2(cpuUsageTime, _previousCgroupCpuTime, deltaCpuPeriodInNanoseconds, coresUsed); - - _lastCpuCoresUsed = coresUsed; - _refreshAfterCpu = now.Add(_cpuRefreshInterval); - _previousCgroupCpuTime = cpuUsageTime; - _previousCgroupCpuPeriodCounter = cpuPeriodCounter; - } - } - else - { - if (deltaCgroup > 0) - { - coresUsed = deltaCgroup / actualElapsedNanoseconds; - - _logger.CpuUsageDataV2(cpuUsageTime, _previousCgroupCpuTime, actualElapsedNanoseconds, coresUsed); - - _lastCpuCoresUsed = coresUsed; - _refreshAfterCpu = now.Add(_cpuRefreshInterval); - _previousCgroupCpuTime = cpuUsageTime; - - // Update the timestamp for next calculation - _lastCpuMeasurementTime = now; - } - } + return _lastCpuCoresUsed; } - } - return _lastCpuCoresUsed; - } + long deltaCgroup = cpuUsageTime - _previousCgroupCpuTime; + long deltaPeriodCount = cpuPeriodCounter - _previousCgroupCpuPeriodCounter; - /// - /// Calculates CPU utilization relative to the CPU limit. - /// - /// The CPU limit to use for the calculation. - /// CPU usage as a ratio of the limit. - public double CpuUtilizationLimit(float cpuLimit) - { - double utilization = CpuUtilizationWithoutHostDelta() / cpuLimit; + if (deltaCgroup <= 0 || deltaPeriodCount <= 0) + { + return _lastCpuCoresUsed; + } - // Increment counter if utilization exceeds 1 (100%) - if (utilization > 1.0) - { - _cpuUtilizationLimit100PercentExceededCounter?.Add(1); - _cpuUtilizationLimit100PercentExceeded++; - _logger.CounterMessage100(_cpuUtilizationLimit100PercentExceeded); - } + long deltaCpuPeriodInNanoseconds = deltaPeriodCount * _cpuPeriodsInterval * 1000; + double coresUsed = deltaCgroup / (double)deltaCpuPeriodInNanoseconds; - // Increment counter if utilization exceeds 110% - if (utilization > CpuLimitThreshold110Percent) - { - _cpuUtilizationLimit110PercentExceededCounter?.Add(1); - _cpuUtilizationLimit110PercentExceeded++; - _logger.CounterMessage110(_cpuUtilizationLimit110PercentExceeded); + _logger.CpuUsageDataV2(cpuUsageTime, _previousCgroupCpuTime, deltaCpuPeriodInNanoseconds, coresUsed); + + _lastCpuCoresUsed = coresUsed; + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + _previousCgroupCpuTime = cpuUsageTime; + _previousCgroupCpuPeriodCounter = cpuPeriodCounter; } - return utilization; + return _lastCpuCoresUsed; } public double CpuUtilization() @@ -252,23 +185,27 @@ public double CpuUtilization() lock (_cpuLocker) { - if (now >= _refreshAfterCpu) + if (now < _refreshAfterCpu) { - long deltaHost = hostCpuTime - _previousHostCpuTime; - long deltaCgroup = cgroupCpuTime - _previousCgroupCpuTime; - - if (deltaHost > 0 && deltaCgroup > 0) - { - double percentage = Math.Min(One, (double)deltaCgroup / deltaHost); + return _cpuPercentage; + } - _logger.CpuUsageData(cgroupCpuTime, hostCpuTime, _previousCgroupCpuTime, _previousHostCpuTime, percentage); + long deltaHost = hostCpuTime - _previousHostCpuTime; + long deltaCgroup = cgroupCpuTime - _previousCgroupCpuTime; - _cpuPercentage = percentage; - _refreshAfterCpu = now.Add(_cpuRefreshInterval); - _previousCgroupCpuTime = cgroupCpuTime; - _previousHostCpuTime = hostCpuTime; - } + if (deltaHost <= 0 || deltaCgroup <= 0) + { + return _cpuPercentage; } + + double percentage = Math.Min(One, (double)deltaCgroup / deltaHost); + + _logger.CpuUsageData(cgroupCpuTime, hostCpuTime, _previousCgroupCpuTime, _previousHostCpuTime, percentage); + + _cpuPercentage = percentage; + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + _previousCgroupCpuTime = cgroupCpuTime; + _previousHostCpuTime = hostCpuTime; } return _cpuPercentage; @@ -351,4 +288,9 @@ ex is System.IO.DirectoryNotFoundException || return Enumerable.Empty>(); } } + + // Math.Min() is used below to mitigate margin errors and various kinds of precisions losses + // due to the fact that the calculation itself is not an atomic operation: + private double CpuUtilizationRequest(double cpuRequest) => Math.Min(One, CpuUtilizationV2() / cpuRequest); + private double CpuUtilizationLimit(double cpuLimit) => Math.Min(One, CpuUtilizationV2() / cpuLimit); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs index b78f64ddfe0..209a495e844 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs @@ -50,19 +50,7 @@ public static partial void CpuUsageDataV2( double actualElapsedNanoseconds, double cpuCores); - [LoggerMessage(5, LogLevel.Debug, - "CPU utilization exceeded 100%: Counter = {counterValue}")] - public static partial void CounterMessage100( - this ILogger logger, - long counterValue); - - [LoggerMessage(6, LogLevel.Debug, - "CPU utilization exceeded 110%: Counter = {counterValue}")] - public static partial void CounterMessage110( - this ILogger logger, - long counterValue); - - [LoggerMessage(7, LogLevel.Warning, + [LoggerMessage(5, LogLevel.Warning, "Error while getting disk stats: Error={errorMessage}")] public static partial void HandleDiskStatsException( this ILogger logger, diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs index 420d6001f57..eb890da291e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs @@ -100,23 +100,18 @@ public partial class ResourceMonitoringOptions public TimeSpan MemoryConsumptionRefreshInterval { get; set; } = DefaultRefreshInterval; /// - /// Gets or sets a value indicating whether CPU metrics are calculated via cgroup CPU limits instead of Host CPU delta. + /// Gets or sets a value indicating whether CPU metrics for Linux are calculated using V2 method - via cgroup CPU limits instead of Host CPU delta. /// /// /// The default value is . /// + /// + /// This applies to cgroups v2 only and not supported on cgroups v1. + /// This is a more accurate way to calculate CPU utilization on Linux systems, please enable if possible. + /// It will be the default in the future. + /// [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] - public bool CalculateCpuUsageWithoutHostDelta { get; set; } - - /// - /// Gets or sets a value indicating whether to use the number of periods in cpu.stat for cgroup CPU usage. - /// We use delta time for CPU usage calculation when this flag is not set. - /// - /// The default value is . - /// - /// - [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] - public bool UseDeltaNrPeriodsForCpuCalculation { get; set; } + public bool UseLinuxCalculationV2 { get; set; } /// /// Gets or sets a value indicating whether disk I/O metrics should be enabled. diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index 3b3e4f80ea2..c0a230c84ea 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -89,22 +89,6 @@ internal static class ResourceUtilizationInstruments /// The type of an instrument is . /// public const string SystemNetworkConnections = "system.network.connections"; - - /// - /// The name of an instrument to count occurrences when CPU utilization exceeds 100% of the limit. - /// - /// - /// The type of an instrument is . - /// - public const string CpuUtilizationLimit100PercentExceeded = "cpu.utilization.limit.100percent.exceeded"; - - /// - /// The name of an instrument to count occurrences when CPU utilization exceeds 110% of the limit. - /// - /// - /// The type of an instrument is . - /// - public const string CpuUtilizationLimit110PercentExceeded = "cpu.utilization.limit.110percent.exceeded"; } #pragma warning disable CS1574 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index efaaea1a51a..b71ea1c47d2 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -359,82 +359,6 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou return Task.CompletedTask; } - [ConditionalFact] - [CombinatorialData] - [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] - public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_v2() - { - var cpuRefresh = TimeSpan.FromMinutes(13); - var memoryRefresh = TimeSpan.FromMinutes(14); - var fileSystem = new HardcodedValueFileSystem(new Dictionary - { - { new FileInfo("/proc/self/cgroup"), "0::/fakeslice"}, - { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, - { new FileInfo("/sys/fs/cgroup/fakeslice/cpu.stat"), "usage_usec 1020000\nnr_periods 50"}, - { new FileInfo("/sys/fs/cgroup/memory.max"), "1048576" }, - { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"}, - { new FileInfo("/sys/fs/cgroup/cpuset.cpus.effective"), "0-19"}, - { new FileInfo("/sys/fs/cgroup/fakeslice/cpu.max"), "40000 10000"}, - { new FileInfo("/sys/fs/cgroup/fakeslice/cpu.weight"), "79"}, - }); - - using var listener = new MeterListener(); - var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); - var cpuFromGauge = 0.0d; - var cpuLimitFromGauge = 0.0d; - var cpuRequestFromGauge = 0.0d; - var memoryFromGauge = 0.0d; - var memoryLimitFromGauge = 0.0d; - using var e = new ManualResetEventSlim(); - - object? meterScope = null; - listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) - => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); - listener.Start(); - - using var host = FakeHost.CreateBuilder() - .ConfigureServices(x => - x.AddLogging() - .AddSingleton(clock) - .AddSingleton(new FakeUserHz(100)) - .AddSingleton(fileSystem) - .AddSingleton(new GenericPublisher(_ => e.Set())) - .AddResourceMonitoring(x => x.ConfigureMonitor(options => options.CalculateCpuUsageWithoutHostDelta = true)) - .Replace(ServiceDescriptor.Singleton())) - .Build(); - - meterScope = host.Services.GetRequiredService(); - var tracker = host.Services.GetService(); - Assert.NotNull(tracker); - - _ = host.RunAsync(); - - listener.RecordObservableInstruments(); - - var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); - - fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu 11 10 10 10 10 10 10 10 10 10"); - fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/fakeslice/cpu.stat"), "usage_usec 1120000\nnr_periods 56"); - fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.current"), "524298"); - fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.stat"), "inactive_file 10"); - - clock.Advance(TimeSpan.FromSeconds(1)); - listener.RecordObservableInstruments(); - - e.Wait(); - - utilization = tracker.GetUtilization(TimeSpan.FromSeconds(1)); - - var roundedCpuUsedPercentage = Math.Round(utilization.CpuUsedPercentage, 1); - - Assert.Equal(0, Math.Round(cpuLimitFromGauge * 100)); - Assert.Equal(0, Math.Round(cpuRequestFromGauge * 100)); - - return Task.CompletedTask; - } - [ConditionalFact] [CombinatorialData] [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] @@ -479,8 +403,7 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou .AddSingleton(new GenericPublisher(_ => e.Set())) .AddResourceMonitoring(x => x.ConfigureMonitor(options => { - options.CalculateCpuUsageWithoutHostDelta = true; - options.UseDeltaNrPeriodsForCpuCalculation = true; + options.UseLinuxCalculationV2 = true; })) .Replace(ServiceDescriptor.Singleton())) .Build(); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index 6ee3c40d44d..57b92bcfa85 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -213,7 +213,7 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() { var meterName = Guid.NewGuid().ToString(); var logger = new FakeLogger(); - var options = Options.Options.Create(new ResourceMonitoringOptions { CalculateCpuUsageWithoutHostDelta = true }); + var options = Options.Options.Create(new ResourceMonitoringOptions { UseLinuxCalculationV2 = true }); using var meter = new Meter(nameof(Provider_Registers_Instruments_CgroupV2_WithoutHostCpu)); var meterFactoryMock = new Mock(); meterFactoryMock.Setup(x => x.Create(It.IsAny())) diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringOptionsTests.cs index 1a40889fd72..ca3126e8b97 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringOptionsTests.cs @@ -19,7 +19,7 @@ public void Basic() }; Assert.NotNull(options); - Assert.False(options.CalculateCpuUsageWithoutHostDelta); + Assert.False(options.UseLinuxCalculationV2); } [Fact] @@ -27,9 +27,9 @@ public void CalculateCpuUsageWithoutHostDelta_WhenSet_ReturnsExpectedValue() { var options = new ResourceMonitoringOptions { - CalculateCpuUsageWithoutHostDelta = true + UseLinuxCalculationV2 = true }; - Assert.True(options.CalculateCpuUsageWithoutHostDelta); + Assert.True(options.UseLinuxCalculationV2); } } From 2c2f51f35edbdb4dcb3ee1c52c384f70ae2f463e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 3 Jul 2025 14:09:56 +0300 Subject: [PATCH 179/472] AIFunctionFactory: tolerate JSON string function parameters. (#6572) * AIFunctionFactory: tolerate JSON string function parameters. * Add debug assertion. * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs Co-authored-by: Stephen Toub * Add regex-based JSON string recognition and add more tests. --------- Co-authored-by: Stephen Toub --- .../Functions/AIFunctionFactory.cs | 47 ++++++++++++++ .../ChatClientIntegrationTests.cs | 33 ++++++++++ .../Functions/AIFunctionFactoryTest.cs | 61 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 320df4098a3..e864923883e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -26,6 +26,7 @@ #pragma warning disable S2333 // Redundant modifiers should not be used #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable SA1202 // Public members should come before private members +#pragma warning disable SA1203 // Constants should appear before fields namespace Microsoft.Extensions.AI; @@ -825,6 +826,23 @@ static bool IsAsyncMethod(MethodInfo method) { try { + if (value is string text && IsPotentiallyJson(text)) + { + Debug.Assert(typeInfo.Type != typeof(string), "string parameters should not enter this branch."); + + // Account for the parameter potentially being a JSON string. + // The value is a string but the type is not. Try to deserialize it under the assumption that it's JSON. + // If it's not, we'll fall through to the default path that makes it valid JSON and then tries to deserialize. + try + { + return JsonSerializer.Deserialize(text, typeInfo); + } + catch (JsonException) + { + // If the string is not valid JSON, fall through to the round-trip. + } + } + string json = JsonSerializer.Serialize(value, serializerOptions.GetTypeInfo(value.GetType())); return JsonSerializer.Deserialize(json, typeInfo); } @@ -1021,6 +1039,35 @@ private record struct DescriptorKey( AIJsonSchemaCreateOptions SchemaOptions); } + /// + /// Quickly checks if the specified string is potentially JSON + /// by checking if the first non-whitespace characters are valid JSON start tokens. + /// + /// The string to check. + /// If then the string is definitely not valid JSON. + private static bool IsPotentiallyJson(string value) => PotentiallyJsonRegex().IsMatch(value); +#if NET + [GeneratedRegex(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace)] + private static partial Regex PotentiallyJsonRegex(); +#else + private static Regex PotentiallyJsonRegex() => _potentiallyJsonRegex; + private static readonly Regex _potentiallyJsonRegex = new(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); +#endif + private const string PotentiallyJsonRegexString = """ + ^\s* # Optional whitespace at the start of the string + ( null # null literal + | false # false literal + | true # true literal + | \d # positive number + | -\d # negative number + | " # string + | \[ # start array + | { # start object + | // # Start of single-line comment + | /\* # Start of multi-line comment + ) + """; + /// /// Removes characters from a .NET member name that shouldn't be used in an AI function name. /// diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index d84d767fd4c..ffa94f64531 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -341,6 +341,39 @@ public virtual async Task FunctionInvocation_NestedParameters() AssertUsageAgainstActivities(response, activities); } + [ConditionalFact] + public virtual async Task FunctionInvocation_ArrayParameter() + { + SkipIfNotEnabled(); + + var sourceName = Guid.NewGuid().ToString(); + var activities = new List(); + using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddInMemoryExporter(activities) + .Build(); + + using var chatClient = new FunctionInvokingChatClient( + new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + + List messages = + [ + new(ChatRole.User, "Can you add bacon, lettuce, and tomatoes to Peter's shopping cart?") + ]; + + string? shopperName = null; + List shoppingCart = []; + AIFunction func = AIFunctionFactory.Create((string[] items, string shopperId) => { shoppingCart.AddRange(items); shopperName = shopperId; }, "AddItemsToShoppingCart"); + var response = await chatClient.GetResponseAsync(messages, new() + { + Tools = [func] + }); + + Assert.Equal("Peter", shopperName); + Assert.Equal(["bacon", "lettuce", "tomatoes"], shoppingCart); + AssertUsageAgainstActivities(response, activities); + } + private static void AssertUsageAgainstActivities(ChatResponse response, List activities) { // If the underlying IChatClient provides usage data, function invocation should aggregate the diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 84298788e8c..b15d200a39a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Reflection; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -75,6 +76,66 @@ public async Task Parameters_MissingRequiredParametersFail_Async() } } + [Fact] + public async Task Parameters_ToleratesJsonEncodedParameters() + { + AIFunction func = AIFunctionFactory.Create((int x, int y, int z, int w, int u) => x + y + z + w + u); + + var result = await func.InvokeAsync(new() + { + ["x"] = "1", + ["y"] = JsonNode.Parse("2"), + ["z"] = JsonDocument.Parse("3"), + ["w"] = JsonDocument.Parse("4").RootElement, + ["u"] = 5M, // boxed decimal cannot be cast to int, requires conversion + }); + + AssertExtensions.EqualFunctionCallResults(15, result); + } + + [Theory] + [InlineData(" null")] + [InlineData(" false ")] + [InlineData("true ")] + [InlineData("42")] + [InlineData("0.0")] + [InlineData("-1e15")] + [InlineData(" \"I am a string!\" ")] + [InlineData(" {}")] + [InlineData("[]")] + public async Task Parameters_ToleratesJsonStringParameters(string jsonStringParam) + { + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); + JsonElement expectedResult = JsonDocument.Parse(jsonStringParam).RootElement; + + var result = await func.InvokeAsync(new() + { + ["param"] = jsonStringParam + }); + + AssertExtensions.EqualFunctionCallResults(expectedResult, result); + } + + [Theory] + [InlineData("")] + [InlineData(" \r\n")] + [InlineData("I am a string!")] + [InlineData("/* Code snippet */ int main(void) { return 0; }")] + [InlineData("let rec Y F x = F (Y F) x")] + [InlineData("+3")] + public async Task Parameters_ToleratesInvalidJsonStringParameters(string invalidJsonParam) + { + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); + JsonElement expectedResult = JsonDocument.Parse(JsonSerializer.Serialize(invalidJsonParam, JsonContext.Default.String)).RootElement; + + var result = await func.InvokeAsync(new() + { + ["param"] = invalidJsonParam + }); + + AssertExtensions.EqualFunctionCallResults(expectedResult, result); + } + [Fact] public async Task Parameters_MappedByType_Async() { From 3b9643582907b4f9981abd1f035df987b7224ddd Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 3 Jul 2025 18:14:36 +0300 Subject: [PATCH 180/472] AIFunctionFactory: add test coverage for JSON comments. (#6576) --- .../Functions/AIFunctionFactory.cs | 3 +-- .../Functions/AIFunctionFactoryTest.cs | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index e864923883e..5ad178e7bc8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -1058,8 +1058,7 @@ private record struct DescriptorKey( ( null # null literal | false # false literal | true # true literal - | \d # positive number - | -\d # negative number + | -?[0-9]# number | " # string | \[ # start array | { # start object diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index b15d200a39a..afced22038f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -103,10 +103,13 @@ public async Task Parameters_ToleratesJsonEncodedParameters() [InlineData(" \"I am a string!\" ")] [InlineData(" {}")] [InlineData("[]")] + [InlineData("// single-line comment\r\nnull")] + [InlineData("/* multi-line\r\ncomment */\r\nnull")] public async Task Parameters_ToleratesJsonStringParameters(string jsonStringParam) { - AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); - JsonElement expectedResult = JsonDocument.Parse(jsonStringParam).RootElement; + JsonSerializerOptions options = new(AIJsonUtilities.DefaultOptions) { ReadCommentHandling = JsonCommentHandling.Skip }; + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param, serializerOptions: options); + JsonElement expectedResult = JsonDocument.Parse(jsonStringParam, new() { CommentHandling = JsonCommentHandling.Skip }).RootElement; var result = await func.InvokeAsync(new() { From a1bf53c8eb2a1cd8f58ff928a22e9168a0339746 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 2 Jul 2025 11:16:01 -0400 Subject: [PATCH 181/472] Add DelegatingAIFunction (#6565) * Add DelegatingAIFunction To simplify scenarios where someone wants to augment an existing AIFunction's behavior, tweak what one of its properties returns, etc. * Address PR feedback --- .../Functions/DelegatingAIFunction.cs | 61 ++++++++++++ .../Microsoft.Extensions.AI.Abstractions.json | 52 +++++++++++ .../Functions/DelegatingAIFunctionTests.cs | 92 +++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs new file mode 100644 index 00000000000..a52c5acd959 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs @@ -0,0 +1,61 @@ +// 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.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1202 // Elements should be ordered by access + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +public class DelegatingAIFunction : AIFunction +{ + /// + /// Initializes a new instance of the class as a wrapper around . + /// + /// The inner AI function to which all calls are delegated by default. + /// is . + protected DelegatingAIFunction(AIFunction innerFunction) + { + InnerFunction = Throw.IfNull(innerFunction); + } + + /// Gets the inner . + protected AIFunction InnerFunction { get; } + + /// + public override string Name => InnerFunction.Name; + + /// + public override string Description => InnerFunction.Description; + + /// + public override JsonElement JsonSchema => InnerFunction.JsonSchema; + + /// + public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema; + + /// + public override JsonSerializerOptions JsonSerializerOptions => InnerFunction.JsonSerializerOptions; + + /// + public override MethodInfo? UnderlyingMethod => InnerFunction.UnderlyingMethod; + + /// + public override IReadOnlyDictionary AdditionalProperties => InnerFunction.AdditionalProperties; + + /// + public override string ToString() => InnerFunction.ToString(); + + /// + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => + InnerFunction.InvokeAsync(arguments, cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 79776b0ecb4..5e87edc01f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1355,6 +1355,58 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.DelegatingAIFunction : Microsoft.Extensions.AI.AIFunction", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", + "Stage": "Stable" + }, + { + "Member": "override System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonElement Microsoft.Extensions.AI.DelegatingAIFunction.JsonSchema { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DelegatingAIFunction.JsonSerializerOptions { get; }", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Name { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Text.Json.JsonElement? Microsoft.Extensions.AI.DelegatingAIFunction.ReturnJsonSchema { get; }", + "Stage": "Stable" + }, + { + "Member": "override System.Reflection.MethodInfo? Microsoft.Extensions.AI.DelegatingAIFunction.UnderlyingMethod { get; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.DelegatingChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable", "Stage": "Stable", diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs new file mode 100644 index 00000000000..cfad15efdc0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs @@ -0,0 +1,92 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DelegatingAIFunctionTests +{ + [Fact] + public void Constructor_NullInnerFunction_ThrowsArgumentNullException() + { + Assert.Throws("innerFunction", () => new DerivedFunction(null!)); + } + + [Fact] + public void DefaultOverrides_DelegateToInnerFunction() + { + AIFunction expected = AIFunctionFactory.Create(() => 42); + DerivedFunction actual = new(expected); + + Assert.Same(expected, actual.InnerFunction); + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.Description, actual.Description); + Assert.Equal(expected.JsonSchema, actual.JsonSchema); + Assert.Equal(expected.ReturnJsonSchema, actual.ReturnJsonSchema); + Assert.Same(expected.JsonSerializerOptions, actual.JsonSerializerOptions); + Assert.Same(expected.UnderlyingMethod, actual.UnderlyingMethod); + Assert.Same(expected.AdditionalProperties, actual.AdditionalProperties); + Assert.Equal(expected.ToString(), actual.ToString()); + } + + private sealed class DerivedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction) + { + public new AIFunction InnerFunction => base.InnerFunction; + } + + [Fact] + public void Virtuals_AllOverridden() + { + Assert.All(typeof(DelegatingAIFunction).GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), m => + { + switch (m) + { + case MethodInfo methodInfo when methodInfo.IsVirtual && methodInfo.Name is not ("Finalize" or "Equals" or "GetHashCode"): + Assert.True(methodInfo.DeclaringType == typeof(DelegatingAIFunction), $"{methodInfo.Name} not overridden"); + break; + + case PropertyInfo propertyInfo when propertyInfo.GetMethod?.IsVirtual is true: + Assert.True(propertyInfo.DeclaringType == typeof(DelegatingAIFunction), $"{propertyInfo.Name} not overridden"); + break; + } + }); + } + + [Fact] + public async Task OverriddenInvocation_SuccessfullyInvoked() + { + bool innerInvoked = false; + AIFunction inner = AIFunctionFactory.Create(int () => + { + innerInvoked = true; + throw new Exception("uh oh"); + }, "TestFunction", "A test function for DelegatingAIFunction"); + + AIFunction actual = new OverridesInvocation(inner, (args, ct) => new ValueTask(84)); + + Assert.Equal(inner.Name, actual.Name); + Assert.Equal(inner.Description, actual.Description); + Assert.Equal(inner.JsonSchema, actual.JsonSchema); + Assert.Equal(inner.ReturnJsonSchema, actual.ReturnJsonSchema); + Assert.Same(inner.JsonSerializerOptions, actual.JsonSerializerOptions); + Assert.Same(inner.UnderlyingMethod, actual.UnderlyingMethod); + Assert.Same(inner.AdditionalProperties, actual.AdditionalProperties); + Assert.Equal(inner.ToString(), actual.ToString()); + + object? result = await actual.InvokeAsync(new(), CancellationToken.None); + Assert.Contains("84", result?.ToString()); + + Assert.False(innerInvoked); + } + + private sealed class OverridesInvocation(AIFunction innerFunction, Func> invokeAsync) : DelegatingAIFunction(innerFunction) + { + protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => + invokeAsync(arguments, cancellationToken); + } +} From 498c427539298e8e6105c8c67b1430d9eeed5cec Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 2 Jul 2025 12:06:38 -0400 Subject: [PATCH 182/472] Create a project template for an MCP server (#6547) The conventions we're pushing for on NuGet are: 1. `PackAsTool` 2. `McpServer` package type (in addition to the default `DotnetTool`) 3. Embed server.json This provides an `mcpserver` template as part of `Microsoft.Extensions.AI.Templates`. This currently only covers a local MCP server, with stdio transport. --- src/ProjectTemplates/GeneratedContent.targets | 8 ++ .../Microsoft.Extensions.AI.Templates.csproj | 10 ++ .../McpServer-CSharp/.mcp/server.json | 20 ++++ .../.template.config/dotnetcli.host.json | 7 ++ .../.template.config/ide.host.json | 6 ++ .../.template.config/ide/icon.ico | Bin 0 -> 38045 bytes .../.template.config/template.json | 46 +++++++++ .../McpServer-CSharp.csproj.in | 32 ++++++ .../src/McpServer/McpServer-CSharp/Program.cs | 16 +++ .../src/McpServer/McpServer-CSharp/README.md | 82 +++++++++++++++ .../Tools/RandomNumberTools.cs | 18 ++++ .../McpServerSnapshotTests.cs | 95 ++++++++++++++++++ .../mcpserver/.mcp/server.json | 20 ++++ .../mcpserver/Program.cs | 16 +++ .../mcpserver/README.md | 82 +++++++++++++++ .../mcpserver/Tools/RandomNumberTools.cs | 18 ++++ .../mcpserver/mcpserver.csproj | 32 ++++++ 17 files changed, 508 insertions(+) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/template.json create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index c3287408034..ce6ba2bc502 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -10,6 +10,7 @@ <_LocalChatTemplateVariant>aspire <_ChatWithCustomDataContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\ChatWithCustomData\ + <_McpServerContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\McpServer\ @@ -34,9 +35,11 @@ 1.14.0 11.6.0 9.4.1-beta.291 + 10.0.0-preview.5.25277.114 9.3.0 1.53.0 1.53.0-preview + 0.3.0-preview.1 5.1.18 1.12.0 0.1.10 @@ -64,9 +67,11 @@ TemplatePackageVersion_AzureIdentity=$(TemplatePackageVersion_AzureIdentity); TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire); + TemplatePackageVersion_MicrosoftExtensionsHosting=$(TemplatePackageVersion_MicrosoftExtensionsHosting); TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery); TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel); TemplatePackageVersion_MicrosoftSemanticKernel_Preview=$(TemplatePackageVersion_MicrosoftSemanticKernel_Preview); + TemplatePackageVersion_ModelContextProtocol=$(TemplatePackageVersion_ModelContextProtocol); TemplatePackageVersion_OllamaSharp=$(TemplatePackageVersion_OllamaSharp); TemplatePackageVersion_OpenTelemetry=$(TemplatePackageVersion_OpenTelemetry); TemplatePackageVersion_PdfPig=$(TemplatePackageVersion_PdfPig); @@ -100,6 +105,9 @@ + <_GeneratedContentEnablingJustBuiltPackages diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 2827734c794..7784747028e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -61,6 +61,16 @@ **\NuGet.config; **\Directory.Build.targets; **\Directory.Build.props;" /> + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json new file mode 100644 index 00000000000..d4b9d0edf5b --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 00000000000..5be51dd6357 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host", + "symbolInfo": {}, + "usageExamples": [ + "" + ] +} \ No newline at end of file diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json new file mode 100644 index 00000000000..5edf447bbd4 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/ide.host", + "order": 0, + "icon": "ide/icon.ico", + "symbolInfo": [] +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..954709ffd6b9d360fbc0b121a63a29657e0fe768 GIT binary patch literal 38045 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 + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs new file mode 100644 index 00000000000..f320c93fd88 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md new file mode 100644 index 00000000000..dc6f5038b61 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..4542f8505a5 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs new file mode 100644 index 00000000000..a3f3dedd1b5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs @@ -0,0 +1,95 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Templates.Tests; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.AI.Templates.Tests; + +public class McpServerSnapshotTests +{ + // Keep the exclude patterns below in sync with those in Microsoft.Extensions.AI.Templates.csproj. + private static readonly string[] _verificationExcludePatterns = [ + "**/bin/**", + "**/obj/**", + "**/.vs/**", + "**/*.sln", + "**/*.in", + ]; + + private readonly ILogger _log; + + public McpServerSnapshotTests(ITestOutputHelper log) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + [Fact] + public async Task BasicTest() + { + await TestTemplateCoreAsync(scenarioName: "Basic"); + } + + private async Task TestTemplateCoreAsync(string scenarioName, IEnumerable? templateArgs = null) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "mcpserver"; + + // Get the template location + string templateLocation = Path.Combine(WellKnownPaths.TemplateFeedLocation, "Microsoft.Extensions.AI.Templates", "src", "McpServer"); + + var verificationExcludePatterns = Path.DirectorySeparatorChar is '/' + ? _verificationExcludePatterns + : _verificationExcludePatterns.Select(p => p.Replace('/', Path.DirectorySeparatorChar)).ToArray(); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + TemplateSpecificArgs = templateArgs, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = scenarioName, + VerificationExcludePatterns = verificationExcludePatterns, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty.AddScrubber((path, content) => + { + string filePath = path.UnixifyDirSeparators(); + + if (filePath.EndsWith(".csproj")) + { + // Scrub references to just-built packages and remove the suffix, if it exists. + // This allows the snapshots to remain the same regardless of where the repo is built (e.g., locally, public CI, internal CI). + var pattern = @"(?<=)"; + content.ScrubByRegex(pattern, replacement: "$1"); + } + })); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + Directory.Delete(workingDir, recursive: true); + } + catch + { + /* don't care */ + } +#pragma warning restore CA1031 // Do not catch general exception types + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..ab997541e52 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md new file mode 100644 index 00000000000..25704e5d135 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..72af767e320 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..a71ac148e6f --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + From 7780060c1f13f9c2fd35a68ac486d7d4f74ca7c4 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 2 Jul 2025 13:27:21 -0400 Subject: [PATCH 183/472] Add FunctionInvokingChatClient.FunctionInvoker delegate (#6564) We've had a bunch of requests to be able to customize how function invocation is handled, and while it's already possible today by deriving from FunctionInvokingChatClient and overriding its InvokeFunctionAsync, there's a lot of ceremony involved in that. By having a property on the client instance, that behavior can instead be configured as part of a UseFunctionInvocation call. --- .../FunctionInvokingChatClient.cs | 13 +++- .../Microsoft.Extensions.AI.json | 4 + .../FunctionInvokingChatClientTests.cs | 73 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 293ff1d98f1..6b1d3b3e905 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -205,6 +205,15 @@ public int MaximumConsecutiveErrorsPerRequest set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0); } + /// Gets or sets a delegate used to invoke instances. + /// + /// By default, the protected method is called for each to be invoked, + /// invoking the instance and returning its result. If this delegate is set to a non- value, + /// will replace its normal invocation with a call to this delegate, enabling + /// this delegate to assume all invocation handling of the function. + /// + public Func>? FunctionInvoker { get; set; } + /// public override async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) @@ -872,7 +881,9 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul { _ = Throw.IfNull(context); - return context.Function.InvokeAsync(context.Arguments, cancellationToken); + return FunctionInvoker is { } invoker ? + invoker(context, cancellationToken) : + context.Function.InvokeAsync(context.Arguments, cancellationToken); } private static TimeSpan GetElapsedTime(long startingTimestamp) => diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index 4f4317c9978..59ed3d32fab 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -527,6 +527,10 @@ "Member": "System.IServiceProvider? Microsoft.Extensions.AI.FunctionInvokingChatClient.FunctionInvocationServices { get; }", "Stage": "Stable" }, + { + "Member": "System.Func>? Microsoft.Extensions.AI.FunctionInvokingChatClient.FunctionInvoker { get; set; }", + "Stage": "Stable" + }, { "Member": "bool Microsoft.Extensions.AI.FunctionInvokingChatClient.IncludeDetailedErrors { get; set; }", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 26554946dca..1379cef8bf0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -37,6 +38,35 @@ public void Ctor_HasExpectedDefaults() Assert.False(client.IncludeDetailedErrors); Assert.Equal(10, client.MaximumIterationsPerRequest); Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); + Assert.Null(client.FunctionInvoker); + } + + [Fact] + public void Properties_Roundtrip() + { + using TestChatClient innerClient = new(); + using FunctionInvokingChatClient client = new(innerClient); + + Assert.False(client.AllowConcurrentInvocation); + client.AllowConcurrentInvocation = true; + Assert.True(client.AllowConcurrentInvocation); + + Assert.False(client.IncludeDetailedErrors); + client.IncludeDetailedErrors = true; + Assert.True(client.IncludeDetailedErrors); + + Assert.Equal(10, client.MaximumIterationsPerRequest); + client.MaximumIterationsPerRequest = 5; + Assert.Equal(5, client.MaximumIterationsPerRequest); + + Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); + client.MaximumConsecutiveErrorsPerRequest = 1; + Assert.Equal(1, client.MaximumConsecutiveErrorsPerRequest); + + Assert.Null(client.FunctionInvoker); + Func> invoker = (ctx, ct) => new ValueTask("test"); + client.FunctionInvoker = invoker; + Assert.Same(invoker, client.FunctionInvoker); } [Fact] @@ -208,6 +238,49 @@ public async Task ConcurrentInvocationOfParallelCallsDisabledByDefaultAsync() await InvokeAndAssertStreamingAsync(options, plan); } + [Fact] + public async Task FunctionInvokerDelegateOverridesHandlingAsync() + { + var options = new ChatOptions + { + Tools = + [ + AIFunctionFactory.Create(() => "Result 1", "Func1"), + AIFunctionFactory.Create((int i) => $"Result 2: {i}", "Func2"), + AIFunctionFactory.Create((int i) => { }, "VoidReturn"), + ] + }; + + List plan = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1 from delegate")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42 from delegate")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId3", "VoidReturn", arguments: new Dictionary { { "i", 43 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Success: Function completed.")]), + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + Func configure = b => b.Use( + s => new FunctionInvokingChatClient(s) + { + FunctionInvoker = async (ctx, cancellationToken) => + { + Assert.NotNull(ctx); + var result = await ctx.Function.InvokeAsync(ctx.Arguments, cancellationToken); + return result is JsonElement e ? + JsonSerializer.SerializeToElement($"{e.GetString()} from delegate", AIJsonUtilities.DefaultOptions) : + result; + } + }); + + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); + + await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure); + } + [Fact] public async Task ContinuesWithSuccessfulCallsUntilMaximumIterations() { From 1a35757910d32601a458c1749de71164138e87ac Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 2 Jul 2025 13:28:50 -0400 Subject: [PATCH 184/472] Use dnx instead of dotnet tool exec in template README (#6571) --- .../src/McpServer/McpServer-CSharp/README.md | 4 +--- .../Snapshots/mcpserver.Basic.verified/mcpserver/README.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md index dc6f5038b61..50091888ad8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -24,10 +24,8 @@ Once the MCP server package is published to NuGet.org, you can use the following "servers": { "McpServer-CSharp": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "tool", - "exec", "", "--version", "", diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md index 25704e5d135..5c00a3bf669 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -24,10 +24,8 @@ Once the MCP server package is published to NuGet.org, you can use the following "servers": { "mcpserver": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "tool", - "exec", "", "--version", "", From 93b8616aa132856df0e31cc3b996de7c10e25c37 Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Wed, 2 Jul 2025 17:15:40 -0400 Subject: [PATCH 185/472] Add reporting tests that show NLP results. (#6574) * Add reporting tests that show NLP results. * Cleanup analyzer errors. * Add global tags for NLP * Add more precision to the evaluator timing * More tags * Add another partial match test --- .../BLEUEvaluator.cs | 2 +- .../F1Evaluator.cs | 2 +- .../GLEUEvaluator.cs | 2 +- .../EvaluationMetricExtensions.cs | 2 +- ...ons.AI.Evaluation.Integration.Tests.csproj | 1 + .../NLPEvaluatorTests.cs | 162 ++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs index e1419bd630e..f3030ec7cfb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -86,7 +86,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs index b0806be6d66..e070577c448 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs @@ -77,7 +77,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs index 0c9805ee108..60df30879a4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs @@ -86,7 +86,7 @@ public ValueTask EvaluateAsync( }); metric.Value = score; - string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); metric.AddOrUpdateContext(context); metric.Interpretation = metric.Interpret(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs index d3012030cec..534f5e300f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs @@ -177,7 +177,7 @@ public static void AddOrUpdateChatMetadata( if (duration is not null) { - string durationText = $"{duration.Value.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s"; + string durationText = $"{duration.Value.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s"; metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj index c08667ff421..6e3332ebca6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs new file mode 100644 index 00000000000..a4f3b75045a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/NLPEvaluatorTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take it. +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.NLP; +using Microsoft.Extensions.AI.Evaluation.Reporting; +using Microsoft.Extensions.AI.Evaluation.Reporting.Storage; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Integration.Tests; + +[Experimental("AIEVAL001")] +public class NLPEvaluatorTests +{ + private static readonly ReportingConfiguration? _nlpReportingConfiguration; + + static NLPEvaluatorTests() + { + if (Settings.Current.Configured) + { + string version = $"Product Version: {Constants.Version}"; + string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; + string projectName = $"Project: Integration Tests"; + string testClass = $"Test Class: {nameof(NLPEvaluatorTests)}"; + string usesContext = $"Feature: Context"; + + IEvaluator bleuEvaluator = new BLEUEvaluator(); + IEvaluator gleuEvaluator = new GLEUEvaluator(); + IEvaluator f1Evaluator = new F1Evaluator(); + + _nlpReportingConfiguration = + DiskBasedReportingConfiguration.Create( + storageRootPath: Settings.Current.StorageRootPath, + evaluators: [bleuEvaluator, gleuEvaluator, f1Evaluator], + executionName: Constants.Version, + tags: [version, date, projectName, testClass, usesContext]); + } + } + + [ConditionalFact] + public async Task ExactMatch() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(ExactMatch)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + EvaluationResult result = await scenarioRun.EvaluateAsync(referenceText, [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task PartialMatch() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(PartialMatch)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + var similarText = "The brown fox quickly jumps over a lazy dog."; + EvaluationResult result = await scenarioRun.EvaluateAsync(similarText, [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task Unmatched() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(Unmatched)}"); + + var referenceText = "The quick brown fox jumps over the lazy dog."; + var bleuContext = new BLEUEvaluatorContext(referenceText); + var gleuContext = new GLEUEvaluatorContext(referenceText); + var f1Context = new F1EvaluatorContext(referenceText); + + EvaluationResult result = await scenarioRun.EvaluateAsync("What is life's meaning?", [bleuContext, gleuContext, f1Context]); + + Assert.False( + result.ContainsDiagnostics(d => d.Severity >= EvaluationDiagnosticSeverity.Warning), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? _)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? _)); + } + + [ConditionalFact] + public async Task AdditionalContextIsNotPassed() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _nlpReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(NLPEvaluatorTests)}.{nameof(AdditionalContextIsNotPassed)}"); + + EvaluationResult result = await scenarioRun.EvaluateAsync("What is the meaning of life?"); + + Assert.True( + result.Metrics.Values.All(m => m.ContainsDiagnostics(d => d.Severity is EvaluationDiagnosticSeverity.Error)), + string.Join("\r\n\r\n", result.Metrics.Values.SelectMany(m => m.Diagnostics ?? []).Select(d => d.ToString()))); + + Assert.Equal(3, result.Metrics.Count); + Assert.True(result.TryGet(BLEUEvaluator.BLEUMetricName, out NumericMetric? bleu)); + Assert.True(result.TryGet(GLEUEvaluator.GLEUMetricName, out NumericMetric? gleu)); + Assert.True(result.TryGet(F1Evaluator.F1MetricName, out NumericMetric? f1)); + + Assert.Null(bleu.Context); + Assert.Null(gleu.Context); + Assert.Null(f1.Context); + + } + + [MemberNotNull(nameof(_nlpReportingConfiguration))] + private static void SkipIfNotConfigured() + { + if (!Settings.Current.Configured) + { + throw new SkipTestException("Test is not configured"); + } + + Assert.NotNull(_nlpReportingConfiguration); + } +} From aa6e9af7dd70e3ab2cb1667854a9291d682097ea Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 2 Jul 2025 23:53:52 -0400 Subject: [PATCH 186/472] Enable specifying "strict" for OpenAI clients via ChatOptions (#6552) * Enable specifying "strict" for OpenAI clients via ChatOptions * Address PR feedback --- .../OpenAIAssistantChatClient.cs | 13 +-- .../OpenAIChatClient.cs | 19 ++-- .../OpenAIClientExtensions.cs | 19 ++-- .../OpenAIRealtimeConversationClient.cs | 9 +- .../OpenAIResponseChatClient.cs | 19 ++-- .../OpenAIChatClientTests.cs | 86 +++++++++++++++---- .../OpenAIResponseClientTests.cs | 75 ++++++++++++++++ 7 files changed, 195 insertions(+), 45 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index 3547483da10..0b6c5f5122f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -237,14 +237,16 @@ void IDisposable.Dispose() } /// Converts an Extensions function to an OpenAI assistants function tool. - internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction) + internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null) { - (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + bool? strict = + OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); return new FunctionToolDefinition(aiFunction.Name) { Description = aiFunction.Description, - Parameters = parameters, + Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), StrictParameterSchemaEnabled = strict, }; } @@ -296,7 +298,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( switch (tool) { case AIFunction aiFunction: - runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); + runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; case HostedCodeInterpreterTool: @@ -342,7 +344,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName, BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription); + jsonFormat.SchemaDescription, + OpenAIClientExtensions.HasStrict(options.AdditionalProperties)); break; case ChatResponseFormatJson jsonFormat: diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index abbcb0ed0ae..c051550d493 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -101,11 +101,17 @@ void IDisposable.Dispose() } /// Converts an Extensions function to an OpenAI chat tool. - internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction) + internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? options = null) { - (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); - - return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict); + bool? strict = + OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); + + return ChatTool.CreateFunctionTool( + aiFunction.Name, + aiFunction.Description, + OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), + strict); } /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. @@ -517,7 +523,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) { if (tool is AIFunction af) { - result.Tools.Add(ToOpenAIChatTool(af)); + result.Tools.Add(ToOpenAIChatTool(af, options)); } } @@ -555,7 +561,8 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription) : + jsonFormat.SchemaDescription, + OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) : OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 24fd93ccb65..b20769c0dc4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -21,6 +21,7 @@ #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable SA1515 // Single-line comment should be preceded by blank line #pragma warning disable CA1305 // Specify IFormatProvider +#pragma warning disable S1135 // Track uses of "TODO" tags namespace Microsoft.Extensions.AI; @@ -182,15 +183,17 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); + // TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict. + + /// Gets whether the properties specify that strict schema handling is desired. + internal static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => + additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && + strictObj is bool strictValue ? + strictValue : null; + /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. - internal static (BinaryData Parameters, bool? Strict) ToOpenAIFunctionParameters(AIFunction aiFunction) + internal static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, bool? strict) { - // Extract any strict setting from AdditionalProperties. - bool? strict = - aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) && - strictObj is bool strictValue ? - strictValue : null; - // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting. JsonElement jsonSchema = strict is true ? StrictSchemaTransformCache.GetOrCreateTransformedSchema(aiFunction) : @@ -201,7 +204,7 @@ strictObj is bool strictValue ? var tool = JsonSerializer.Deserialize(jsonSchema, OpenAIJsonContext.Default.ToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson)); - return (functionParameters, strict); + return functionParameters; } /// Used to create the JSON payload for an OpenAI tool description. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs index 892a9e9aa2a..abfebd99f34 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -1,7 +1,6 @@ // 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 OpenAI.RealtimeConversation; namespace Microsoft.Extensions.AI; @@ -9,14 +8,16 @@ namespace Microsoft.Extensions.AI; /// Provides helpers for interacting with OpenAI Realtime. internal sealed class OpenAIRealtimeConversationClient { - public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction) + public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction, ChatOptions? options = null) { - (BinaryData parameters, _) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); + bool? strict = + OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); return new ConversationFunctionTool(aiFunction.Name) { Description = aiFunction.Description, - Parameters = parameters, + Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), }; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index a5f68e10365..46019166719 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -323,11 +323,17 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } - internal static ResponseTool ToResponseTool(AIFunction aiFunction) + internal static ResponseTool ToResponseTool(AIFunction aiFunction, ChatOptions? options = null) { - (BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction); - - return ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict ?? false); + bool? strict = + OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); + + return ResponseTool.CreateFunctionTool( + aiFunction.Name, + aiFunction.Description, + OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), + strict ?? false); } /// Creates a from a . @@ -380,7 +386,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt switch (tool) { case AIFunction aiFunction: - ResponseTool rtool = ToResponseTool(aiFunction); + ResponseTool rtool = ToResponseTool(aiFunction, options); result.Tools.Add(rtool); break; @@ -442,7 +448,8 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt ResponseTextFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription) : + jsonFormat.SchemaDescription, + OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) : ResponseTextFormat.CreateJsonObjectFormat(), }; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 30d03b6eee3..edb5d9fab07 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -276,6 +276,74 @@ public async Task BasicRequestResponse_Streaming() }, usage.Details.AdditionalCounts); } + [Fact] + public async Task ChatOptions_StrictRespected() + { + const string Input = """ + { + "tools": [ + { + "function": { + "description": "Gets the age of the specified person.", + "name": "GetPersonAge", + "strict": true, + "parameters": { + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": false + } + }, + "type": "function" + } + ], + "messages": [ + { + "role": "user", + "content": "hello" + } + ], + "model": "gpt-4o-mini", + "tool_choice": "auto" + } + """; + + const string Output = """ + { + "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", + "object": "chat.completion", + "created": 1727888631, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "refusal": null + }, + "logprobs": null, + "finish_reason": "stop" + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); + + var response = await client.GetResponseAsync("hello", new() + { + Tools = [AIFunctionFactory.Create(() => 42, "GetPersonAge", "Gets the age of the specified person.")], + AdditionalProperties = new() + { + ["strictJsonSchema"] = true, + }, + }); + Assert.NotNull(response); + } + [Fact] public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() { @@ -337,7 +405,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; openAIOptions.StopSequences.Add("hello"); - openAIOptions.Tools.Add(ToOpenAIChatTool(tool)); + openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool)); return openAIOptions; }, ModelId = null, @@ -416,7 +484,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; openAIOptions.StopSequences.Add("hello"); - openAIOptions.Tools.Add(ToOpenAIChatTool(tool)); + openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool)); return openAIOptions; }, ModelId = null, // has no effect, you cannot change the model of an OpenAI's ChatClient. @@ -600,20 +668,6 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream Assert.Equal("Hello! How can I assist you today?", responseText); } - /// Converts an Extensions function to an OpenAI chat tool. - private static ChatTool ToOpenAIChatTool(AIFunction aiFunction) - { - bool? strict = - aiFunction.AdditionalProperties.TryGetValue("strictJsonSchema", 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.JsonSchema)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); - return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict); - } - /// Used to create the JSON payload for an OpenAI chat tool description. internal sealed class ChatToolJson { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 8b27cd918a7..28125e462b7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -288,6 +288,81 @@ public async Task BasicRequestResponse_Streaming() Assert.Equal(36, usage.Details.TotalTokenCount); } + [Fact] + public async Task ChatOptions_StrictRespected() + { + const string Input = """ + { + "model": "gpt-4o-mini", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello" + } + ] + } + ], + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "name": "GetPersonAge", + "description": "Gets the age of the specified person.", + "parameters": { + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": false + }, + "strict": true + } + ] + } + """; + + const string Output = """ + { + "id": "resp_67d327649b288191aeb46a824e49dc40058a5e08c46a181d", + "object": "response", + "status": "completed", + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "type": "message", + "id": "msg_67d32764fcdc8191bcf2e444d4088804058a5e08c46a181d", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello! How can I assist you today?", + "annotations": [] + } + ] + } + ] + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + var response = await client.GetResponseAsync("hello", new() + { + Tools = [AIFunctionFactory.Create(() => 42, "GetPersonAge", "Gets the age of the specified person.")], + AdditionalProperties = new() + { + ["strictJsonSchema"] = true, + }, + }); + Assert.NotNull(response); + } + [Fact] public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() { From 61dc5a90a979eb5d36df0d46379db9a4f58333d9 Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Thu, 3 Jul 2025 01:59:20 -0400 Subject: [PATCH 187/472] Fix ConfigureEvaluationTests script when is not supplied (#6575) --- scripts/ConfigureEvaluationTests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ConfigureEvaluationTests.ps1 b/scripts/ConfigureEvaluationTests.ps1 index d58cb1352db..b00624ac29d 100644 --- a/scripts/ConfigureEvaluationTests.ps1 +++ b/scripts/ConfigureEvaluationTests.ps1 @@ -31,7 +31,7 @@ if ($Configure -and $Unconfigure) { Exit 1 } -if (!(Test-Path $ConfigRoot)) { +if (-not $ConfigRoot -or -not (Test-Path $ConfigRoot)) { $ConfigRoot = "$HOME/.config/dotnet-extensions" } From e4ba7e8d1ecdd99f8fa0818e0dc35aa837af757c Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 3 Jul 2025 14:09:56 +0300 Subject: [PATCH 188/472] AIFunctionFactory: tolerate JSON string function parameters. (#6572) * AIFunctionFactory: tolerate JSON string function parameters. * Add debug assertion. * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs Co-authored-by: Stephen Toub * Add regex-based JSON string recognition and add more tests. --------- Co-authored-by: Stephen Toub --- .../Functions/AIFunctionFactory.cs | 47 ++++++++++++++ .../ChatClientIntegrationTests.cs | 33 ++++++++++ .../Functions/AIFunctionFactoryTest.cs | 61 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 320df4098a3..e864923883e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -26,6 +26,7 @@ #pragma warning disable S2333 // Redundant modifiers should not be used #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable SA1202 // Public members should come before private members +#pragma warning disable SA1203 // Constants should appear before fields namespace Microsoft.Extensions.AI; @@ -825,6 +826,23 @@ static bool IsAsyncMethod(MethodInfo method) { try { + if (value is string text && IsPotentiallyJson(text)) + { + Debug.Assert(typeInfo.Type != typeof(string), "string parameters should not enter this branch."); + + // Account for the parameter potentially being a JSON string. + // The value is a string but the type is not. Try to deserialize it under the assumption that it's JSON. + // If it's not, we'll fall through to the default path that makes it valid JSON and then tries to deserialize. + try + { + return JsonSerializer.Deserialize(text, typeInfo); + } + catch (JsonException) + { + // If the string is not valid JSON, fall through to the round-trip. + } + } + string json = JsonSerializer.Serialize(value, serializerOptions.GetTypeInfo(value.GetType())); return JsonSerializer.Deserialize(json, typeInfo); } @@ -1021,6 +1039,35 @@ private record struct DescriptorKey( AIJsonSchemaCreateOptions SchemaOptions); } + /// + /// Quickly checks if the specified string is potentially JSON + /// by checking if the first non-whitespace characters are valid JSON start tokens. + /// + /// The string to check. + /// If then the string is definitely not valid JSON. + private static bool IsPotentiallyJson(string value) => PotentiallyJsonRegex().IsMatch(value); +#if NET + [GeneratedRegex(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace)] + private static partial Regex PotentiallyJsonRegex(); +#else + private static Regex PotentiallyJsonRegex() => _potentiallyJsonRegex; + private static readonly Regex _potentiallyJsonRegex = new(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); +#endif + private const string PotentiallyJsonRegexString = """ + ^\s* # Optional whitespace at the start of the string + ( null # null literal + | false # false literal + | true # true literal + | \d # positive number + | -\d # negative number + | " # string + | \[ # start array + | { # start object + | // # Start of single-line comment + | /\* # Start of multi-line comment + ) + """; + /// /// Removes characters from a .NET member name that shouldn't be used in an AI function name. /// diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index d84d767fd4c..ffa94f64531 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -341,6 +341,39 @@ public virtual async Task FunctionInvocation_NestedParameters() AssertUsageAgainstActivities(response, activities); } + [ConditionalFact] + public virtual async Task FunctionInvocation_ArrayParameter() + { + SkipIfNotEnabled(); + + var sourceName = Guid.NewGuid().ToString(); + var activities = new List(); + using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddInMemoryExporter(activities) + .Build(); + + using var chatClient = new FunctionInvokingChatClient( + new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + + List messages = + [ + new(ChatRole.User, "Can you add bacon, lettuce, and tomatoes to Peter's shopping cart?") + ]; + + string? shopperName = null; + List shoppingCart = []; + AIFunction func = AIFunctionFactory.Create((string[] items, string shopperId) => { shoppingCart.AddRange(items); shopperName = shopperId; }, "AddItemsToShoppingCart"); + var response = await chatClient.GetResponseAsync(messages, new() + { + Tools = [func] + }); + + Assert.Equal("Peter", shopperName); + Assert.Equal(["bacon", "lettuce", "tomatoes"], shoppingCart); + AssertUsageAgainstActivities(response, activities); + } + private static void AssertUsageAgainstActivities(ChatResponse response, List activities) { // If the underlying IChatClient provides usage data, function invocation should aggregate the diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 84298788e8c..b15d200a39a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Reflection; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -75,6 +76,66 @@ public async Task Parameters_MissingRequiredParametersFail_Async() } } + [Fact] + public async Task Parameters_ToleratesJsonEncodedParameters() + { + AIFunction func = AIFunctionFactory.Create((int x, int y, int z, int w, int u) => x + y + z + w + u); + + var result = await func.InvokeAsync(new() + { + ["x"] = "1", + ["y"] = JsonNode.Parse("2"), + ["z"] = JsonDocument.Parse("3"), + ["w"] = JsonDocument.Parse("4").RootElement, + ["u"] = 5M, // boxed decimal cannot be cast to int, requires conversion + }); + + AssertExtensions.EqualFunctionCallResults(15, result); + } + + [Theory] + [InlineData(" null")] + [InlineData(" false ")] + [InlineData("true ")] + [InlineData("42")] + [InlineData("0.0")] + [InlineData("-1e15")] + [InlineData(" \"I am a string!\" ")] + [InlineData(" {}")] + [InlineData("[]")] + public async Task Parameters_ToleratesJsonStringParameters(string jsonStringParam) + { + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); + JsonElement expectedResult = JsonDocument.Parse(jsonStringParam).RootElement; + + var result = await func.InvokeAsync(new() + { + ["param"] = jsonStringParam + }); + + AssertExtensions.EqualFunctionCallResults(expectedResult, result); + } + + [Theory] + [InlineData("")] + [InlineData(" \r\n")] + [InlineData("I am a string!")] + [InlineData("/* Code snippet */ int main(void) { return 0; }")] + [InlineData("let rec Y F x = F (Y F) x")] + [InlineData("+3")] + public async Task Parameters_ToleratesInvalidJsonStringParameters(string invalidJsonParam) + { + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); + JsonElement expectedResult = JsonDocument.Parse(JsonSerializer.Serialize(invalidJsonParam, JsonContext.Default.String)).RootElement; + + var result = await func.InvokeAsync(new() + { + ["param"] = invalidJsonParam + }); + + AssertExtensions.EqualFunctionCallResults(expectedResult, result); + } + [Fact] public async Task Parameters_MappedByType_Async() { From dd5fa173e3eb1c5a459ba50fb32b8dd20d5cb25d Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 3 Jul 2025 18:14:36 +0300 Subject: [PATCH 189/472] AIFunctionFactory: add test coverage for JSON comments. (#6576) --- .../Functions/AIFunctionFactory.cs | 3 +-- .../Functions/AIFunctionFactoryTest.cs | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index e864923883e..5ad178e7bc8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -1058,8 +1058,7 @@ private record struct DescriptorKey( ( null # null literal | false # false literal | true # true literal - | \d # positive number - | -\d # negative number + | -?[0-9]# number | " # string | \[ # start array | { # start object diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index b15d200a39a..afced22038f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -103,10 +103,13 @@ public async Task Parameters_ToleratesJsonEncodedParameters() [InlineData(" \"I am a string!\" ")] [InlineData(" {}")] [InlineData("[]")] + [InlineData("// single-line comment\r\nnull")] + [InlineData("/* multi-line\r\ncomment */\r\nnull")] public async Task Parameters_ToleratesJsonStringParameters(string jsonStringParam) { - AIFunction func = AIFunctionFactory.Create((JsonElement param) => param); - JsonElement expectedResult = JsonDocument.Parse(jsonStringParam).RootElement; + JsonSerializerOptions options = new(AIJsonUtilities.DefaultOptions) { ReadCommentHandling = JsonCommentHandling.Skip }; + AIFunction func = AIFunctionFactory.Create((JsonElement param) => param, serializerOptions: options); + JsonElement expectedResult = JsonDocument.Parse(jsonStringParam, new() { CommentHandling = JsonCommentHandling.Skip }).RootElement; var result = await func.InvokeAsync(new() { From 34ed44a1843070a4c895a55b5b2344774c167d2f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 4 Jul 2025 19:56:19 -0400 Subject: [PATCH 190/472] Update M.E.AI.OpenAI for latest OpenAI release (#6577) --- eng/packages/General.props | 2 +- .../SpeechToText/SpeechToTextOptions.cs | 14 +- .../Microsoft.Extensions.AI.OpenAI.csproj | 6 +- .../OpenAIAssistantChatClient.cs | 2 - .../OpenAIChatClient.cs | 47 +++-- .../OpenAIClientExtensions.cs | 4 +- .../OpenAIRealtimeConversationClient.cs | 2 +- .../OpenAIResponseChatClient.cs | 48 +++-- .../OpenAISpeechToTextClient.cs | 199 ++++++++---------- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 3 +- .../OpenAIAIFunctionConversionTests.cs | 14 +- ...enAIAssistantChatClientIntegrationTests.cs | 3 +- .../OpenAIAssistantChatClientTests.cs | 12 +- .../OpenAIChatClientTests.cs | 19 +- .../OpenAIEmbeddingGeneratorTests.cs | 11 +- .../OpenAIResponseClientTests.cs | 11 +- ...penAISpeechToTextClientIntegrationTests.cs | 2 +- .../OpenAISpeechToTextClientTests.cs | 14 +- 18 files changed, 186 insertions(+), 227 deletions(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index fa2d51de886..aa9771bfb4a 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -16,7 +16,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 5ff0135cec7..8efbf510164 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -11,20 +11,20 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextOptions { + /// Gets or sets any additional properties associated with the options. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// Gets or sets the model ID for the speech to text. public string? ModelId { get; set; } /// Gets or sets the language of source speech. public string? SpeechLanguage { get; set; } - /// Gets or sets the language for the target generated text. - public string? TextLanguage { get; set; } - /// Gets or sets the sample rate of the speech input audio. public int? SpeechSampleRate { get; set; } - /// Gets or sets any additional properties associated with the options. - public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// Gets or sets the language for the target generated text. + public string? TextLanguage { get; set; } /// /// Gets or sets a callback responsible for creating the raw representation of the embedding generation options from an underlying implementation. @@ -51,11 +51,11 @@ public virtual SpeechToTextOptions Clone() { SpeechToTextOptions options = new() { + AdditionalProperties = AdditionalProperties?.Clone(), ModelId = ModelId, SpeechLanguage = SpeechLanguage, - TextLanguage = TextLanguage, SpeechSampleRate = SpeechSampleRate, - AdditionalProperties = AdditionalProperties?.Clone(), + TextLanguage = TextLanguage, }; return options; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index 552d45f0fc6..a135ee011ea 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.AI @@ -15,8 +15,8 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002;OPENAI002 - $(NoWarn);MEAI001 + $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002 + $(NoWarn);OPENAI001;OPENAI002;MEAI001 true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index 0b6c5f5122f..c1d59e30a68 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -28,7 +27,6 @@ namespace Microsoft.Extensions.AI; /// Represents an for an Azure.AI.Agents.Persistent . -[Experimental("OPENAI001")] internal sealed class OpenAIAssistantChatClient : IChatClient { /// The underlying . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index c051550d493..3be0a1cc1ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -91,7 +91,7 @@ public IAsyncEnumerable GetStreamingResponseAsync( // Make the call to OpenAI. var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken); - return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken); + return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken); } /// @@ -290,7 +290,8 @@ private static List ToOpenAIChatContent(IList private static async IAsyncEnumerable FromOpenAIStreamingChatCompletionAsync( IAsyncEnumerable updates, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + ChatCompletionOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken) { Dictionary? functionCallInfos = null; ChatRole? streamedRole = null; @@ -334,6 +335,14 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha } } + if (update.OutputAudioUpdate is { } audioUpdate) + { + responseUpdate.Contents.Add(new DataContent(audioUpdate.AudioBytesUpdate.ToMemory(), GetOutputAudioMimeType(options)) + { + RawRepresentation = audioUpdate, + }); + } + // Transfer over refusal updates. if (update.RefusalUpdate is not null) { @@ -363,8 +372,10 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha // Transfer over usage updates. if (update.Usage is ChatTokenUsage tokenUsage) { - var usageDetails = FromOpenAIUsage(tokenUsage); - responseUpdate.Contents.Add(new UsageContent(usageDetails)); + responseUpdate.Contents.Add(new UsageContent(FromOpenAIUsage(tokenUsage)) + { + RawRepresentation = tokenUsage, + }); } // Now yield the item. @@ -408,6 +419,17 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha } } + private static string GetOutputAudioMimeType(ChatCompletionOptions? options) => + options?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch + { + "opus" => "audio/opus", + "aac" => "audio/aac", + "flac" => "audio/flac", + "wav" => "audio/wav", + "pcm" => "audio/pcm", + "mp3" or _ => "audio/mpeg", + }; + private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions) { _ = Throw.IfNull(openAICompletion); @@ -432,19 +454,10 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple // Output audio is handled separately from message content parts. if (openAICompletion.OutputAudio is ChatOutputAudio audio) { - string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch + returnMessage.Contents.Add(new DataContent(audio.AudioBytes.ToMemory(), GetOutputAudioMimeType(chatCompletionOptions)) { - "opus" => "audio/opus", - "aac" => "audio/aac", - "flac" => "audio/flac", - "wav" => "audio/wav", - "pcm" => "audio/pcm", - "mp3" or _ => "audio/mpeg", - }; - - var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType); - - returnMessage.Contents.Add(dc); + RawRepresentation = audio, + }); } // Also manufacture function calling content items from any tool calls in the response. @@ -505,9 +518,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) result.PresencePenalty ??= options.PresencePenalty; result.Temperature ??= options.Temperature; result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. result.Seed ??= options.Seed; -#pragma warning restore OPENAI001 if (options.StopSequences is { Count: > 0 } stopSequences) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index b20769c0dc4..dccddf3038e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -14,7 +14,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long @@ -134,7 +134,6 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient /// is . /// is . /// is empty or composed entirely of whitespace. - [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); @@ -165,7 +164,6 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) => /// The function to convert. /// An OpenAI representing . /// is . - [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs index abfebd99f34..7c944ac5edb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 46019166719..6aee4bc77e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -117,6 +117,13 @@ public async Task GetResponseAsync( ((List)message.Contents).AddRange(ToAIContents(messageItem.Content)); break; + case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary && !string.IsNullOrWhiteSpace(summary): + message.Contents.Add(new TextReasoningContent(summary) + { + RawRepresentation = reasoningItem + }); + break; + case FunctionCallResponseItem functionCall: response.FinishReason ??= ChatFinishReason.ToolCalls; var fcc = FunctionCallContent.CreateFromParsedArguments( @@ -139,7 +146,7 @@ public async Task GetResponseAsync( if (openAIResponse.Error is { } error) { - message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code }); + message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() }); } } @@ -367,10 +374,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt // Handle strongly-typed properties. result.MaxOutputTokenCount ??= options.MaxOutputTokens; + result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; result.PreviousResponseId ??= options.ConversationId; - result.TopP ??= options.TopP; result.Temperature ??= options.Temperature; - result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; + result.TopP ??= options.TopP; + if (options.Instructions is { } instructions) { result.Instructions = string.IsNullOrEmpty(result.Instructions) ? @@ -386,22 +394,21 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt switch (tool) { case AIFunction aiFunction: - ResponseTool rtool = ToResponseTool(aiFunction, options); - result.Tools.Add(rtool); + result.Tools.Add(ToResponseTool(aiFunction, options)); break; case HostedWebSearchTool: - WebSearchToolLocation? location = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) + WebSearchUserLocation? location = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchUserLocation), out object? objLocation)) { - location = objLocation as WebSearchToolLocation; + location = objLocation as WebSearchUserLocation; } - WebSearchToolContextSize? size = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && - objSize is WebSearchToolContextSize) + WebSearchContextSize? size = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchContextSize), out object? objSize) && + objSize is WebSearchContextSize) { - size = (WebSearchToolContextSize)objSize; + size = (WebSearchContextSize)objSize; } result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); @@ -522,6 +529,10 @@ private static IEnumerable ToOpenAIResponseItems( yield return ResponseItem.CreateAssistantMessageItem(textContent.Text); break; + case TextReasoningContent reasoningContent: + yield return ResponseItem.CreateReasoningItem(reasoningContent.Text); + break; + case FunctionCallContent callContent: yield return ResponseItem.CreateFunctionCallItem( callContent.CallId, @@ -555,12 +566,16 @@ private static IEnumerable ToOpenAIResponseItems( TotalTokenCount = usage.TotalTokenCount, }; - if (usage.OutputTokenDetails is { } outputDetails) + if (usage.InputTokenDetails is { } inputDetails) { ud.AdditionalCounts ??= []; + ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount); + } - const string OutputDetails = nameof(usage.OutputTokenDetails); - ud.AdditionalCounts.Add($"{OutputDetails}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); + if (usage.OutputTokenDetails is { } outputDetails) + { + ud.AdditionalCounts ??= []; + ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); } } @@ -624,8 +639,7 @@ private static List ToOpenAIResponsesContent(IListDefault OpenAI endpoint. - private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); + /// Filename to use when audio lacks a name. + /// This information internally is required but is only being used to create a header name in the multipart request. + private const string Filename = "audio.mp3"; /// Metadata about the client. private readonly SpeechToTextClientMetadata _metadata; @@ -45,7 +46,7 @@ public OpenAISpeechToTextClient(AudioClient audioClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(AudioClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(audioClient) as Uri ?? _defaultOpenAIEndpoint; + ?.GetValue(audioClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; string? model = typeof(AudioClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(audioClient) as string; @@ -65,20 +66,6 @@ public OpenAISpeechToTextClient(AudioClient audioClient) null; } - /// - public async IAsyncEnumerable GetStreamingTextAsync( - Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(audioSpeechStream); - - var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); - - foreach (var update in speechResponse.ToSpeechToTextResponseUpdates()) - { - yield return update; - } - } - /// public async Task GetTextAsync( Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) @@ -87,140 +74,126 @@ public async Task GetTextAsync( SpeechToTextResponse response = new(); - // A translation is triggered when the target text language is specified and the source language is not provided or different. - static bool IsTranslationRequest(SpeechToTextOptions? options) - => options is not null && options.TextLanguage is not null - && (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); + string filename = audioSpeechStream is FileStream fileStream ? + Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream. + Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request. if (IsTranslationRequest(options)) { - _ = Throw.IfNull(options); + var translation = (await _audioClient.TranslateAudioAsync(audioSpeechStream, filename, ToOpenAITranslationOptions(options), cancellationToken).ConfigureAwait(false)).Value; - var openAIOptions = ToOpenAITranslationOptions(options); - AudioTranslation translationResult; + response.Contents = [new TextContent(translation.Text)]; + response.RawRepresentation = translation; -#if NET - await using (audioSpeechStream.ConfigureAwait(false)) -#else - using (audioSpeechStream) -#endif + int segmentCount = translation.Segments.Count; + if (segmentCount > 0) { - translationResult = (await _audioClient.TranslateAudioAsync( - audioSpeechStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + response.StartTime = translation.Segments[0].StartTime; + response.EndTime = translation.Segments[segmentCount - 1].EndTime; } - - UpdateResponseFromOpenAIAudioTranslation(response, translationResult); } else { - var openAIOptions = ToOpenAITranscriptionOptions(options); + var transcription = (await _audioClient.TranscribeAudioAsync(audioSpeechStream, filename, ToOpenAITranscriptionOptions(options), cancellationToken).ConfigureAwait(false)).Value; - // Transcription request - AudioTranscription transcriptionResult; + response.Contents = [new TextContent(transcription.Text)]; + response.RawRepresentation = transcription; -#if NET - await using (audioSpeechStream.ConfigureAwait(false)) -#else - using (audioSpeechStream) -#endif + int segmentCount = transcription.Segments.Count; + if (segmentCount > 0) { - transcriptionResult = (await _audioClient.TranscribeAudioAsync( - audioSpeechStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + response.StartTime = transcription.Segments[0].StartTime; + response.EndTime = transcription.Segments[segmentCount - 1].EndTime; + } + else + { + int wordCount = transcription.Words.Count; + if (wordCount > 0) + { + response.StartTime = transcription.Words[0].StartTime; + response.EndTime = transcription.Words[wordCount - 1].EndTime; + } } - - UpdateResponseFromOpenAIAudioTranscription(response, transcriptionResult); } return response; } /// - void IDisposable.Dispose() - { - // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. - } - - /// Updates a from an OpenAI . - /// The response to update. - /// The OpenAI audio transcription. - private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextResponse response, AudioTranscription audioTranscription) + public async IAsyncEnumerable GetStreamingTextAsync( + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioTranscription); + _ = Throw.IfNull(audioSpeechStream); - var segmentCount = audioTranscription.Segments.Count; - var wordCount = audioTranscription.Words.Count; + string filename = audioSpeechStream is FileStream fileStream ? + Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream. + Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request. - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) + if (IsTranslationRequest(options)) { - endTime = audioTranscription.Segments[segmentCount - 1].EndTime; - startTime = audioTranscription.Segments[0].StartTime; + foreach (var update in (await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)).ToSpeechToTextResponseUpdates()) + { + yield return update; + } } - else if (wordCount > 0) + else { - endTime = audioTranscription.Words[wordCount - 1].EndTime; - startTime = audioTranscription.Words[0].StartTime; + await foreach (var update in _audioClient.TranscribeAudioStreamingAsync( + audioSpeechStream, + filename, + ToOpenAITranscriptionOptions(options), + cancellationToken).ConfigureAwait(false)) + { + SpeechToTextResponseUpdate result = new() + { + ModelId = options?.ModelId, + RawRepresentation = update, + }; + + switch (update) + { + case StreamingAudioTranscriptionTextDeltaUpdate deltaUpdate: + result.Kind = SpeechToTextResponseUpdateKind.TextUpdated; + result.Contents = [new TextContent(deltaUpdate.Delta)]; + break; + + case StreamingAudioTranscriptionTextDoneUpdate doneUpdate: + result.Kind = SpeechToTextResponseUpdateKind.SessionClose; + break; + } + + yield return result; + } } + } - // Update the response - response.RawRepresentation = audioTranscription; - response.Contents = [new TextContent(audioTranscription.Text)]; - response.StartTime = startTime; - response.EndTime = endTime; - response.AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranscription.Language)] = audioTranscription.Language, - [nameof(audioTranscription.Duration)] = audioTranscription.Duration - }; + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. } - /// Converts an extensions options instance to an OpenAI options instance. + // A translation is triggered when the target text language is specified and the source language is not provided or different. + private static bool IsTranslationRequest(SpeechToTextOptions? options) => + options is not null && + options.TextLanguage is not null && + (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); + + /// Converts an extensions options instance to an OpenAI transcription options instance. private AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) { - if (options?.RawRepresentationFactory?.Invoke(this) is not AudioTranscriptionOptions result) - { - result = new AudioTranscriptionOptions(); - } + AudioTranscriptionOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranscriptionOptions ?? new(); result.Language ??= options?.SpeechLanguage; + return result; } - /// Updates a from an OpenAI . - /// The response to update. - /// The OpenAI audio translation. - private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextResponse response, AudioTranslation audioTranslation) + /// Converts an extensions options instance to an OpenAI translation options instance. + private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) { - _ = Throw.IfNull(audioTranslation); - - var segmentCount = audioTranslation.Segments.Count; - - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) - { - endTime = audioTranslation.Segments[segmentCount - 1].EndTime; - startTime = audioTranslation.Segments[0].StartTime; - } + AudioTranslationOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new(); - // Update the response - response.RawRepresentation = audioTranslation; - response.Contents = [new TextContent(audioTranslation.Text)]; - response.StartTime = startTime; - response.EndTime = endTime; - response.AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranslation.Language)] = audioTranslation.Language, - [nameof(audioTranslation.Duration)] = audioTranslation.Duration - }; + return result; } - - /// Converts an extensions options instance to an OpenAI options instance. - private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) - => options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new AudioTranslationOptions(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 60b6f8c84ab..536c250cb47 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -2,7 +2,8 @@ Microsoft.Extensions.AI Unit tests for Microsoft.Extensions.AI.OpenAI - $(NoWarn);OPENAI002;MEAI001;S104 + $(NoWarn);S104 + $(NoWarn);OPENAI001;MEAI001 diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs index f2f0c9d8a3f..ce458473c59 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs @@ -6,12 +6,10 @@ using System.Text.Json; using OpenAI.Assistants; using OpenAI.Chat; -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; using OpenAI.Responses; using Xunit; -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - namespace Microsoft.Extensions.AI; public class OpenAIAIFunctionConversionTests @@ -24,7 +22,7 @@ public class OpenAIAIFunctionConversionTests [Fact] public void AsOpenAIChatTool_ProducesValidInstance() { - ChatTool tool = _testFunction.AsOpenAIChatTool(); + var tool = _testFunction.AsOpenAIChatTool(); Assert.NotNull(tool); Assert.Equal("test_function", tool.FunctionName); @@ -35,7 +33,7 @@ public void AsOpenAIChatTool_ProducesValidInstance() [Fact] public void AsOpenAIResponseTool_ProducesValidInstance() { - ResponseTool tool = _testFunction.AsOpenAIResponseTool(); + var tool = _testFunction.AsOpenAIResponseTool(); Assert.NotNull(tool); } @@ -43,7 +41,7 @@ public void AsOpenAIResponseTool_ProducesValidInstance() [Fact] public void AsOpenAIConversationFunctionTool_ProducesValidInstance() { - ConversationFunctionTool tool = _testFunction.AsOpenAIConversationFunctionTool(); + var tool = _testFunction.AsOpenAIConversationFunctionTool(); Assert.NotNull(tool); Assert.Equal("test_function", tool.Name); @@ -54,7 +52,7 @@ public void AsOpenAIConversationFunctionTool_ProducesValidInstance() [Fact] public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() { - FunctionToolDefinition tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); + var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); Assert.NotNull(tool); Assert.Equal("test_function", tool.FunctionName); @@ -62,7 +60,7 @@ public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() ValidateSchemaParameters(tool.Parameters); } - /// Helper method to validate function parameters match our schema + /// Helper method to validate function parameters match our schema. private static void ValidateSchemaParameters(BinaryData parameters) { Assert.NotNull(parameters); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs index e616d5fb87b..90bcf9f2632 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable CA1822 // Mark members as static #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable S1135 // Track uses of "TODO" tags @@ -62,7 +61,7 @@ public async Task DeleteAllThreads() client.DefaultRequestHeaders.Add("openai-organization", "org-ENTERYOURORGID"); client.DefaultRequestHeaders.Add("openai-project", "proj_ENTERYOURPROJECTID"); - AssistantClient ac = new AssistantClient(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); + AssistantClient ac = new(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); while (true) { string listing = await client.GetStringAsync("https://api.openai.com/v1/threads?limit=100"); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs index 6d3a02a08ec..3b084b5ec8a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs @@ -3,7 +3,6 @@ using System; using System.ClientModel; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -11,7 +10,6 @@ using Xunit; #pragma warning disable S103 // Lines should not be too long -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. namespace Microsoft.Extensions.AI; @@ -24,16 +22,12 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!)); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient[] clients = [ diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index edb5d9fab07..d06d8f520be 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -11,7 +11,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -30,17 +29,13 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("chatClient", () => ((ChatClient)null!).AsIChatClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); var metadata = chatClient.GetService(); @@ -398,9 +393,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio TopP = 0.5f, PresencePenalty = 0.5f, Temperature = 0.5f, -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Seed = 42, -#pragma warning restore OPENAI001 ToolChoice = ChatToolChoice.CreateAutoChoice(), ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; @@ -477,9 +470,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio TopP = 0.5f, PresencePenalty = 0.5f, Temperature = 0.5f, -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Seed = 42, -#pragma warning restore OPENAI001 ToolChoice = ChatToolChoice.CreateAutoChoice(), ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; @@ -561,9 +552,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStr Assert.Null(openAIOptions.TopP); Assert.Null(openAIOptions.PresencePenalty); Assert.Null(openAIOptions.Temperature); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Assert.Null(openAIOptions.Seed); -#pragma warning restore OPENAI001 Assert.Empty(openAIOptions.StopSequences); Assert.Empty(openAIOptions.Tools); Assert.Null(openAIOptions.ToolChoice); @@ -637,9 +626,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream Assert.Null(openAIOptions.TopP); Assert.Null(openAIOptions.PresencePenalty); Assert.Null(openAIOptions.Temperature); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Assert.Null(openAIOptions.Seed); -#pragma warning restore OPENAI001 Assert.Empty(openAIOptions.StopSequences); Assert.Empty(openAIOptions.Tools); Assert.Null(openAIOptions.ToolChoice); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs index 9d8a1219ea7..43112fa88e3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs @@ -6,7 +6,6 @@ using System.ClientModel.Primitives; using System.Net.Http; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -25,17 +24,13 @@ public void AsIEmbeddingGenerator_InvalidArgs_Throws() Assert.Throws("embeddingClient", () => ((EmbeddingClient)null!).AsIEmbeddingGenerator()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IEmbeddingGenerator> embeddingGenerator = client.GetEmbeddingClient(model).AsIEmbeddingGenerator(); var metadata = embeddingGenerator.GetService(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 28125e462b7..b98eb89197f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -10,7 +10,6 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -29,17 +28,13 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("responseClient", () => ((OpenAIResponseClient)null!).AsIChatClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient chatClient = client.GetOpenAIResponseClient(model).AsIChatClient(); var metadata = chatClient.GetService(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs index c80b37c865e..bb721806ff6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -7,6 +7,6 @@ public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegr { protected override ISpeechToTextClient? CreateClient() => IntegrationTestHelpers.GetOpenAIClient()? - .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1") + .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "gpt-4o-mini-transcribe") .AsISpeechToTextClient(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index c92d9627968..1252a20741b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -8,7 +8,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using OpenAI; using OpenAI.Audio; @@ -26,17 +25,13 @@ public void AsISpeechToTextClient_InvalidArgs_Throws() Assert.Throws("audioClient", () => ((AudioClient)null!).AsISpeechToTextClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); ISpeechToTextClient speechToTextClient = client.GetAudioClient(model).AsISpeechToTextClient(); var metadata = speechToTextClient.GetService(); @@ -148,7 +143,8 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu string input = $$""" { "model": "whisper-1", - "language": "{{speechLanguage}}" + "language": "{{speechLanguage}}", + "stream":true } """; From f9c61b4a372e372b9a11914a7dbdbdca59a912fe Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 4 Jul 2025 19:56:30 -0400 Subject: [PATCH 191/472] Update McpServer template for 0.3.0-preview.2 (#6578) --- src/ProjectTemplates/GeneratedContent.targets | 2 +- .../src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs | 2 +- .../mcpserver/Tools/RandomNumberTools.cs | 2 +- .../mcpserver.Basic.verified/mcpserver/mcpserver.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index ce6ba2bc502..dfe93dbc5a8 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -39,7 +39,7 @@ 9.3.0 1.53.0 1.53.0-preview - 0.3.0-preview.1 + 0.3.0-preview.2 5.1.18 1.12.0 0.1.10 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs index 4542f8505a5..568574f47d9 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs @@ -7,7 +7,7 @@ /// internal class RandomNumberTools { - [McpServerTool(Name = "get_random_number")] + [McpServerTool] [Description("Generates a random number between the specified minimum and maximum values.")] public int GetRandomNumber( [Description("Minimum value (inclusive)")] int min = 0, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs index 72af767e320..611745f4129 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs @@ -7,7 +7,7 @@ /// internal class RandomNumberTools { - [McpServerTool(Name = "get_random_number")] + [McpServerTool] [Description("Generates a random number between the specified minimum and maximum values.")] public int GetRandomNumber( [Description("Minimum value (inclusive)")] int min = 0, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index a71ac148e6f..e959c64702f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -26,7 +26,7 @@ - + From c21e89d7911b62b05e1eae6272bcb31729f62919 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 4 Jul 2025 19:56:19 -0400 Subject: [PATCH 192/472] Update M.E.AI.OpenAI for latest OpenAI release (#6577) --- eng/packages/General.props | 2 +- .../SpeechToText/SpeechToTextOptions.cs | 14 +- .../Microsoft.Extensions.AI.OpenAI.csproj | 6 +- .../OpenAIAssistantChatClient.cs | 2 - .../OpenAIChatClient.cs | 47 +++-- .../OpenAIClientExtensions.cs | 4 +- .../OpenAIRealtimeConversationClient.cs | 2 +- .../OpenAIResponseChatClient.cs | 48 +++-- .../OpenAISpeechToTextClient.cs | 199 ++++++++---------- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 3 +- .../OpenAIAIFunctionConversionTests.cs | 14 +- ...enAIAssistantChatClientIntegrationTests.cs | 3 +- .../OpenAIAssistantChatClientTests.cs | 12 +- .../OpenAIChatClientTests.cs | 19 +- .../OpenAIEmbeddingGeneratorTests.cs | 11 +- .../OpenAIResponseClientTests.cs | 11 +- ...penAISpeechToTextClientIntegrationTests.cs | 2 +- .../OpenAISpeechToTextClientTests.cs | 14 +- 18 files changed, 186 insertions(+), 227 deletions(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index fa2d51de886..aa9771bfb4a 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -16,7 +16,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 5ff0135cec7..8efbf510164 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -11,20 +11,20 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextOptions { + /// Gets or sets any additional properties associated with the options. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// Gets or sets the model ID for the speech to text. public string? ModelId { get; set; } /// Gets or sets the language of source speech. public string? SpeechLanguage { get; set; } - /// Gets or sets the language for the target generated text. - public string? TextLanguage { get; set; } - /// Gets or sets the sample rate of the speech input audio. public int? SpeechSampleRate { get; set; } - /// Gets or sets any additional properties associated with the options. - public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// Gets or sets the language for the target generated text. + public string? TextLanguage { get; set; } /// /// Gets or sets a callback responsible for creating the raw representation of the embedding generation options from an underlying implementation. @@ -51,11 +51,11 @@ public virtual SpeechToTextOptions Clone() { SpeechToTextOptions options = new() { + AdditionalProperties = AdditionalProperties?.Clone(), ModelId = ModelId, SpeechLanguage = SpeechLanguage, - TextLanguage = TextLanguage, SpeechSampleRate = SpeechSampleRate, - AdditionalProperties = AdditionalProperties?.Clone(), + TextLanguage = TextLanguage, }; return options; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index 552d45f0fc6..a135ee011ea 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.AI @@ -15,8 +15,8 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002;OPENAI002 - $(NoWarn);MEAI001 + $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002 + $(NoWarn);OPENAI001;OPENAI002;MEAI001 true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs index 0b6c5f5122f..c1d59e30a68 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -28,7 +27,6 @@ namespace Microsoft.Extensions.AI; /// Represents an for an Azure.AI.Agents.Persistent . -[Experimental("OPENAI001")] internal sealed class OpenAIAssistantChatClient : IChatClient { /// The underlying . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index c051550d493..3be0a1cc1ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -91,7 +91,7 @@ public IAsyncEnumerable GetStreamingResponseAsync( // Make the call to OpenAI. var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken); - return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken); + return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken); } /// @@ -290,7 +290,8 @@ private static List ToOpenAIChatContent(IList private static async IAsyncEnumerable FromOpenAIStreamingChatCompletionAsync( IAsyncEnumerable updates, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + ChatCompletionOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken) { Dictionary? functionCallInfos = null; ChatRole? streamedRole = null; @@ -334,6 +335,14 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha } } + if (update.OutputAudioUpdate is { } audioUpdate) + { + responseUpdate.Contents.Add(new DataContent(audioUpdate.AudioBytesUpdate.ToMemory(), GetOutputAudioMimeType(options)) + { + RawRepresentation = audioUpdate, + }); + } + // Transfer over refusal updates. if (update.RefusalUpdate is not null) { @@ -363,8 +372,10 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha // Transfer over usage updates. if (update.Usage is ChatTokenUsage tokenUsage) { - var usageDetails = FromOpenAIUsage(tokenUsage); - responseUpdate.Contents.Add(new UsageContent(usageDetails)); + responseUpdate.Contents.Add(new UsageContent(FromOpenAIUsage(tokenUsage)) + { + RawRepresentation = tokenUsage, + }); } // Now yield the item. @@ -408,6 +419,17 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha } } + private static string GetOutputAudioMimeType(ChatCompletionOptions? options) => + options?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch + { + "opus" => "audio/opus", + "aac" => "audio/aac", + "flac" => "audio/flac", + "wav" => "audio/wav", + "pcm" => "audio/pcm", + "mp3" or _ => "audio/mpeg", + }; + private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions) { _ = Throw.IfNull(openAICompletion); @@ -432,19 +454,10 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple // Output audio is handled separately from message content parts. if (openAICompletion.OutputAudio is ChatOutputAudio audio) { - string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch + returnMessage.Contents.Add(new DataContent(audio.AudioBytes.ToMemory(), GetOutputAudioMimeType(chatCompletionOptions)) { - "opus" => "audio/opus", - "aac" => "audio/aac", - "flac" => "audio/flac", - "wav" => "audio/wav", - "pcm" => "audio/pcm", - "mp3" or _ => "audio/mpeg", - }; - - var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType); - - returnMessage.Contents.Add(dc); + RawRepresentation = audio, + }); } // Also manufacture function calling content items from any tool calls in the response. @@ -505,9 +518,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) result.PresencePenalty ??= options.PresencePenalty; result.Temperature ??= options.Temperature; result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. result.Seed ??= options.Seed; -#pragma warning restore OPENAI001 if (options.StopSequences is { Count: > 0 } stopSequences) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index b20769c0dc4..dccddf3038e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -14,7 +14,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long @@ -134,7 +134,6 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient /// is . /// is . /// is empty or composed entirely of whitespace. - [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); @@ -165,7 +164,6 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) => /// The function to convert. /// An OpenAI representing . /// is . - [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs index abfebd99f34..7c944ac5edb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 46019166719..6aee4bc77e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -117,6 +117,13 @@ public async Task GetResponseAsync( ((List)message.Contents).AddRange(ToAIContents(messageItem.Content)); break; + case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary && !string.IsNullOrWhiteSpace(summary): + message.Contents.Add(new TextReasoningContent(summary) + { + RawRepresentation = reasoningItem + }); + break; + case FunctionCallResponseItem functionCall: response.FinishReason ??= ChatFinishReason.ToolCalls; var fcc = FunctionCallContent.CreateFromParsedArguments( @@ -139,7 +146,7 @@ public async Task GetResponseAsync( if (openAIResponse.Error is { } error) { - message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code }); + message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() }); } } @@ -367,10 +374,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt // Handle strongly-typed properties. result.MaxOutputTokenCount ??= options.MaxOutputTokens; + result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; result.PreviousResponseId ??= options.ConversationId; - result.TopP ??= options.TopP; result.Temperature ??= options.Temperature; - result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; + result.TopP ??= options.TopP; + if (options.Instructions is { } instructions) { result.Instructions = string.IsNullOrEmpty(result.Instructions) ? @@ -386,22 +394,21 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt switch (tool) { case AIFunction aiFunction: - ResponseTool rtool = ToResponseTool(aiFunction, options); - result.Tools.Add(rtool); + result.Tools.Add(ToResponseTool(aiFunction, options)); break; case HostedWebSearchTool: - WebSearchToolLocation? location = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) + WebSearchUserLocation? location = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchUserLocation), out object? objLocation)) { - location = objLocation as WebSearchToolLocation; + location = objLocation as WebSearchUserLocation; } - WebSearchToolContextSize? size = null; - if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && - objSize is WebSearchToolContextSize) + WebSearchContextSize? size = null; + if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchContextSize), out object? objSize) && + objSize is WebSearchContextSize) { - size = (WebSearchToolContextSize)objSize; + size = (WebSearchContextSize)objSize; } result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); @@ -522,6 +529,10 @@ private static IEnumerable ToOpenAIResponseItems( yield return ResponseItem.CreateAssistantMessageItem(textContent.Text); break; + case TextReasoningContent reasoningContent: + yield return ResponseItem.CreateReasoningItem(reasoningContent.Text); + break; + case FunctionCallContent callContent: yield return ResponseItem.CreateFunctionCallItem( callContent.CallId, @@ -555,12 +566,16 @@ private static IEnumerable ToOpenAIResponseItems( TotalTokenCount = usage.TotalTokenCount, }; - if (usage.OutputTokenDetails is { } outputDetails) + if (usage.InputTokenDetails is { } inputDetails) { ud.AdditionalCounts ??= []; + ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount); + } - const string OutputDetails = nameof(usage.OutputTokenDetails); - ud.AdditionalCounts.Add($"{OutputDetails}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); + if (usage.OutputTokenDetails is { } outputDetails) + { + ud.AdditionalCounts ??= []; + ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount); } } @@ -624,8 +639,7 @@ private static List ToOpenAIResponsesContent(IListDefault OpenAI endpoint. - private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1"); + /// Filename to use when audio lacks a name. + /// This information internally is required but is only being used to create a header name in the multipart request. + private const string Filename = "audio.mp3"; /// Metadata about the client. private readonly SpeechToTextClientMetadata _metadata; @@ -45,7 +46,7 @@ public OpenAISpeechToTextClient(AudioClient audioClient) // implement the abstractions directly rather than providing adapters on top of the public APIs, // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(AudioClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(audioClient) as Uri ?? _defaultOpenAIEndpoint; + ?.GetValue(audioClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; string? model = typeof(AudioClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(audioClient) as string; @@ -65,20 +66,6 @@ public OpenAISpeechToTextClient(AudioClient audioClient) null; } - /// - public async IAsyncEnumerable GetStreamingTextAsync( - Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(audioSpeechStream); - - var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false); - - foreach (var update in speechResponse.ToSpeechToTextResponseUpdates()) - { - yield return update; - } - } - /// public async Task GetTextAsync( Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) @@ -87,140 +74,126 @@ public async Task GetTextAsync( SpeechToTextResponse response = new(); - // A translation is triggered when the target text language is specified and the source language is not provided or different. - static bool IsTranslationRequest(SpeechToTextOptions? options) - => options is not null && options.TextLanguage is not null - && (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); + string filename = audioSpeechStream is FileStream fileStream ? + Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream. + Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request. if (IsTranslationRequest(options)) { - _ = Throw.IfNull(options); + var translation = (await _audioClient.TranslateAudioAsync(audioSpeechStream, filename, ToOpenAITranslationOptions(options), cancellationToken).ConfigureAwait(false)).Value; - var openAIOptions = ToOpenAITranslationOptions(options); - AudioTranslation translationResult; + response.Contents = [new TextContent(translation.Text)]; + response.RawRepresentation = translation; -#if NET - await using (audioSpeechStream.ConfigureAwait(false)) -#else - using (audioSpeechStream) -#endif + int segmentCount = translation.Segments.Count; + if (segmentCount > 0) { - translationResult = (await _audioClient.TranslateAudioAsync( - audioSpeechStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + response.StartTime = translation.Segments[0].StartTime; + response.EndTime = translation.Segments[segmentCount - 1].EndTime; } - - UpdateResponseFromOpenAIAudioTranslation(response, translationResult); } else { - var openAIOptions = ToOpenAITranscriptionOptions(options); + var transcription = (await _audioClient.TranscribeAudioAsync(audioSpeechStream, filename, ToOpenAITranscriptionOptions(options), cancellationToken).ConfigureAwait(false)).Value; - // Transcription request - AudioTranscription transcriptionResult; + response.Contents = [new TextContent(transcription.Text)]; + response.RawRepresentation = transcription; -#if NET - await using (audioSpeechStream.ConfigureAwait(false)) -#else - using (audioSpeechStream) -#endif + int segmentCount = transcription.Segments.Count; + if (segmentCount > 0) { - transcriptionResult = (await _audioClient.TranscribeAudioAsync( - audioSpeechStream, - "file.wav", // this information internally is required but is only being used to create a header name in the multipart request. - openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + response.StartTime = transcription.Segments[0].StartTime; + response.EndTime = transcription.Segments[segmentCount - 1].EndTime; + } + else + { + int wordCount = transcription.Words.Count; + if (wordCount > 0) + { + response.StartTime = transcription.Words[0].StartTime; + response.EndTime = transcription.Words[wordCount - 1].EndTime; + } } - - UpdateResponseFromOpenAIAudioTranscription(response, transcriptionResult); } return response; } /// - void IDisposable.Dispose() - { - // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. - } - - /// Updates a from an OpenAI . - /// The response to update. - /// The OpenAI audio transcription. - private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextResponse response, AudioTranscription audioTranscription) + public async IAsyncEnumerable GetStreamingTextAsync( + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - _ = Throw.IfNull(audioTranscription); + _ = Throw.IfNull(audioSpeechStream); - var segmentCount = audioTranscription.Segments.Count; - var wordCount = audioTranscription.Words.Count; + string filename = audioSpeechStream is FileStream fileStream ? + Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream. + Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request. - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) + if (IsTranslationRequest(options)) { - endTime = audioTranscription.Segments[segmentCount - 1].EndTime; - startTime = audioTranscription.Segments[0].StartTime; + foreach (var update in (await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)).ToSpeechToTextResponseUpdates()) + { + yield return update; + } } - else if (wordCount > 0) + else { - endTime = audioTranscription.Words[wordCount - 1].EndTime; - startTime = audioTranscription.Words[0].StartTime; + await foreach (var update in _audioClient.TranscribeAudioStreamingAsync( + audioSpeechStream, + filename, + ToOpenAITranscriptionOptions(options), + cancellationToken).ConfigureAwait(false)) + { + SpeechToTextResponseUpdate result = new() + { + ModelId = options?.ModelId, + RawRepresentation = update, + }; + + switch (update) + { + case StreamingAudioTranscriptionTextDeltaUpdate deltaUpdate: + result.Kind = SpeechToTextResponseUpdateKind.TextUpdated; + result.Contents = [new TextContent(deltaUpdate.Delta)]; + break; + + case StreamingAudioTranscriptionTextDoneUpdate doneUpdate: + result.Kind = SpeechToTextResponseUpdateKind.SessionClose; + break; + } + + yield return result; + } } + } - // Update the response - response.RawRepresentation = audioTranscription; - response.Contents = [new TextContent(audioTranscription.Text)]; - response.StartTime = startTime; - response.EndTime = endTime; - response.AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranscription.Language)] = audioTranscription.Language, - [nameof(audioTranscription.Duration)] = audioTranscription.Duration - }; + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface. } - /// Converts an extensions options instance to an OpenAI options instance. + // A translation is triggered when the target text language is specified and the source language is not provided or different. + private static bool IsTranslationRequest(SpeechToTextOptions? options) => + options is not null && + options.TextLanguage is not null && + (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage); + + /// Converts an extensions options instance to an OpenAI transcription options instance. private AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options) { - if (options?.RawRepresentationFactory?.Invoke(this) is not AudioTranscriptionOptions result) - { - result = new AudioTranscriptionOptions(); - } + AudioTranscriptionOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranscriptionOptions ?? new(); result.Language ??= options?.SpeechLanguage; + return result; } - /// Updates a from an OpenAI . - /// The response to update. - /// The OpenAI audio translation. - private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextResponse response, AudioTranslation audioTranslation) + /// Converts an extensions options instance to an OpenAI translation options instance. + private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) { - _ = Throw.IfNull(audioTranslation); - - var segmentCount = audioTranslation.Segments.Count; - - TimeSpan? endTime = null; - TimeSpan? startTime = null; - if (segmentCount > 0) - { - endTime = audioTranslation.Segments[segmentCount - 1].EndTime; - startTime = audioTranslation.Segments[0].StartTime; - } + AudioTranslationOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new(); - // Update the response - response.RawRepresentation = audioTranslation; - response.Contents = [new TextContent(audioTranslation.Text)]; - response.StartTime = startTime; - response.EndTime = endTime; - response.AdditionalProperties = new AdditionalPropertiesDictionary - { - [nameof(audioTranslation.Language)] = audioTranslation.Language, - [nameof(audioTranslation.Duration)] = audioTranslation.Duration - }; + return result; } - - /// Converts an extensions options instance to an OpenAI options instance. - private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options) - => options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new AudioTranslationOptions(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 60b6f8c84ab..536c250cb47 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -2,7 +2,8 @@ Microsoft.Extensions.AI Unit tests for Microsoft.Extensions.AI.OpenAI - $(NoWarn);OPENAI002;MEAI001;S104 + $(NoWarn);S104 + $(NoWarn);OPENAI001;MEAI001 diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs index f2f0c9d8a3f..ce458473c59 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs @@ -6,12 +6,10 @@ using System.Text.Json; using OpenAI.Assistants; using OpenAI.Chat; -using OpenAI.RealtimeConversation; +using OpenAI.Realtime; using OpenAI.Responses; using Xunit; -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - namespace Microsoft.Extensions.AI; public class OpenAIAIFunctionConversionTests @@ -24,7 +22,7 @@ public class OpenAIAIFunctionConversionTests [Fact] public void AsOpenAIChatTool_ProducesValidInstance() { - ChatTool tool = _testFunction.AsOpenAIChatTool(); + var tool = _testFunction.AsOpenAIChatTool(); Assert.NotNull(tool); Assert.Equal("test_function", tool.FunctionName); @@ -35,7 +33,7 @@ public void AsOpenAIChatTool_ProducesValidInstance() [Fact] public void AsOpenAIResponseTool_ProducesValidInstance() { - ResponseTool tool = _testFunction.AsOpenAIResponseTool(); + var tool = _testFunction.AsOpenAIResponseTool(); Assert.NotNull(tool); } @@ -43,7 +41,7 @@ public void AsOpenAIResponseTool_ProducesValidInstance() [Fact] public void AsOpenAIConversationFunctionTool_ProducesValidInstance() { - ConversationFunctionTool tool = _testFunction.AsOpenAIConversationFunctionTool(); + var tool = _testFunction.AsOpenAIConversationFunctionTool(); Assert.NotNull(tool); Assert.Equal("test_function", tool.Name); @@ -54,7 +52,7 @@ public void AsOpenAIConversationFunctionTool_ProducesValidInstance() [Fact] public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() { - FunctionToolDefinition tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); + var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); Assert.NotNull(tool); Assert.Equal("test_function", tool.FunctionName); @@ -62,7 +60,7 @@ public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() ValidateSchemaParameters(tool.Parameters); } - /// Helper method to validate function parameters match our schema + /// Helper method to validate function parameters match our schema. private static void ValidateSchemaParameters(BinaryData parameters) { Assert.NotNull(parameters); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs index e616d5fb87b..90bcf9f2632 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable CA1822 // Mark members as static #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable S1135 // Track uses of "TODO" tags @@ -62,7 +61,7 @@ public async Task DeleteAllThreads() client.DefaultRequestHeaders.Add("openai-organization", "org-ENTERYOURORGID"); client.DefaultRequestHeaders.Add("openai-project", "proj_ENTERYOURPROJECTID"); - AssistantClient ac = new AssistantClient(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); + AssistantClient ac = new(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!); while (true) { string listing = await client.GetStringAsync("https://api.openai.com/v1/threads?limit=100"); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs index 6d3a02a08ec..3b084b5ec8a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs @@ -3,7 +3,6 @@ using System; using System.ClientModel; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -11,7 +10,6 @@ using Xunit; #pragma warning disable S103 // Lines should not be too long -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. namespace Microsoft.Extensions.AI; @@ -24,16 +22,12 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!)); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient[] clients = [ diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index edb5d9fab07..d06d8f520be 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -11,7 +11,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -30,17 +29,13 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("chatClient", () => ((ChatClient)null!).AsIChatClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); var metadata = chatClient.GetService(); @@ -398,9 +393,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio TopP = 0.5f, PresencePenalty = 0.5f, Temperature = 0.5f, -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Seed = 42, -#pragma warning restore OPENAI001 ToolChoice = ChatToolChoice.CreateAutoChoice(), ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; @@ -477,9 +470,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio TopP = 0.5f, PresencePenalty = 0.5f, Temperature = 0.5f, -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Seed = 42, -#pragma warning restore OPENAI001 ToolChoice = ChatToolChoice.CreateAutoChoice(), ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; @@ -561,9 +552,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStr Assert.Null(openAIOptions.TopP); Assert.Null(openAIOptions.PresencePenalty); Assert.Null(openAIOptions.Temperature); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Assert.Null(openAIOptions.Seed); -#pragma warning restore OPENAI001 Assert.Empty(openAIOptions.StopSequences); Assert.Empty(openAIOptions.Tools); Assert.Null(openAIOptions.ToolChoice); @@ -637,9 +626,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream Assert.Null(openAIOptions.TopP); Assert.Null(openAIOptions.PresencePenalty); Assert.Null(openAIOptions.Temperature); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Assert.Null(openAIOptions.Seed); -#pragma warning restore OPENAI001 Assert.Empty(openAIOptions.StopSequences); Assert.Empty(openAIOptions.Tools); Assert.Null(openAIOptions.ToolChoice); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs index 9d8a1219ea7..43112fa88e3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs @@ -6,7 +6,6 @@ using System.ClientModel.Primitives; using System.Net.Http; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -25,17 +24,13 @@ public void AsIEmbeddingGenerator_InvalidArgs_Throws() Assert.Throws("embeddingClient", () => ((EmbeddingClient)null!).AsIEmbeddingGenerator()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IEmbeddingGenerator> embeddingGenerator = client.GetEmbeddingClient(model).AsIEmbeddingGenerator(); var metadata = embeddingGenerator.GetService(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 28125e462b7..b98eb89197f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -10,7 +10,6 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; @@ -29,17 +28,13 @@ public void AsIChatClient_InvalidArgs_Throws() Assert.Throws("responseClient", () => ((OpenAIResponseClient)null!).AsIChatClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsIChatClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsIChatClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); IChatClient chatClient = client.GetOpenAIResponseClient(model).AsIChatClient(); var metadata = chatClient.GetService(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs index c80b37c865e..bb721806ff6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs @@ -7,6 +7,6 @@ public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegr { protected override ISpeechToTextClient? CreateClient() => IntegrationTestHelpers.GetOpenAIClient()? - .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1") + .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "gpt-4o-mini-transcribe") .AsISpeechToTextClient(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index c92d9627968..1252a20741b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -8,7 +8,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using OpenAI; using OpenAI.Audio; @@ -26,17 +25,13 @@ public void AsISpeechToTextClient_InvalidArgs_Throws() Assert.Throws("audioClient", () => ((AudioClient)null!).AsISpeechToTextClient()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata(bool useAzureOpenAI) + [Fact] + public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata() { Uri endpoint = new("http://localhost/some/endpoint"); string model = "amazingModel"; - var client = useAzureOpenAI ? - new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) : - new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); ISpeechToTextClient speechToTextClient = client.GetAudioClient(model).AsISpeechToTextClient(); var metadata = speechToTextClient.GetService(); @@ -148,7 +143,8 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu string input = $$""" { "model": "whisper-1", - "language": "{{speechLanguage}}" + "language": "{{speechLanguage}}", + "stream":true } """; From 0e90bf3d09eff30dfa29cdc54b9d89622d4e1cb2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 4 Jul 2025 19:56:30 -0400 Subject: [PATCH 193/472] Update McpServer template for 0.3.0-preview.2 (#6578) --- src/ProjectTemplates/GeneratedContent.targets | 2 +- .../src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs | 2 +- .../mcpserver/Tools/RandomNumberTools.cs | 2 +- .../mcpserver.Basic.verified/mcpserver/mcpserver.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index ce6ba2bc502..dfe93dbc5a8 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -39,7 +39,7 @@ 9.3.0 1.53.0 1.53.0-preview - 0.3.0-preview.1 + 0.3.0-preview.2 5.1.18 1.12.0 0.1.10 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs index 4542f8505a5..568574f47d9 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs @@ -7,7 +7,7 @@ /// internal class RandomNumberTools { - [McpServerTool(Name = "get_random_number")] + [McpServerTool] [Description("Generates a random number between the specified minimum and maximum values.")] public int GetRandomNumber( [Description("Minimum value (inclusive)")] int min = 0, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs index 72af767e320..611745f4129 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs @@ -7,7 +7,7 @@ /// internal class RandomNumberTools { - [McpServerTool(Name = "get_random_number")] + [McpServerTool] [Description("Generates a random number between the specified minimum and maximum values.")] public int GetRandomNumber( [Description("Minimum value (inclusive)")] int min = 0, diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index a71ac148e6f..e959c64702f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -26,7 +26,7 @@ - + From 403882c5f385cfc0b3d4b5132b8840141c0f4a6c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:25:40 +0300 Subject: [PATCH 194/472] Update OpenTelemetry semantic conventions version from 1.35 to 1.36 (#6579) * Initial plan * Update OpenTelemetry semantic conventions version from 1.35 to 1.36 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatCompletion/OpenTelemetryChatClient.cs | 2 +- .../Embeddings/OpenTelemetryEmbeddingGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index d66266c39bc..a759aeea248 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.35, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.36, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed partial class OpenTelemetryChatClient : DelegatingChatClient diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 2eeb32891ea..87af9e52717 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.35, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.36, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// /// The type of input used to produce embeddings. From 48f95ca63e0b02aba43eb99893d50c10d65ccc57 Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:49:07 +0200 Subject: [PATCH 195/472] Add container.cpu.time metric (#5806) --- .../Linux/LinuxUtilizationParserCgroupV1.cs | 2 +- .../Linux/LinuxUtilizationParserCgroupV2.cs | 4 +- .../Linux/LinuxUtilizationProvider.cs | 57 ++++++++++---- .../WindowsContainerSnapshotProvider.cs | 32 +++++--- .../ResourceUtilizationInstruments.cs | 8 ++ .../ResourceHealthCheckExtensionsTests.cs | 1 + .../Linux/AcceptanceTest.cs | 77 ++++++++++++------- .../Linux/LinuxUtilizationProviderTests.cs | 5 +- .../WindowsContainerSnapshotProviderTests.cs | 60 +++++++++++++++ 9 files changed, 192 insertions(+), 54 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs index af90e447df7..5a96d115bb5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs @@ -151,7 +151,7 @@ public long GetHostCpuUsageInNanoseconds() $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); } - stat = stat.Slice(next, stat.Length - next); + stat = stat.Slice(next); } return (long)(total / (double)_userHz * NanosecondsInSecond); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs index abb4230d6bc..0e367891a96 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs @@ -131,7 +131,7 @@ public string GetCgroupPath(string filename) } // Extract the part after the last colon and cache it for future use - ReadOnlySpan trimmedPath = fileContent.Slice(colonIndex + 1); + ReadOnlySpan trimmedPath = fileContent[(colonIndex + 1)..]; _cachedCgroupPath = "/sys/fs/cgroup" + trimmedPath.ToString().TrimEnd('/') + "/"; return $"{_cachedCgroupPath}{filename}"; @@ -195,7 +195,7 @@ public long GetHostCpuUsageInNanoseconds() $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); } - stat = stat.Slice(next, stat.Length - next); + stat = stat.Slice(next); } return (long)(total / (double)_userHz * NanosecondsInSecond); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 5fb2f0a189e..0c3d4124e17 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Metrics; -using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -17,6 +16,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider { private const double One = 1.0; private const long Hundred = 100L; + private const double NanosecondsInSecond = 1_000_000_000; private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); @@ -82,14 +82,19 @@ public LinuxUtilizationProvider(IOptions options, ILi (_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); _ = meter.CreateObservableGauge( - ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), - "1"); + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), + unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)), unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: GetCpuTime, + unit: "1"); } else { @@ -111,12 +116,12 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + observeValues: () => GetMeasurementWithRetry(MemoryUtilization), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValues: () => GetMeasurementWithRetry(() => MemoryUtilization()), + observeValues: () => GetMeasurementWithRetry(MemoryUtilization), unit: "1"); // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core @@ -259,23 +264,32 @@ public Snapshot GetSnapshot() memoryUsageInBytes: memoryUsed); } - private IEnumerable> GetMeasurementWithRetry(Func func) + private Measurement[] GetMeasurementWithRetry(Func func) + { + if (!TryGetValueWithRetry(func, out double value)) + { + return Array.Empty>(); + } + + return new[] { new Measurement(value) }; + } + + private bool TryGetValueWithRetry(Func func, out T value) + where T : struct { + value = default; if (Volatile.Read(ref _measurementsUnavailable) == 1 && _timeProvider.GetUtcNow() - _lastFailure < _retryInterval) { - return Enumerable.Empty>(); + return false; } try { - double result = func(); - if (Volatile.Read(ref _measurementsUnavailable) == 1) - { - _ = Interlocked.Exchange(ref _measurementsUnavailable, 0); - } + value = func(); + _ = Interlocked.CompareExchange(ref _measurementsUnavailable, 0, 1); - return new[] { new Measurement(result) }; + return true; } catch (Exception ex) when ( ex is System.IO.FileNotFoundException || @@ -285,7 +299,7 @@ ex is System.IO.DirectoryNotFoundException || _lastFailure = _timeProvider.GetUtcNow(); _ = Interlocked.Exchange(ref _measurementsUnavailable, 1); - return Enumerable.Empty>(); + return false; } } @@ -293,4 +307,17 @@ ex is System.IO.DirectoryNotFoundException || // due to the fact that the calculation itself is not an atomic operation: private double CpuUtilizationRequest(double cpuRequest) => Math.Min(One, CpuUtilizationV2() / cpuRequest); private double CpuUtilizationLimit(double cpuLimit) => Math.Min(One, CpuUtilizationV2() / cpuLimit); + + private IEnumerable> GetCpuTime() + { + if (TryGetValueWithRetry(_parser.GetHostCpuUsageInNanoseconds, out long systemCpuTime)) + { + yield return new Measurement(systemCpuTime / NanosecondsInSecond, [new KeyValuePair("cpu.mode", "system")]); + } + + if (TryGetValueWithRetry(CpuUtilizationV2, out double userCpuTime)) + { + yield return new Measurement(userCpuTime, [new KeyValuePair("cpu.mode", "user")]); + } + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 27156ea874e..ca6ceaff8bd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; @@ -17,6 +18,7 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider { private const double One = 1.0d; private const double Hundred = 100.0d; + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; private readonly Lazy _memoryStatus; @@ -85,16 +87,16 @@ internal WindowsContainerSnapshotProvider( _timeProvider = timeProvider; - using var jobHandle = _createJobHandleObject(); + using IJobHandle jobHandle = _createJobHandleObject(); - var memoryLimitLong = GetMemoryLimit(jobHandle); + ulong memoryLimitLong = GetMemoryLimit(jobHandle); _memoryLimit = memoryLimitLong; _cpuLimit = GetCpuLimit(jobHandle, systemInfo); // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). - var cpuRequest = _cpuLimit; - var memoryRequest = memoryLimitLong; + double cpuRequest = _cpuLimit; + ulong memoryRequest = memoryLimitLong; Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); @@ -110,10 +112,11 @@ internal WindowsContainerSnapshotProvider( // We don't dispose the meter because IMeterFactory handles that // An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912 // Related documentation: https://github.com/dotnet/docs/pull/37170 - var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); + Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope // Container based metrics: + _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); @@ -155,7 +158,7 @@ private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) cpuRatio = cpuLimit.CpuRate / CpuCycles; } - var systemInfoValue = systemInfo.GetSystemInfo(); + SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo(); // Multiply the cpu ratio by the number of processors to get you the portion // of processors used from the system. @@ -172,7 +175,7 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) if (memoryLimitInBytes <= 0) { - var memoryStatus = _memoryStatus.Value; + MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; // Technically, the unconstrained limit is memoryStatus.TotalPageFile. // Leaving this at physical as it is more understandable to consumers. @@ -184,7 +187,7 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) private double MemoryPercentage(Func getMemoryUsage) { - var now = _timeProvider.GetUtcNow(); + DateTimeOffset now = _timeProvider.GetUtcNow(); lock (_memoryLocker) { @@ -194,7 +197,7 @@ private double MemoryPercentage(Func getMemoryUsage) } } - var memoryUsage = getMemoryUsage(); + ulong memoryUsage = getMemoryUsage(); lock (_memoryLocker) { @@ -211,6 +214,17 @@ private double MemoryPercentage(Func getMemoryUsage) } } + private IEnumerable> GetCpuTime() + { + using IJobHandle jobHandle = _createJobHandleObject(); + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + + yield return new Measurement(basicAccountingInfo.TotalUserTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "user")]); + yield return new Measurement(basicAccountingInfo.TotalKernelTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "system")]); + } + private double CpuPercentage() { var now = _timeProvider.GetUtcNow(); diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index c0a230c84ea..835d3099782 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -18,6 +18,14 @@ internal static class ResourceUtilizationInstruments /// public const string MeterName = "Microsoft.Extensions.Diagnostics.ResourceMonitoring"; + /// + /// The name of an instrument to retrieve CPU time consumed by the specific container on all available CPU cores, measured in seconds. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerCpuTime = "container.cpu.time"; + /// /// The name of an instrument to retrieve CPU limit consumption of all processes running inside a container or control group in range [0, 1]. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs index 16ec64fa003..7d9b347a59d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs @@ -500,6 +500,7 @@ public async Task TestCpuAndMemoryChecks_WithMetrics( accountingInfoAfter1Ms.TotalUserTime = (long)(utilization * 100); jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) .Returns(() => initialAccountingInfo) // this is called from the WindowsContainerSnapshotProvider's constructor + .Returns(() => initialAccountingInfo) // this is called from the WindowsContainerSnapshotProvider's GetCpuTime method .Returns(() => accountingInfoAfter1Ms); // this is called from the WindowsContainerSnapshotProvider's CpuPercentage method using var meter = new Meter("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index b71ea1c47d2..0221efc27c9 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -209,6 +210,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var listener = new MeterListener(); var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; var cpuRequestFromGauge = 0.0d; @@ -219,8 +222,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -292,6 +295,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var listener = new MeterListener(); var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; var cpuRequestFromGauge = 0.0d; @@ -302,8 +307,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -362,10 +367,8 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou [ConditionalFact] [CombinatorialData] [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] - public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_v2_Using_NrPeriods() + public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgroupsv2_Using_LinuxCalculationV2() { - var cpuRefresh = TimeSpan.FromMinutes(13); - var memoryRefresh = TimeSpan.FromMinutes(14); var fileSystem = new HardcodedValueFileSystem(new Dictionary { { new FileInfo("/proc/self/cgroup"), "0::/fakeslice"}, @@ -382,25 +385,34 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); var cpuFromGauge = 0.0d; var cpuLimitFromGauge = 0.0d; + var cpuUserTime = 0.0d; + var cpuKernelTime = 0.0d; var cpuRequestFromGauge = 0.0d; var memoryFromGauge = 0.0d; var memoryLimitFromGauge = 0.0d; - using var e = new ManualResetEventSlim(); object? meterScope = null; listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, _, _) - => OnMeasurementReceived(m, f, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((m, f, tags, _) + => OnMeasurementReceived(m, + f, + tags, + ref cpuUserTime, + ref cpuKernelTime, + ref cpuFromGauge, + ref cpuLimitFromGauge, + ref cpuRequestFromGauge, + ref memoryFromGauge, + ref memoryLimitFromGauge)); listener.Start(); - using var host = FakeHost.CreateBuilder() + using IHost host = FakeHost.CreateBuilder() .ConfigureServices(x => x.AddLogging() .AddSingleton(clock) .AddSingleton(new FakeUserHz(100)) .AddSingleton(fileSystem) - .AddSingleton(new GenericPublisher(_ => e.Set())) .AddResourceMonitoring(x => x.ConfigureMonitor(options => { options.UseLinuxCalculationV2 = true; @@ -409,15 +421,11 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou .Build(); meterScope = host.Services.GetRequiredService(); - var tracker = host.Services.GetService(); - Assert.NotNull(tracker); _ = host.RunAsync(); listener.RecordObservableInstruments(); - var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); - fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu 11 10 10 10 10 10 10 10 10 10"); fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/fakeslice/cpu.stat"), "usage_usec 1120000\nnr_periods 56"); fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory.current"), "524298"); @@ -426,14 +434,10 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou clock.Advance(TimeSpan.FromSeconds(6)); listener.RecordObservableInstruments(); - e.Wait(); - - utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); - - var roundedCpuUsedPercentage = Math.Round(utilization.CpuUsedPercentage, 1); - Assert.Equal(42, Math.Round(cpuLimitFromGauge * 100)); Assert.Equal(83, Math.Round(cpuRequestFromGauge * 100)); + Assert.Equal(167, Math.Round(cpuUserTime * 100)); + Assert.Equal(81, Math.Round(cpuKernelTime * 100)); return Task.CompletedTask; } @@ -448,6 +452,7 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m #pragma warning disable S1067 // Expressions should not be too complex if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization || instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization || + instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime || instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization || instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization || instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization) @@ -457,10 +462,18 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m #pragma warning restore S1067 // Expressions should not be too complex } - private static void OnMeasurementReceived( - Instrument instrument, double value, - ref double cpuFromGauge, ref double cpuLimitFromGauge, ref double cpuRequestFromGauge, - ref double memoryFromGauge, ref double memoryLimitFromGauge) +#pragma warning disable S107 // Methods should not have too many parameters + private static void OnMeasurementReceived(Instrument instrument, + double value, + ReadOnlySpan> tags, + ref double cpuUserTime, + ref double cpuKernelTime, + ref double cpuFromGauge, + ref double cpuLimitFromGauge, + ref double cpuRequestFromGauge, + ref double memoryFromGauge, + ref double memoryLimitFromGauge) +#pragma warning restore S107 // Methods should not have too many parameters { if (instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization) { @@ -470,6 +483,18 @@ private static void OnMeasurementReceived( { memoryFromGauge = value; } + else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime) + { + var tagsArray = tags.ToArray(); + if (tagsArray.Contains(new KeyValuePair("cpu.mode", "user"))) + { + cpuUserTime = value; + } + else if (tagsArray.Contains(new KeyValuePair("cpu.mode", "system"))) + { + cpuKernelTime = value; + } + } else if (instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization) { cpuLimitFromGauge = value; diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index 57b92bcfa85..b34dfb1c258 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -259,7 +259,7 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(4, samples.Count); + Assert.Equal(6, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -267,6 +267,9 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization).value)); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime); + Assert.All(samples.Where(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime), item => double.IsNaN(item.value)); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs index 08d3ecf6020..fc9113eacc9 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -190,6 +190,66 @@ public void GetSnapshot_With_JobMemoryLimit_Set_To_Zero_ProducesCorrectSnapshot( Assert.True(data.MemoryUsageInBytes > 0); } + [Fact] + public void SnapshotProvider_EmitsCpuTimeMetric() + { + // Simulating 10% CPU usage (2 CPUs, 2000 ticks initially, 4000 ticks after 1 ms): + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION updatedAccountingInfo = default; + updatedAccountingInfo.TotalKernelTime = 2500; + updatedAccountingInfo.TotalUserTime = 1500; + + _jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) + .Returns(_accountingInfo) + .Returns(_accountingInfo) + .Returns(updatedAccountingInfo) + .Returns(updatedAccountingInfo) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + _sysInfo.NumberOfProcessors = 2; + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_EmitsCpuMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuTime, fakeClock); + + var options = new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) }; + + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + var snapshot = metricCollector.GetMeasurementSnapshot(); + Assert.Equal(2, snapshot.Count); + Assert.Contains(_accountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(_accountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + snapshot = metricCollector.GetMeasurementSnapshot(); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + + // Step #2 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + snapshot = metricCollector.GetMeasurementSnapshot(); + + // CPU time should be the same as before, as we're not simulating any CPU usage: + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + Assert.Contains(updatedAccountingInfo.TotalKernelTime / (double)TimeSpan.TicksPerSecond, snapshot.Select(m => m.Value)); + } + [Theory] [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization, true)] [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization, false)] From e79652fed4000a954eb8349cd03361f4db205658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borja=20Dom=C3=ADnguez?= Date: Mon, 7 Jul 2025 13:23:10 +0200 Subject: [PATCH 196/472] Add netstandard2.0 compatibility to Microsoft.Extensions.Diagnostics.Testing and dependencies (#6219) --- .../Microsoft.Extensions.Diagnostics.Testing.csproj | 1 + .../Microsoft.Extensions.Telemetry.Abstractions.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj index 2ab592c4a4a..c38dcdea395 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj @@ -1,6 +1,7 @@  Microsoft.Extensions.Diagnostics.Testing + $(NetCoreTargetFrameworks);netstandard2.0;net462 Hand-crafted fakes to make telemetry-related testing easier. Telemetry $(PackageTags);Testing diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj index 816ad679585..a461593894e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj @@ -1,6 +1,7 @@  Microsoft.Extensions.Telemetry + $(NetCoreTargetFrameworks);netstandard2.0;net462 Common abstractions for high-level telemetry primitives. Telemetry From 63ede029911898438a78e6bf141f61c38176240b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borja=20Dom=C3=ADnguez?= Date: Mon, 7 Jul 2025 14:10:05 +0200 Subject: [PATCH 197/472] Add netstandard2.0 compatibility to Microsoft.Extensions.Telemetry and dependencies (#6218) --- .../Microsoft.Extensions.AmbientMetadata.Application.csproj | 1 + ...oft.Extensions.DependencyInjection.AutoActivation.csproj | 1 + .../Microsoft.Extensions.Telemetry.csproj | 1 + .../Sampling/RandomProbabilisticSampler.cs | 6 +++--- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj index f631a4047bb..86f07dc205f 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj @@ -1,6 +1,7 @@ Microsoft.Extensions.AmbientMetadata + $(NetCoreTargetFrameworks);netstandard2.0;net462 Runtime information provider for application-level ambient metadata. Telemetry diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj index 7d02c3f1e90..5dd62090e1e 100644 --- a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj @@ -1,6 +1,7 @@ Microsoft.Extensions.DependencyInjection + $(NetCoreTargetFrameworks);netstandard2.0;net462 Extensions to auto-activate registered singletons in the dependency injection system. Fundamentals diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index 9dc4a11ca6c..8ff5676e349 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -1,6 +1,7 @@  Microsoft.Extensions.Diagnostics + $(NetCoreTargetFrameworks);netstandard2.0;net462 Provides canonical implementations of telemetry abstractions. Telemetry diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSampler.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSampler.cs index 6ba0a376c25..d809da8a2ad 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSampler.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Sampling/RandomProbabilisticSampler.cs @@ -3,7 +3,7 @@ using System; using System.Linq; -#if !NETFRAMEWORK +#if !NETFRAMEWORK && !NETSTANDARD using System.Security.Cryptography; #endif using Microsoft.Extensions.Logging; @@ -22,7 +22,7 @@ internal sealed class RandomProbabilisticSampler : LoggingSampler, IDisposable { internal RandomProbabilisticSamplerFilterRule[] LastKnownGoodSamplerRules; -#if NETFRAMEWORK +#if NETFRAMEWORK || NETSTANDARD private static readonly System.Threading.ThreadLocal _randomInstance = new(() => new Random()); #endif @@ -50,7 +50,7 @@ public override bool ShouldSample(in LogEntry logEntry) return true; } -#if NETFRAMEWORK +#if NETFRAMEWORK || NETSTANDARD return _randomInstance.Value!.Next(int.MaxValue) < int.MaxValue * probability; #else return RandomNumberGenerator.GetInt32(int.MaxValue) < int.MaxValue * probability; From abae3a9deda15b03d21c95a9e81631ec8b90d9cd Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 7 Jul 2025 20:59:59 +0300 Subject: [PATCH 198/472] AIFunctionFactory: add a flag for disabling return schema generation. (#6551) * AIFunctionFactory: add a flag for disabling return schema generation. * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs Co-authored-by: Stephen Toub * Address feedback --------- Co-authored-by: Stephen Toub --- .../Functions/AIFunctionFactory.cs | 5 +++-- .../Functions/AIFunctionFactoryOptions.cs | 13 +++++++++++++ .../Microsoft.Extensions.AI.Abstractions.json | 4 ++++ .../Functions/AIFunctionFactoryTest.cs | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 5ad178e7bc8..0d7de9a341e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -615,7 +615,7 @@ public static ReflectionAIFunctionDescriptor GetOrCreate(MethodInfo method, AIFu serializerOptions.MakeReadOnly(); ConcurrentDictionary innerCache = _descriptorCache.GetOrCreateValue(serializerOptions); - DescriptorKey key = new(method, options.Name, options.Description, options.ConfigureParameterBinding, options.MarshalResult, schemaOptions); + DescriptorKey key = new(method, options.Name, options.Description, options.ConfigureParameterBinding, options.MarshalResult, options.ExcludeResultSchema, schemaOptions); if (innerCache.TryGetValue(key, out ReflectionAIFunctionDescriptor? descriptor)) { return descriptor; @@ -690,7 +690,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions Name = key.Name ?? GetFunctionName(key.Method); Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; JsonSerializerOptions = serializerOptions; - ReturnJsonSchema = returnType is null ? null : AIJsonUtilities.CreateJsonSchema( + ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema( returnType, description: key.Method.ReturnParameter.GetCustomAttribute(inherit: true)?.Description, serializerOptions: serializerOptions, @@ -1036,6 +1036,7 @@ private record struct DescriptorKey( string? Description, Func? GetBindParameterOptions, Func>? MarshalResult, + bool ExcludeResultSchema, AIJsonSchemaCreateOptions SchemaOptions); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index e71a4687422..ebfffc34908 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -106,6 +106,19 @@ public AIFunctionFactoryOptions() /// public Func>? MarshalResult { get; set; } + /// + /// Gets or sets a value indicating whether a schema should be created for the function's result type, if possible, and included as . + /// + /// + /// + /// The default value is . + /// + /// + /// When set to , results in the produced to always be . + /// + /// + public bool ExcludeResultSchema { get; set; } + /// Provides configuration options produced by the delegate. public readonly record struct ParameterBindingOptions { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 5e87edc01f9..b47765269af 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -303,6 +303,10 @@ "Member": "string? Microsoft.Extensions.AI.AIFunctionFactoryOptions.Description { get; set; }", "Stage": "Stable" }, + { + "Member": "bool Microsoft.Extensions.AI.AIFunctionFactoryOptions.ExcludeResultSchema { get; set; }", + "Stage": "Stable" + }, { "Member": "Microsoft.Extensions.AI.AIJsonSchemaCreateOptions? Microsoft.Extensions.AI.AIFunctionFactoryOptions.JsonSchemaCreateOptions { get; set; }", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index afced22038f..8c0c7d057a6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -274,6 +274,7 @@ public void AIFunctionFactoryOptions_DefaultValues() Assert.Null(options.SerializerOptions); Assert.Null(options.JsonSchemaCreateOptions); Assert.Null(options.ConfigureParameterBinding); + Assert.False(options.ExcludeResultSchema); } [Fact] @@ -300,6 +301,21 @@ public async Task AIFunctionFactoryOptions_SupportsSkippingParameters() Assert.Contains("test42", result.ToString()); } + [Fact] + public void AIFunctionFactoryOptions_SupportsSkippingReturnSchema() + { + AIFunction func = AIFunctionFactory.Create( + (string firstParameter, int secondParameter) => firstParameter + secondParameter, + new() + { + ExcludeResultSchema = true, + }); + + Assert.Contains("firstParameter", func.JsonSchema.ToString()); + Assert.Contains("secondParameter", func.JsonSchema.ToString()); + Assert.Null(func.ReturnJsonSchema); + } + [Fact] public async Task AIFunctionArguments_SatisfiesParameters() { From eb845248ca8e31de47939df785506bcb7030ac8d Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 8 Jul 2025 09:45:50 +0300 Subject: [PATCH 199/472] Simplify Http.Diagnostics (#6174) --- .../DownstreamDependencyMetadataManager.cs | 33 +++++--- .../HttpClientLatencyTelemetryExtensions.cs | 4 +- .../Internal/HttpClientLatencyContext.cs | 5 -- .../Internal/HttpLatencyTelemetryHandler.cs | 18 ++--- .../Internal/HttpRequestLatencyListener.cs | 81 +++++-------------- .../Logging/Internal/HttpClientLogger.cs | 8 +- .../Logging/Internal/HttpRequestBodyReader.cs | 37 ++------- .../Internal/HttpResponseBodyReader.cs | 34 +++----- ...crosoft.Extensions.Http.Diagnostics.csproj | 5 +- ...icrosoft.Extensions.Http.Resilience.csproj | 4 - .../HttpRequestLatencyListenerTest.cs | 30 ++----- 11 files changed, 78 insertions(+), 181 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs index fbe54413cbf..c8b03913533 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Http/DownstreamDependencyMetadataManager.cs @@ -100,31 +100,40 @@ private static void AddRouteToTrie(RequestMetadata routeMetadata, Dictionary requestRouteAsSpan = routeMetadata.RequestRoute.AsSpan(); - - if (requestRouteAsSpan.Length > 0) + var route = routeMetadata.RequestRoute; + if (!string.IsNullOrEmpty(route)) { - if (requestRouteAsSpan[0] != '/') + var routeSpan = route.AsSpan(); + if (routeSpan.StartsWith("//".AsSpan())) { - requestRouteAsSpan = $"/{routeMetadata.RequestRoute}".AsSpan(); + routeSpan = routeSpan.Slice(1); } - else if (requestRouteAsSpan.StartsWith("//".AsSpan(), StringComparison.OrdinalIgnoreCase)) + + if (routeSpan.Length > 1 && routeSpan[routeSpan.Length - 1] == '/') { - requestRouteAsSpan = requestRouteAsSpan.Slice(1); + routeSpan = routeSpan.Slice(0, routeSpan.Length - 1); } - if (requestRouteAsSpan.Length > 1 && requestRouteAsSpan[requestRouteAsSpan.Length - 1] == '/') + if (routeSpan[0] != '/') { - requestRouteAsSpan = requestRouteAsSpan.Slice(0, requestRouteAsSpan.Length - 1); +#if NET + route = $"/{routeSpan}"; +#else + route = $"/{routeSpan.ToString()}"; +#endif } + else if (routeSpan.Length != route.Length) + { + route = routeSpan.ToString(); + } + + route = _routeRegex.Replace(route, "*").ToUpperInvariant(); } else { - requestRouteAsSpan = "/".AsSpan(); + route = "/"; } - var route = _routeRegex.Replace(requestRouteAsSpan.ToString(), "*"); - route = route.ToUpperInvariant(); for (int i = 0; i < route.Length; i++) { char ch = route[i]; diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs index 309f24e1f2d..fa77360a8c1 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -30,8 +30,8 @@ public static IServiceCollection AddHttpClientLatencyTelemetry(this IServiceColl _ = services.RegisterCheckpointNames(HttpCheckpoints.Checkpoints); _ = services.AddOptions(); - _ = services.AddActivatedSingleton(); - _ = services.AddActivatedSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); _ = services.AddTransient(); _ = services.AddHttpClientLogEnricher(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs index 338326cc690..d5b5f0a40fb 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpClientLatencyContext.cs @@ -19,9 +19,4 @@ public void Set(ILatencyContext context) { _latencyContext.Value = context; } - - public void Unset() - { - _latencyContext.Value = null; - } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs index d0d38a875ec..3180cb890c6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Diagnostics.Latency; using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Latency.Internal; @@ -24,17 +23,14 @@ internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler private readonly string _applicationName; public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, - IOptions options, IOptions appMetdata) + IOptions options, IOptions appMetadata) { - var appMetadata = Throw.IfMemberNull(appMetdata, appMetdata.Value); - var telemetryOptions = Throw.IfMemberNull(options, options.Value); - _latencyListener = latencyListener; _latencyContextProvider = latencyContextProvider; _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); - _applicationName = appMetdata.Value.ApplicationName; + _applicationName = appMetadata.Value.ApplicationName; - if (telemetryOptions.EnableDetailedLatencyBreakdown) + if (options.Value.EnableDetailedLatencyBreakdown) { _latencyListener.Enable(); } @@ -46,12 +42,8 @@ protected async override Task SendAsync(HttpRequestMessage context.AddCheckpoint(_handlerStart); _latencyListener.LatencyContext.Set(context); - request.Headers.Add(TelemetryConstants.ClientApplicationNameHeader, _applicationName); - - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - - _latencyListener.LatencyContext.Unset(); + _ = request.Headers.TryAddWithoutValidation(TelemetryConstants.ClientApplicationNameHeader, _applicationName); - return response; + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs index 744d7a5ddd3..214d877b92e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs @@ -1,8 +1,6 @@ // 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.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -17,17 +15,10 @@ internal sealed class HttpRequestLatencyListener : EventListener private const string HttpProviderName = "System.Net.Http"; private const string NameResolutionProivderName = "System.Net.NameResolution"; - private readonly ConcurrentDictionary _eventSources = new() - { - [SocketProviderName] = null, - [HttpProviderName] = null, - [NameResolutionProivderName] = null - }; + private readonly FrozenDictionary> _eventToTokenMap; internal HttpClientLatencyContext LatencyContext { get; } - private readonly EventToCheckpointToken _eventToCheckpointToken; - private int _enabled; internal bool Enabled => _enabled == 1; @@ -35,39 +26,32 @@ internal sealed class HttpRequestLatencyListener : EventListener public HttpRequestLatencyListener(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) { LatencyContext = latencyContext; - _eventToCheckpointToken = new(tokenIssuer); + _eventToTokenMap = EventToCheckpointToken.Build(tokenIssuer); } public void Enable() { if (Interlocked.CompareExchange(ref _enabled, 1, 0) == 0) { - foreach (var eventSource in _eventSources) - { - if (eventSource.Value != null) - { - EnableEventSource(eventSource.Value); - } - } + // process already existing listeners once again + EventSourceCreated += (_, args) => OnEventSourceCreated(args.EventSource!); } } internal void OnEventWritten(string eventSourceName, string? eventName) { // If event of interest, add a checkpoint for it. - CheckpointToken? token = _eventToCheckpointToken.GetCheckpointToken(eventSourceName, eventName); - if (token.HasValue) + if (eventName != null && _eventToTokenMap[eventSourceName].TryGetValue(eventName, out var token)) { - LatencyContext.Get()?.AddCheckpoint(token.Value); + LatencyContext.Get()?.AddCheckpoint(token); } } internal void OnEventSourceCreated(string eventSourceName, EventSource eventSource) { - if (_eventSources.ContainsKey(eventSourceName)) + if (Enabled && _eventToTokenMap.ContainsKey(eventSourceName)) { - _eventSources[eventSourceName] = eventSource; - EnableEventSource(eventSource); + EnableEvents(eventSource, EventLevel.Informational); } } @@ -81,15 +65,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) OnEventWritten(eventData.EventSource.Name, eventData.EventName); } - private void EnableEventSource(EventSource eventSource) - { - if (Enabled && !eventSource.IsEnabled()) - { - EnableEvents(eventSource, EventLevel.Informational); - } - } - - private sealed class EventToCheckpointToken + private static class EventToCheckpointToken { private static readonly Dictionary _socketMap = new() { @@ -117,47 +93,32 @@ private sealed class EventToCheckpointToken { "ResponseContentStop", HttpCheckpoints.ResponseContentEnd } }; - private readonly FrozenDictionary> _eventToTokenMap; - - public EventToCheckpointToken(ILatencyContextTokenIssuer tokenIssuer) + public static FrozenDictionary> Build(ILatencyContextTokenIssuer tokenIssuer) { Dictionary socket = []; - foreach (string key in _socketMap.Keys) + foreach (var kv in _socketMap) { - socket[key] = tokenIssuer.GetCheckpointToken(_socketMap[key]); + socket[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } Dictionary nameResolution = []; - foreach (string key in _nameResolutionMap.Keys) + foreach (var kv in _nameResolutionMap) { - nameResolution[key] = tokenIssuer.GetCheckpointToken(_nameResolutionMap[key]); + nameResolution[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } Dictionary http = []; - foreach (string key in _httpMap.Keys) + foreach (var kv in _httpMap) { - http[key] = tokenIssuer.GetCheckpointToken(_httpMap[key]); + http[kv.Key] = tokenIssuer.GetCheckpointToken(kv.Value); } - _eventToTokenMap = new Dictionary> + return new Dictionary> { - { SocketProviderName, socket.ToFrozenDictionary(StringComparer.Ordinal) }, - { NameResolutionProivderName, nameResolution.ToFrozenDictionary(StringComparer.Ordinal) }, - { HttpProviderName, http.ToFrozenDictionary(StringComparer.Ordinal) } - }.ToFrozenDictionary(StringComparer.Ordinal); - } - - public CheckpointToken? GetCheckpointToken(string eventSourceName, string? eventName) - { - if (eventName != null && _eventToTokenMap.TryGetValue(eventSourceName, out var events)) - { - if (events.TryGetValue(eventName, out var token)) - { - return token; - } - } - - return null; + { SocketProviderName, socket.ToFrozenDictionary() }, + { NameResolutionProivderName, nameResolution.ToFrozenDictionary() }, + { HttpProviderName, http.ToFrozenDictionary() } + }.ToFrozenDictionary(); } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs index bbf8095e384..0e1aeb6d565 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpClientLogger.cs @@ -109,22 +109,22 @@ internal HttpClientLogger( } } - public async ValueTask LogRequestStopAsync( + public ValueTask LogRequestStopAsync( object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed, CancellationToken cancellationToken = default) - => await LogResponseAsync(context, request, response, null, elapsed, cancellationToken).ConfigureAwait(false); + => LogResponseAsync(context, request, response, null, elapsed, cancellationToken); - public async ValueTask LogRequestFailedAsync( + public ValueTask LogRequestFailedAsync( object? context, HttpRequestMessage request, HttpResponseMessage? response, Exception exception, TimeSpan elapsed, CancellationToken cancellationToken = default) - => await LogResponseAsync(context, request, response, exception, elapsed, cancellationToken).ConfigureAwait(false); + => LogResponseAsync(context, request, response, exception, elapsed, cancellationToken); public object? LogRequestStart(HttpRequestMessage request) => throw new NotSupportedException(SyncLoggingExceptionMessage); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs index ed5a3c3f33d..c7abf7f0df8 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestBodyReader.cs @@ -2,21 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Frozen; using System.IO; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -#if NETCOREAPP3_1_OR_GREATER -using Microsoft.Extensions.ObjectPool; -#endif using Microsoft.Shared.Diagnostics; -#if NETCOREAPP3_1_OR_GREATER -using Microsoft.Shared.Pools; -#else -using System.Buffers; -#endif namespace Microsoft.Extensions.Http.Logging.Internal; @@ -27,9 +20,6 @@ internal sealed class HttpRequestBodyReader /// internal readonly TimeSpan RequestReadTimeout; -#if NETCOREAPP3_1_OR_GREATER - private static readonly ObjectPool> _bufferWriterPool = BufferWriterPool.SharedBufferWriterPool; -#endif private readonly FrozenSet _readableRequestContentTypes; private readonly int _requestReadLimit; @@ -93,33 +83,20 @@ private static async ValueTask ReadFromStreamAsync(HttpRequestMessage re #endif var readLimit = Math.Min(readSizeLimit, (int)streamToReadFrom.Length); -#if NETCOREAPP3_1_OR_GREATER - var bufferWriter = _bufferWriterPool.Get(); - try - { - var memory = bufferWriter.GetMemory(readLimit).Slice(0, readLimit); - var charsWritten = await streamToReadFrom.ReadAsync(memory, cancellationToken).ConfigureAwait(false); - - return Encoding.UTF8.GetString(memory[..charsWritten].Span); - } - finally - { - _bufferWriterPool.Return(bufferWriter); - streamToReadFrom.Seek(0, SeekOrigin.Begin); - } - -#else var buffer = ArrayPool.Shared.Rent(readLimit); try { - _ = await streamToReadFrom.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - return Encoding.UTF8.GetString(buffer.AsSpan(0, readLimit).ToArray()); +#if NET + var read = await streamToReadFrom.ReadAsync(buffer.AsMemory(0, readLimit), cancellationToken).ConfigureAwait(false); +#else + var read = await streamToReadFrom.ReadAsync(buffer, 0, readLimit, cancellationToken).ConfigureAwait(false); +#endif + return Encoding.UTF8.GetString(buffer, 0, read); } finally { ArrayPool.Shared.Return(buffer); streamToReadFrom.Seek(0, SeekOrigin.Begin); } -#endif } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs index 0c5b6a672b1..8022ee74197 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpResponseBodyReader.cs @@ -112,10 +112,7 @@ private static async ValueTask ReadFromStreamAsync(HttpResponseMessage r // if stream is not seekable we need to write the rest of the stream to the pipe // and create a new response content with the pipe reader as stream - _ = Task.Run(async () => - { - await WriteStreamToPipeAsync(streamToReadFrom, pipe.Writer, cancellationToken).ConfigureAwait(false); - }, CancellationToken.None); + _ = WriteStreamToPipeAsync(streamToReadFrom, pipe.Writer, cancellationToken); // use the pipe reader as stream for the new content var newContent = new StreamContent(pipe.Reader.AsStream()); @@ -130,41 +127,29 @@ private static async ValueTask ReadFromStreamAsync(HttpResponseMessage r } #if NET6_0_OR_GREATER - private static async Task BufferStreamAndWriteToPipeAsync(Stream stream, PipeWriter writer, int bufferSize, CancellationToken cancellationToken) + private static async ValueTask BufferStreamAndWriteToPipeAsync(Stream stream, PipeWriter writer, int bufferSize, CancellationToken cancellationToken) { Memory memory = writer.GetMemory(bufferSize)[..bufferSize]; -#if NET8_0_OR_GREATER int bytesRead = await stream.ReadAtLeastAsync(memory, bufferSize, false, cancellationToken).ConfigureAwait(false); -#else - int bytesRead = 0; - while (bytesRead < bufferSize) - { - int read = await stream.ReadAsync(memory.Slice(bytesRead), cancellationToken).ConfigureAwait(false); - if (read == 0) - { - break; - } - - bytesRead += read; - } -#endif - if (bytesRead == 0) { return string.Empty; } + var res = Encoding.UTF8.GetString(memory.Span[..bytesRead]); writer.Advance(bytesRead); - return Encoding.UTF8.GetString(memory[..bytesRead].Span); + return res; } private static async Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + while (true) { - Memory memory = writer.GetMemory(ChunkSize)[..ChunkSize]; + Memory memory = writer.GetMemory(ChunkSize); int bytesRead = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) @@ -216,7 +201,10 @@ private static async Task BufferStreamAndWriteToPipeAsync(Stream stream, return sb.ToString(); } - private static async Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) + private static Task WriteStreamToPipeAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) + => Task.Run(() => WriteStreamToPipeImplAsync(stream, writer, cancellationToken), CancellationToken.None); + + private static async Task WriteStreamToPipeImplAsync(Stream stream, PipeWriter writer, CancellationToken cancellationToken) { while (true) { diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj index cc00c907ded..53cd4c1e0d2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj @@ -31,13 +31,12 @@ - - - + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index 8d280d747cb..8ab678e8eb0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -36,10 +36,6 @@ - - - - diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs index 3675bc3901a..fa91daa1c3e 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs @@ -16,10 +16,9 @@ public void HttpClientLatencyContext_Set_BasicFunction() { var lc = HttpMockProvider.GetLatencyContext(); var context = new HttpClientLatencyContext(); + Assert.Null(context.Get()); context.Set(lc.Object); Assert.Equal(context.Get(), lc.Object); - context.Unset(); - Assert.Null(context.Get()); } [Fact] @@ -115,28 +114,6 @@ public void HttpRequestLatencyListener_OnEventSourceCreated_HttpSources() Assert.True(esNameRes.IsEnabled()); } - [Fact] - public void HttpRequestLatencyListener_OnEventSourceCreated_Twice() - { - var lcti = HttpMockProvider.GetTokenIssuer(); - var lc = HttpMockProvider.GetLatencyContext(); - var context = new HttpClientLatencyContext(); - context.Set(lc.Object); - - using var listener = HttpMockProvider.GetListener(context, lcti.Object); - Assert.NotNull(listener); - listener.Enable(); - - using var esSockets = new HttpMockProvider.SockeyMockEventSource(); - listener.OnEventSourceCreated("System.Net.Sockets", esSockets); - Assert.Equal(1, esSockets.OnEventInvoked); - Assert.True(esSockets.IsEnabled()); - - listener.OnEventSourceCreated("System.Net.Sockets", esSockets); - Assert.Equal(1, esSockets.OnEventInvoked); - Assert.True(esSockets.IsEnabled()); - } - [Fact] public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_NonHttp() { @@ -146,15 +123,18 @@ public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_NonH context.Set(lc.Object); using var listener = HttpMockProvider.GetListener(context, lcti.Object); + listener.Enable(); var events = new[] { "ConnectionEstablished", "RequestLeftQueue", "ResolutionStop", "ConnectStart", "New" }; + using var es = new HttpMockProvider.MockEventSource(); + listener.OnEventSourceCreated("System.Net", es); for (int i = 0; i < events.Length; i++) { - listener.OnEventWritten("System.Net", events[i]); + es.Write(events[i]); } lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Never); From 23c62b80989f7baf55dc394829895b636f960fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borja=20Dom=C3=ADnguez?= Date: Tue, 8 Jul 2025 11:04:13 +0200 Subject: [PATCH 200/472] Add netstandard2.0 compatibility to Microsoft.Extensions.Http.Resilience and dependencies (#6582) --- ...t.Extensions.Diagnostics.ExceptionSummarization.csproj | 1 + .../Latency/Internal/HttpRequestLatencyListener.cs | 8 ++++++++ .../Microsoft.Extensions.Http.Diagnostics.csproj | 1 + .../Microsoft.Extensions.Http.Resilience.csproj | 3 ++- .../Microsoft.Extensions.Resilience.csproj | 1 + 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj index 0073e039f8f..4b4993a7b99 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj @@ -3,6 +3,7 @@ Microsoft.Extensions.Diagnostics.ExceptionSummarization Lets you retrieve exception summary information. Telemetry + $(NetCoreTargetFrameworks);netstandard2.0;net462 diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs index 214d877b92e..5d7238ed8a3 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs @@ -33,8 +33,16 @@ public void Enable() { if (Interlocked.CompareExchange(ref _enabled, 1, 0) == 0) { +#if NETSTANDARD + foreach (var eventSource in EventSource.GetSources()) + { + OnEventSourceCreated(eventSource.Name, eventSource); + } +#else // process already existing listeners once again EventSourceCreated += (_, args) => OnEventSourceCreated(args.EventSource!); +#endif + } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj index 53cd4c1e0d2..bc7b8f4a6fe 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj @@ -3,6 +3,7 @@ Microsoft.Extensions.Http.Diagnostics Telemetry support for HTTP Client. Telemetry + $(NetCoreTargetFrameworks);netstandard2.0;net462 diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index 8ab678e8eb0..1d1a3684305 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -1,6 +1,7 @@  Microsoft.Extensions.Http.Resilience + $(NetCoreTargetFrameworks);netstandard2.0;net462 Resilience mechanisms for HttpClient. Resilience @@ -28,7 +29,7 @@ - $(NoWarn);LA0006 + $(NoWarn);LA0006 diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj index ebd63256933..519b6632d07 100644 --- a/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj @@ -1,6 +1,7 @@  Microsoft.Extensions.Resilience + $(NetCoreTargetFrameworks);netstandard2.0;net462 Extensions to the Polly libraries to enrich telemetry with metadata and exception summaries. Resilience From 294a33df47d48f39038ebed1c82a8a50e0345e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Hou=C5=A1ka?= Date: Tue, 8 Jul 2025 12:20:46 +0200 Subject: [PATCH 201/472] Ignore null loggers returned by LogProviders in ExtendedLoggerFactory (#6585) --- .../Logging/ExtendedLoggerFactory.cs | 14 +++++++++++--- .../Logging/ExtendedLoggerFactoryTests.cs | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 44ec71b6f68..24d4680a3ad 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Diagnostics.Buffering; #endif using Microsoft.Extensions.Diagnostics.Enrichment; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; @@ -223,13 +224,20 @@ private void AddProviderRegistration(ILoggerProvider provider, bool dispose) private LoggerInformation[] CreateLoggers(string categoryName) { - var loggers = new LoggerInformation[_providerRegistrations.Count]; + var loggers = new List(_providerRegistrations.Count); for (int i = 0; i < _providerRegistrations.Count; i++) { - loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); + var loggerInformation = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); + + // We do not need to check for NullLogger.Instance as no provider would reasonably return it (the handling is at + // outer loggers level, not inner level loggers in Logger/LoggerProvider). + if (loggerInformation.Logger != NullLogger.Instance) + { + loggers.Add(loggerInformation); + } } - return loggers; + return loggers.ToArray(); } private (MessageLogger[] messageLoggers, ScopeLogger[] scopeLoggers) ApplyFilters(LoggerInformation[] loggers) diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerFactoryTests.cs index 3d02c9ae578..f35dc88560a 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerFactoryTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerFactoryTests.cs @@ -8,6 +8,7 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; @@ -536,6 +537,20 @@ public static void CreateDisposeDisposesInnerServiceProvider() Assert.True(disposed); } + [Fact] + public static void NullLoggerByProviderIsIgnored() + { + using var factory = Utils.CreateLoggerFactory(builder => + { + builder.AddProvider(NullLoggerProvider.Instance); + builder.AddProvider(new Provider()); + }); + var logger1 = (ExtendedLogger)factory.CreateLogger("C1"); + Assert.Single(logger1.MessageLoggers); + + logger1.LogInformation("This should not throw an exception."); + } + private class InternalScopeLoggerProvider : ILoggerProvider, ILogger { private IExternalScopeProvider _scopeProvider = new LoggerExternalScopeProvider(); From e6b555c147e47c7b848cae4aa708468deb28b889 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 8 Jul 2025 16:46:11 -0700 Subject: [PATCH 202/472] Fix template tests --- src/ProjectTemplates/GeneratedContent.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index dfe93dbc5a8..e37794c3c0e 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -21,9 +21,9 @@ - Use specific version numbers to pin to already-released packages --> - $(Version) - $(Version) - $(Version) + 9.7.0 + 9.7.0-preview.1.25356.2 + 9.7.0 From d651ccc5fbae3d22812dc1f75ad005be7d1e8415 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 11 Jul 2025 09:56:34 -0400 Subject: [PATCH 203/472] Bump FunctionInvokingChatClient.MaximumIterationsPerRequest from 10 to 40 (#6599) Folks are bumping up against the arbitrary limit of 10, as various modern models are super chatty with tools. While we need a limit to avoid runaway execution, we can make it much higher. --- .../ChatCompletion/FunctionInvokingChatClient.cs | 4 ++-- .../ChatCompletion/FunctionInvokingChatClientTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 6b1d3b3e905..0a8673dc91d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -61,7 +61,7 @@ public partial class FunctionInvokingChatClient : DelegatingChatClient private readonly ActivitySource? _activitySource; /// Maximum number of roundtrips allowed to the inner client. - private int _maximumIterationsPerRequest = 10; + private int _maximumIterationsPerRequest = 40; // arbitrary default to prevent runaway execution /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing. private int _maximumConsecutiveErrorsPerRequest = 3; @@ -142,7 +142,7 @@ public static FunctionInvocationContext? CurrentContext /// /// /// The maximum number of iterations per request. - /// The default value is 10. + /// The default value is 40. /// /// /// diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 1379cef8bf0..b4ce2f1546c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -36,7 +36,7 @@ public void Ctor_HasExpectedDefaults() Assert.False(client.AllowConcurrentInvocation); Assert.False(client.IncludeDetailedErrors); - Assert.Equal(10, client.MaximumIterationsPerRequest); + Assert.Equal(40, client.MaximumIterationsPerRequest); Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); Assert.Null(client.FunctionInvoker); } @@ -55,7 +55,7 @@ public void Properties_Roundtrip() client.IncludeDetailedErrors = true; Assert.True(client.IncludeDetailedErrors); - Assert.Equal(10, client.MaximumIterationsPerRequest); + Assert.Equal(40, client.MaximumIterationsPerRequest); client.MaximumIterationsPerRequest = 5; Assert.Equal(5, client.MaximumIterationsPerRequest); From cc0d010d068ddab0c5951b6cfc5793d379c0532c Mon Sep 17 00:00:00 2001 From: Iliar Turdushev Date: Fri, 11 Jul 2025 15:59:08 +0200 Subject: [PATCH 204/472] Fixes #6598 (#6600) Removes Conditional attribute from the definition of LogProperties and LogPropertyIgnore attributes --- .../Logging/LogPropertiesAttribute.cs | 2 -- .../Logging/LogPropertyIgnoreAttribute.cs | 2 -- .../Generated/LogPropertiesTests.cs | 22 +++++++++++++++++++ ...crosoft.Gen.Logging.Generated.Tests.csproj | 1 + .../HelperLibrary/FieldToLog.cs | 10 +++++++++ ...Microsoft.Gen.Logging.HelperLibrary.csproj | 15 +++++++++++++ .../HelperLibrary/ObjectToLog.cs | 17 ++++++++++++++ .../TestClasses/LogPropertiesExtensions.cs | 4 ++++ .../Unit/EmitterTests.cs | 1 + .../Microsoft.Gen.Logging.Unit.Tests.csproj | 1 + 10 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 test/Generators/Microsoft.Gen.Logging/HelperLibrary/FieldToLog.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/HelperLibrary/Microsoft.Gen.Logging.HelperLibrary.csproj create mode 100644 test/Generators/Microsoft.Gen.Logging/HelperLibrary/ObjectToLog.cs diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs index c2d6a65cd38..ff503f977be 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; @@ -14,7 +13,6 @@ namespace Microsoft.Extensions.Logging; /// /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] -[Conditional("CODE_GENERATION_ATTRIBUTES")] public sealed class LogPropertiesAttribute : Attribute { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs index 954fcdeddb3..4911a6416db 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Logging; @@ -12,7 +11,6 @@ namespace Microsoft.Extensions.Logging; /// /// . [AttributeUsage(AttributeTargets.Property)] -[Conditional("CODE_GENERATION_ATTRIBUTES")] public sealed class LogPropertyIgnoreAttribute : Attribute { } diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/LogPropertiesTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/LogPropertiesTests.cs index a8c5d752fc9..589f237d2cc 100644 --- a/test/Generators/Microsoft.Gen.Logging/Generated/LogPropertiesTests.cs +++ b/test/Generators/Microsoft.Gen.Logging/Generated/LogPropertiesTests.cs @@ -587,4 +587,26 @@ public void LogPropertiesReadonlyRecordStructArgument() latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); } + + [Fact] + public void LogPropertiesCorrectlyLogsObjectFromAnotherAssembly() + { + LogObjectFromAnotherAssembly(_logger, new ObjectToLog + { + PropertyToIgnore = "Foo", + PropertyToLog = "Bar", + FieldToLog = new FieldToLog { Name = "Fizz", Value = "Buzz" } + }); + + Assert.Equal(1, _logger.Collector.Count); + + var state = _logger.Collector.LatestRecord.StructuredState! + .ToDictionary(p => p.Key, p => p.Value); + + Assert.Equal(4, state.Count); + Assert.Contains("{OriginalFormat}", state); + Assert.Contains("logObject.PropertyToLog", state); + Assert.Contains("logObject.FieldToLog.Name", state); + Assert.Contains("logObject.FieldToLog.Value", state); + } } diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Microsoft.Gen.Logging.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Generated/Microsoft.Gen.Logging.Generated.Tests.csproj index 84621f147e7..d4a72e9e371 100644 --- a/test/Generators/Microsoft.Gen.Logging/Generated/Microsoft.Gen.Logging.Generated.Tests.csproj +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Microsoft.Gen.Logging.Generated.Tests.csproj @@ -24,5 +24,6 @@ + diff --git a/test/Generators/Microsoft.Gen.Logging/HelperLibrary/FieldToLog.cs b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/FieldToLog.cs new file mode 100644 index 00000000000..6eb8cc08d5e --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/FieldToLog.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Logging.Test; + +public class FieldToLog +{ + public string? Name { get; set; } + public string? Value { get; set; } +} diff --git a/test/Generators/Microsoft.Gen.Logging/HelperLibrary/Microsoft.Gen.Logging.HelperLibrary.csproj b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/Microsoft.Gen.Logging.HelperLibrary.csproj new file mode 100644 index 00000000000..0f3e6d3bedf --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/Microsoft.Gen.Logging.HelperLibrary.csproj @@ -0,0 +1,15 @@ + + + Microsoft.Gen.Logging.Test + Test classes for Microsoft.Gen.Logging.Generated.Tests. + + + + $(TestNetCoreTargetFrameworks) + $(TestNetCoreTargetFrameworks)$(ConditionalNet462) + + + + + + diff --git a/test/Generators/Microsoft.Gen.Logging/HelperLibrary/ObjectToLog.cs b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/ObjectToLog.cs new file mode 100644 index 00000000000..c8e071ce6d9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/HelperLibrary/ObjectToLog.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Gen.Logging.Test; + +public class ObjectToLog +{ + [LogPropertyIgnore] + public string? PropertyToIgnore { get; set; } + + public string? PropertyToLog { get; set; } + + [LogProperties] + public FieldToLog? FieldToLog { get; set; } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs index d2b9c05b05b..013f8d85956 100644 --- a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.Extensions.Logging; +using Microsoft.Gen.Logging.Test; namespace TestClasses { @@ -244,5 +245,8 @@ public override string ToString() [LoggerMessage(6, LogLevel.Information, "Testing interface-typed argument here...")] public static partial void LogMethodInterfaceArg(ILogger logger, [LogProperties] IMyInterface complexParam); + + [LoggerMessage(7, LogLevel.Information, "Testing logging a complex object residing in another assembly...")] + public static partial void LogObjectFromAnotherAssembly(ILogger logger, [LogProperties] ObjectToLog logObject); } } diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/EmitterTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/EmitterTests.cs index 58012c4915b..a53f1711bd3 100644 --- a/test/Generators/Microsoft.Gen.Logging/Unit/EmitterTests.cs +++ b/test/Generators/Microsoft.Gen.Logging/Unit/EmitterTests.cs @@ -45,6 +45,7 @@ public async Task TestEmitter() Assembly.GetAssembly(typeof(IRedactorProvider))!, Assembly.GetAssembly(typeof(PrivateDataAttribute))!, Assembly.GetAssembly(typeof(BigInteger))!, + Assembly.GetAssembly(typeof(ObjectToLog))! }, sources, symbols) diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Microsoft.Gen.Logging.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Unit/Microsoft.Gen.Logging.Unit.Tests.csproj index e566a5dbe9f..9090a895c67 100644 --- a/test/Generators/Microsoft.Gen.Logging/Unit/Microsoft.Gen.Logging.Unit.Tests.csproj +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Microsoft.Gen.Logging.Unit.Tests.csproj @@ -20,6 +20,7 @@ + From 194d9dbff59c5ea7aeea295dce8f1affb75ee31f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 11 Jul 2025 17:27:11 -0400 Subject: [PATCH 205/472] Expose M.E.AI.OpenAI input message conversions (#6601) Internally we have helpers that convert from M.E.AI chat messages to the various OpenAI object models. To ease interop when a developer gets M.E.AI messages from another library and then wants to submit them on their own to OpenAI, this just exposes those helpers publicly. --- .../OpenAIChatClient.cs | 10 +- .../OpenAIClientExtensions.cs | 12 ++ .../OpenAIResponseChatClient.cs | 4 +- .../OpenAIAIFunctionConversionTests.cs | 77 -------- .../OpenAIConversionTests.cs | 186 ++++++++++++++++++ 5 files changed, 205 insertions(+), 84 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 3be0a1cc1ee..394fccad1b6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -70,7 +70,7 @@ public async Task GetResponseAsync( { _ = Throw.IfNull(messages); - var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions); + var openAIChatMessages = ToOpenAIChatMessages(messages, options); var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. @@ -85,7 +85,7 @@ public IAsyncEnumerable GetStreamingResponseAsync( { _ = Throw.IfNull(messages); - var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions); + var openAIChatMessages = ToOpenAIChatMessages(messages, options); var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. @@ -115,7 +115,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op } /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable. - private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, ChatOptions? chatOptions, JsonSerializerOptions jsonOptions) + internal static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, ChatOptions? chatOptions) { // Maps all of the M.E.AI types to the corresponding OpenAI types. // Unrecognized or non-processable content is ignored. @@ -148,7 +148,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op { try { - result = JsonSerializer.Serialize(resultContent.Result, jsonOptions.GetTypeInfo(typeof(object))); + result = JsonSerializer.Serialize(resultContent.Result, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); } catch (NotSupportedException) { @@ -176,7 +176,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op case FunctionCallContent fc: (toolCalls ??= []).Add( ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes( - fc.Arguments, jsonOptions.GetTypeInfo(typeof(IDictionary)))))); + fc.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))))); break; default: diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index dccddf3038e..9f42fa88773 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -181,6 +181,18 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); + /// Creates a sequence of OpenAI instances from the specified input messages. + /// The input messages to convert. + /// A sequence of OpenAI chat messages. + public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages) => + OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null); + + /// Creates a sequence of OpenAI instances from the specified input messages. + /// The input messages to convert. + /// A sequence of OpenAI response items. + public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages) => + OpenAIResponseChatClient.ToOpenAIResponseItems(Throw.IfNull(messages)); + // TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict. /// Gets whether the properties specify that strict schema handling is desired. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 6aee4bc77e4..c4a1261844c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -17,6 +17,7 @@ #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable S3604 // Member initializer values should not be redundant +#pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; @@ -466,8 +467,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt } /// Convert a sequence of s to s. - private static IEnumerable ToOpenAIResponseItems( - IEnumerable inputs) + internal static IEnumerable ToOpenAIResponseItems(IEnumerable inputs) { foreach (ChatMessage input in inputs) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs deleted file mode 100644 index ce458473c59..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs +++ /dev/null @@ -1,77 +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.ComponentModel; -using System.Text.Json; -using OpenAI.Assistants; -using OpenAI.Chat; -using OpenAI.Realtime; -using OpenAI.Responses; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class OpenAIAIFunctionConversionTests -{ - private static readonly AIFunction _testFunction = AIFunctionFactory.Create( - ([Description("The name parameter")] string name) => name, - "test_function", - "A test function for conversion"); - - [Fact] - public void AsOpenAIChatTool_ProducesValidInstance() - { - var tool = _testFunction.AsOpenAIChatTool(); - - Assert.NotNull(tool); - Assert.Equal("test_function", tool.FunctionName); - Assert.Equal("A test function for conversion", tool.FunctionDescription); - ValidateSchemaParameters(tool.FunctionParameters); - } - - [Fact] - public void AsOpenAIResponseTool_ProducesValidInstance() - { - var tool = _testFunction.AsOpenAIResponseTool(); - - Assert.NotNull(tool); - } - - [Fact] - public void AsOpenAIConversationFunctionTool_ProducesValidInstance() - { - var tool = _testFunction.AsOpenAIConversationFunctionTool(); - - Assert.NotNull(tool); - Assert.Equal("test_function", tool.Name); - Assert.Equal("A test function for conversion", tool.Description); - ValidateSchemaParameters(tool.Parameters); - } - - [Fact] - public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() - { - var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); - - Assert.NotNull(tool); - Assert.Equal("test_function", tool.FunctionName); - Assert.Equal("A test function for conversion", tool.Description); - ValidateSchemaParameters(tool.Parameters); - } - - /// Helper method to validate function parameters match our schema. - private static void ValidateSchemaParameters(BinaryData parameters) - { - Assert.NotNull(parameters); - - using var jsonDoc = JsonDocument.Parse(parameters); - var root = jsonDoc.RootElement; - - Assert.Equal("object", root.GetProperty("type").GetString()); - Assert.True(root.TryGetProperty("properties", out var properties)); - Assert.True(properties.TryGetProperty("name", out var nameProperty)); - Assert.Equal("string", nameProperty.GetProperty("type").GetString()); - Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString()); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs new file mode 100644 index 00000000000..951554eda75 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -0,0 +1,186 @@ +// 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.ComponentModel; +using System.Linq; +using System.Text.Json; +using OpenAI.Assistants; +using OpenAI.Chat; +using OpenAI.Realtime; +using OpenAI.Responses; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenAIConversionTests +{ + private static readonly AIFunction _testFunction = AIFunctionFactory.Create( + ([Description("The name parameter")] string name) => name, + "test_function", + "A test function for conversion"); + + [Fact] + public void AsOpenAIChatTool_ProducesValidInstance() + { + var tool = _testFunction.AsOpenAIChatTool(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.FunctionName); + Assert.Equal("A test function for conversion", tool.FunctionDescription); + ValidateSchemaParameters(tool.FunctionParameters); + } + + [Fact] + public void AsOpenAIResponseTool_ProducesValidInstance() + { + var tool = _testFunction.AsOpenAIResponseTool(); + + Assert.NotNull(tool); + } + + [Fact] + public void AsOpenAIConversationFunctionTool_ProducesValidInstance() + { + var tool = _testFunction.AsOpenAIConversationFunctionTool(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.Name); + Assert.Equal("A test function for conversion", tool.Description); + ValidateSchemaParameters(tool.Parameters); + } + + [Fact] + public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance() + { + var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition(); + + Assert.NotNull(tool); + Assert.Equal("test_function", tool.FunctionName); + Assert.Equal("A test function for conversion", tool.Description); + ValidateSchemaParameters(tool.Parameters); + } + + /// Helper method to validate function parameters match our schema. + private static void ValidateSchemaParameters(BinaryData parameters) + { + Assert.NotNull(parameters); + + using var jsonDoc = JsonDocument.Parse(parameters); + var root = jsonDoc.RootElement; + + Assert.Equal("object", root.GetProperty("type").GetString()); + Assert.True(root.TryGetProperty("properties", out var properties)); + Assert.True(properties.TryGetProperty("name", out var nameProperty)); + Assert.Equal("string", nameProperty.GetProperty("type").GetString()); + Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString()); + } + + [Fact] + public void AsOpenAIChatMessages_ProducesExpectedOutput() + { + Assert.Throws("messages", () => ((IEnumerable)null!).AsOpenAIChatMessages()); + + List messages = + [ + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, + [ + new TextContent("Hi there!"), + new FunctionCallContent("callid123", "SomeFunction", new Dictionary + { + ["param1"] = "value1", + ["param2"] = 42 + }), + ]), + new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]), + new(ChatRole.Assistant, "The answer is 42."), + ]; + + var convertedMessages = messages.AsOpenAIChatMessages().ToArray(); + + Assert.Equal(5, convertedMessages.Length); + + SystemChatMessage m0 = Assert.IsType(convertedMessages[0]); + Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text); + + UserChatMessage m1 = Assert.IsType(convertedMessages[1]); + Assert.Equal("Hello", Assert.Single(m1.Content).Text); + + AssistantChatMessage m2 = Assert.IsType(convertedMessages[2]); + Assert.Single(m2.Content); + Assert.Equal("Hi there!", m2.Content[0].Text); + var tc = Assert.Single(m2.ToolCalls); + Assert.Equal("callid123", tc.Id); + Assert.Equal("SomeFunction", tc.FunctionName); + Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary + { + ["param1"] = "value1", + ["param2"] = 42 + }), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span))); + + ToolChatMessage m3 = Assert.IsType(convertedMessages[3]); + Assert.Equal("callid123", m3.ToolCallId); + Assert.Equal("theresult", Assert.Single(m3.Content).Text); + + AssistantChatMessage m4 = Assert.IsType(convertedMessages[4]); + Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text); + } + + [Fact] + public void AsOpenAIResponseItems_ProducesExpectedOutput() + { + Assert.Throws("messages", () => ((IEnumerable)null!).AsOpenAIResponseItems()); + + List messages = + [ + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, + [ + new TextContent("Hi there!"), + new FunctionCallContent("callid123", "SomeFunction", new Dictionary + { + ["param1"] = "value1", + ["param2"] = 42 + }), + ]), + new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]), + new(ChatRole.Assistant, "The answer is 42."), + ]; + + var convertedItems = messages.AsOpenAIResponseItems().ToArray(); + + Assert.Equal(6, convertedItems.Length); + + MessageResponseItem m0 = Assert.IsAssignableFrom(convertedItems[0]); + Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text); + + MessageResponseItem m1 = Assert.IsAssignableFrom(convertedItems[1]); + Assert.Equal(OpenAI.Responses.MessageRole.User, m1.Role); + Assert.Equal("Hello", Assert.Single(m1.Content).Text); + + MessageResponseItem m2 = Assert.IsAssignableFrom(convertedItems[2]); + Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m2.Role); + Assert.Equal("Hi there!", Assert.Single(m2.Content).Text); + + FunctionCallResponseItem m3 = Assert.IsAssignableFrom(convertedItems[3]); + Assert.Equal("callid123", m3.CallId); + Assert.Equal("SomeFunction", m3.FunctionName); + Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary + { + ["param1"] = "value1", + ["param2"] = 42 + }), JsonSerializer.Deserialize(m3.FunctionArguments.ToMemory().Span))); + + FunctionCallOutputResponseItem m4 = Assert.IsAssignableFrom(convertedItems[4]); + Assert.Equal("callid123", m4.CallId); + Assert.Equal("theresult", m4.FunctionOutput); + + MessageResponseItem m5 = Assert.IsAssignableFrom(convertedItems[5]); + Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m5.Role); + Assert.Equal("The answer is 42.", Assert.Single(m5.Content).Text); + } +} From fb806a646519e8e0996695ac163795c5d4162573 Mon Sep 17 00:00:00 2001 From: Evgeny Fedorov <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:49:39 +0200 Subject: [PATCH 206/472] Add memory usage metric (#6586) --- .../Linux/LinuxUtilizationProvider.cs | 44 ++++-- .../Linux/Log.cs | 8 +- .../Windows/Log.cs | 15 +- .../WindowsContainerSnapshotProvider.cs | 79 ++++++++-- .../Windows/WindowsSnapshotProvider.cs | 2 +- .../ResourceUtilizationInstruments.cs | 8 + .../ResourceHealthCheckExtensionsTests.cs | 1 + .../Linux/AcceptanceTest.cs | 57 +++++-- .../Linux/LinuxUtilizationProviderTests.cs | 17 ++- .../WindowsContainerSnapshotProviderTests.cs | 140 ++++++++++++++++++ 10 files changed, 320 insertions(+), 51 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 0c3d4124e17..611b96f4f1d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -37,7 +37,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private DateTimeOffset _refreshAfterMemory; private double _cpuPercentage = double.NaN; private double _lastCpuCoresUsed = double.NaN; - private double _memoryPercentage; + private ulong _memoryUsage; private long _previousCgroupCpuTime; private long _previousHostCpuTime; private long _previousCgroupCpuPeriodCounter; @@ -116,12 +116,18 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValues: () => GetMeasurementWithRetry(MemoryUtilization), + observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); + _ = meter.CreateObservableUpDownCounter( + name: ResourceUtilizationInstruments.ContainerMemoryUsage, + observeValues: () => GetMeasurementWithRetry(() => (long)MemoryUsage()), + unit: "By", + description: "Memory usage of the container."); + _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValues: () => GetMeasurementWithRetry(MemoryUtilization), + observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core @@ -216,7 +222,7 @@ public double CpuUtilization() return _cpuPercentage; } - public double MemoryUtilization() + public ulong MemoryUsage() { DateTimeOffset now = _timeProvider.GetUtcNow(); @@ -224,26 +230,24 @@ public double MemoryUtilization() { if (now < _refreshAfterMemory) { - return _memoryPercentage; + return _memoryUsage; } } - ulong memoryUsed = _parser.GetMemoryUsageInBytes(); + ulong memoryUsage = _parser.GetMemoryUsageInBytes(); lock (_memoryLocker) { if (now >= _refreshAfterMemory) { - double memoryPercentage = Math.Min(One, (double)memoryUsed / _memoryLimit); - - _memoryPercentage = memoryPercentage; + _memoryUsage = memoryUsage; _refreshAfterMemory = now.Add(_memoryRefreshInterval); } } - _logger.MemoryUsageData(memoryUsed, _memoryLimit, _memoryPercentage); + _logger.MemoryUsageData(_memoryUsage); - return _memoryPercentage; + return _memoryUsage; } /// @@ -264,14 +268,24 @@ public Snapshot GetSnapshot() memoryUsageInBytes: memoryUsed); } - private Measurement[] GetMeasurementWithRetry(Func func) + private double MemoryPercentage() + { + ulong memoryUsage = MemoryUsage(); + double memoryPercentage = Math.Min(One, (double)memoryUsage / _memoryLimit); + + _logger.MemoryPercentageData(memoryUsage, _memoryLimit, memoryPercentage); + return memoryPercentage; + } + + private Measurement[] GetMeasurementWithRetry(Func func) + where T : struct { - if (!TryGetValueWithRetry(func, out double value)) + if (!TryGetValueWithRetry(func, out T value)) { - return Array.Empty>(); + return Array.Empty>(); } - return new[] { new Measurement(value) }; + return new[] { new Measurement(value) }; } private bool TryGetValueWithRetry(Func func, out T value) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs index 209a495e844..c021f48bb1b 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs @@ -24,7 +24,7 @@ public static partial void CpuUsageData( [LoggerMessage(2, LogLevel.Debug, "Computed memory usage with MemoryUsedInBytes = {memoryUsed}, MemoryLimit = {memoryLimit}, MemoryPercentage = {memoryPercentage}.")] - public static partial void MemoryUsageData( + public static partial void MemoryPercentageData( this ILogger logger, ulong memoryUsed, double memoryLimit, @@ -55,4 +55,10 @@ public static partial void CpuUsageDataV2( public static partial void HandleDiskStatsException( this ILogger logger, string errorMessage); + + [LoggerMessage(6, LogLevel.Debug, + "Computed memory usage = {memoryUsed}.")] + public static partial void MemoryUsageData( + this ILogger logger, + ulong memoryUsed); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs index fdc0d17fe44..e8d8411925d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs @@ -26,12 +26,11 @@ public static partial void CpuUsageData( double cpuPercentage); [LoggerMessage(4, LogLevel.Debug, - "Computed memory usage with CurrentMemoryUsage = {currentMemoryUsage}, TotalMemory = {totalMemory}, MemoryPercentage = {memoryPercentage}.")] - public static partial void MemoryUsageData( + "Computed memory usage for container: CurrentMemoryUsage = {currentMemoryUsage}, TotalMemory = {totalMemory}")] + public static partial void ContainerMemoryUsageData( this ILogger logger, ulong currentMemoryUsage, - double totalMemory, - double memoryPercentage); + double totalMemory); #pragma warning disable S103 // Lines should not be too long [LoggerMessage(5, LogLevel.Debug, "Computed CPU usage with CpuUsageKernelTicks = {cpuUsageKernelTicks}, CpuUsageUserTicks = {cpuUsageUserTicks}, OldCpuUsageTicks = {oldCpuUsageTicks}, TimeTickDelta = {timeTickDelta}, CpuUnits = {cpuUnits}, CpuPercentage = {cpuPercentage}.")] @@ -60,4 +59,12 @@ public static partial void DiskIoPerfCounterException( this ILogger logger, string counterName, string errorMessage); + + [LoggerMessage(8, LogLevel.Debug, + "Computed memory usage for current process: ProcessMemoryUsage = {processMemoryUsage}, TotalMemory = {totalMemory}, MemoryPercentage = {memoryPercentage}")] + public static partial void ProcessMemoryPercentageData( + this ILogger logger, + ulong processMemoryUsage, + double totalMemory, + double memoryPercentage); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index ca6ceaff8bd..ce10cad0471 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -29,6 +29,7 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); + private readonly object _processMemoryLocker = new(); private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; private readonly ILogger _logger; @@ -42,8 +43,10 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private long _oldCpuTimeTicks; private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; + private DateTimeOffset _refreshAfterProcessMemory; private double _cpuPercentage = double.NaN; - private double _memoryPercentage; + private ulong _memoryUsage; + private double _processMemoryPercentage; public SystemResources Resources { get; } @@ -107,6 +110,7 @@ internal WindowsContainerSnapshotProvider( _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; _refreshAfterCpu = _timeProvider.GetUtcNow(); _refreshAfterMemory = _timeProvider.GetUtcNow(); + _refreshAfterProcessMemory = _timeProvider.GetUtcNow(); #pragma warning disable CA2000 // Dispose objects before losing scope // We don't dispose the meter because IMeterFactory handles that @@ -116,13 +120,34 @@ internal WindowsContainerSnapshotProvider( #pragma warning restore CA2000 // Dispose objects before losing scope // Container based metrics: - _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); + _ = meter.CreateObservableCounter( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: GetCpuTime, + unit: "s", + description: "CPU time used by the container."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValue: CpuPercentage); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, + observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier)); + + _ = meter.CreateObservableUpDownCounter( + name: ResourceUtilizationInstruments.ContainerMemoryUsage, + observeValue: () => (long)MemoryUsage(), + unit: "By", + description: "Memory usage of the container."); // Process based metrics: - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessCpuUtilization, observeValue: CpuPercentage); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessMemoryUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetCurrentProcessMemoryUsage())); + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessCpuUtilization, + observeValue: CpuPercentage); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessMemoryUtilization, + observeValue: ProcessMemoryPercentage); } public Snapshot GetSnapshot() @@ -185,7 +210,35 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) return memoryLimitInBytes; } - private double MemoryPercentage(Func getMemoryUsage) + private double ProcessMemoryPercentage() + { + DateTimeOffset now = _timeProvider.GetUtcNow(); + + lock (_processMemoryLocker) + { + if (now < _refreshAfterProcessMemory) + { + return _processMemoryPercentage; + } + } + + ulong processMemoryUsage = _processInfo.GetCurrentProcessMemoryUsage(); + + lock (_processMemoryLocker) + { + if (now >= _refreshAfterProcessMemory) + { + _processMemoryPercentage = Math.Min(_metricValueMultiplier, processMemoryUsage / _memoryLimit * _metricValueMultiplier); + _refreshAfterProcessMemory = now.Add(_memoryRefreshInterval); + + _logger.ProcessMemoryPercentageData(processMemoryUsage, _memoryLimit, _processMemoryPercentage); + } + + return _processMemoryPercentage; + } + } + + private ulong MemoryUsage() { DateTimeOffset now = _timeProvider.GetUtcNow(); @@ -193,24 +246,22 @@ private double MemoryPercentage(Func getMemoryUsage) { if (now < _refreshAfterMemory) { - return _memoryPercentage; + return _memoryUsage; } } - ulong memoryUsage = getMemoryUsage(); + ulong memoryUsage = _processInfo.GetMemoryUsage(); lock (_memoryLocker) { if (now >= _refreshAfterMemory) { - // Don't change calculation order, otherwise we loose some precision: - _memoryPercentage = Math.Min(_metricValueMultiplier, memoryUsage / _memoryLimit * _metricValueMultiplier); + _memoryUsage = memoryUsage; _refreshAfterMemory = now.Add(_memoryRefreshInterval); + _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit); } - _logger.MemoryUsageData(memoryUsage, _memoryLimit, _memoryPercentage); - - return _memoryPercentage; + return _memoryUsage; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index 837cd0f9a06..e238a59aff9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -144,7 +144,7 @@ private double MemoryPercentage() _refreshAfterMemory = now.Add(_memoryRefreshInterval); } - _logger.MemoryUsageData((ulong)currentMemoryUsage, _totalMemory, _memoryPercentage); + _logger.ProcessMemoryPercentageData((ulong)currentMemoryUsage, _totalMemory, _memoryPercentage); return _memoryPercentage; } diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index 835d3099782..b1593edd396 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -50,6 +50,14 @@ internal static class ResourceUtilizationInstruments /// public const string ContainerMemoryLimitUtilization = "container.memory.limit.utilization"; + /// + /// The name of an instrument to retrieve memory usage measured in bytes of all processes running inside a container or control group. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerMemoryUsage = "container.memory.usage"; + /// /// The name of an instrument to retrieve CPU consumption share of the running process in range [0, 1]. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs index 7d9b347a59d..2599be2dd9e 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs @@ -490,6 +490,7 @@ public async Task TestCpuAndMemoryChecks_WithMetrics( Mock processInfoMock = new(); var appMemoryUsage = memoryUsed; processInfoMock.Setup(p => p.GetMemoryUsage()).Returns(() => appMemoryUsage); + processInfoMock.Setup(p => p.GetCurrentProcessMemoryUsage()).Returns(() => appMemoryUsage); JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; limitInfo.JobMemoryLimit = new UIntPtr(totalMemory); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs index 0221efc27c9..a4107faef03 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTest.cs @@ -220,10 +220,20 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou using var e = new ManualResetEventSlim(); object? meterScope = null; - listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) - => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, tags, _) - => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => + OnInstrumentPublished(instrument, meterListener, meterScope); + listener.SetMeasurementEventCallback((m, f, tags, _) => + OnMeasurementReceived( + m, + f, + tags, + ref cpuUserTime, + ref cpuKernelTime, + ref cpuFromGauge, + ref cpuLimitFromGauge, + ref cpuRequestFromGauge, + ref memoryFromGauge, + ref memoryLimitFromGauge)); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -302,13 +312,31 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou var cpuRequestFromGauge = 0.0d; var memoryFromGauge = 0.0d; var memoryLimitFromGauge = 0.0d; + long memoryUsageFromGauge = 0; using var e = new ManualResetEventSlim(); object? meterScope = null; - listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) - => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, tags, _) - => OnMeasurementReceived(m, f, tags, ref cpuUserTime, ref cpuKernelTime, ref cpuFromGauge, ref cpuLimitFromGauge, ref cpuRequestFromGauge, ref memoryFromGauge, ref memoryLimitFromGauge)); + listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => + OnInstrumentPublished(instrument, meterListener, meterScope); + listener.SetMeasurementEventCallback((m, f, tags, _) => + OnMeasurementReceived( + m, + f, + tags, + ref cpuUserTime, + ref cpuKernelTime, + ref cpuFromGauge, + ref cpuLimitFromGauge, + ref cpuRequestFromGauge, + ref memoryFromGauge, + ref memoryLimitFromGauge)); + listener.SetMeasurementEventCallback((instrument, value, tags, _) => + { + if (instrument.Name == ResourceUtilizationInstruments.ContainerMemoryUsage) + { + memoryUsageFromGauge = value; + } + }); listener.Start(); using var host = FakeHost.CreateBuilder() @@ -355,6 +383,7 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou Assert.Equal(1, roundedCpuUsedPercentage); Assert.Equal(50, utilization.MemoryUsedPercentage); + Assert.Equal(524288, memoryUsageFromGauge); Assert.Equal(0.5, cpuLimitFromGauge * 100); Assert.Equal(roundedCpuUsedPercentage, Math.Round(cpuRequestFromGauge * 100)); Assert.Equal(utilization.MemoryUsedPercentage, memoryLimitFromGauge * 100); @@ -392,10 +421,11 @@ public Task ResourceUtilizationTracker_And_Metrics_Report_Same_Values_With_Cgrou var memoryLimitFromGauge = 0.0d; object? meterScope = null; - listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) - => OnInstrumentPublished(instrument, meterListener, meterScope); - listener.SetMeasurementEventCallback((m, f, tags, _) - => OnMeasurementReceived(m, + listener.InstrumentPublished = (Instrument instrument, MeterListener meterListener) => + OnInstrumentPublished(instrument, meterListener, meterScope); + listener.SetMeasurementEventCallback((m, f, tags, _) => + OnMeasurementReceived( + m, f, tags, ref cpuUserTime, @@ -455,7 +485,8 @@ private static void OnInstrumentPublished(Instrument instrument, MeterListener m instrument.Name == ResourceUtilizationInstruments.ContainerCpuTime || instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization || instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization || - instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization) + instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization || + instrument.Name == ResourceUtilizationInstruments.ContainerMemoryUsage) { meterListener.EnableMeasurementEvents(instrument); } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index b34dfb1c258..c60ec5fa834 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -72,10 +72,18 @@ public void Provider_Registers_Instruments() } }); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + samples.Add((instrument, value)); + } + }); + listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(5, samples.Count); + Assert.Equal(6, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -86,6 +94,9 @@ public void Provider_Registers_Instruments() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(0.5, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryUsage); + Assert.Equal(524288, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryUsage).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization).value)); @@ -359,7 +370,7 @@ public void Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutu parserMock.Setup(p => p.GetMemoryUsageInBytes()).Returns(() => { callCount++; - if (callCount <= 2) + if (callCount <= 3) { throw new InvalidOperationException("Simulated unhandled exception"); } @@ -403,6 +414,6 @@ public void Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutu var metric = samples.SingleOrDefault(x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1234f / 2000f, metric.value, 0.01f); - parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(3)); + parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(4)); } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs index fc9113eacc9..ead512e015b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -379,6 +379,146 @@ public void SnapshotProvider_EmitsMemoryMetrics(string instrumentName, bool useZ Assert.Equal(0.3 * multiplier, metricCollector.LastMeasurement.Value); // Consuming 30% of the memory afterwards. } + [Fact] + public void SnapshotProvider_TestMemoryMetricsTogether() + { + _appMemoryUsage = 200UL; + ulong containerMemoryUsage = 400UL; + ulong updatedAppMemoryUsage = 600UL; + ulong updatedContainerMemoryUsage = 1200UL; + + _processInfoMock.SetupSequence(p => p.GetCurrentProcessMemoryUsage()) + .Returns(() => _appMemoryUsage) + .Returns(updatedAppMemoryUsage) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + _processInfoMock.SetupSequence(p => p.GetMemoryUsage()) + .Returns(() => containerMemoryUsage) + .Returns(updatedContainerMemoryUsage) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_TestMemoryMetricsTogether)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var processMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ProcessMemoryUtilization, fakeClock); + using var containerLimitMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, fakeClock); + using var containerUsageMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryUsage, fakeClock); + + var options = new ResourceMonitoringOptions + { + MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2) + }; + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + processMetricCollector.RecordObservableInstruments(); + containerLimitMetricCollector.RecordObservableInstruments(); + containerUsageMetricCollector.RecordObservableInstruments(); + + Assert.NotNull(processMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerLimitMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerUsageMetricCollector.LastMeasurement?.Value); + + Assert.Equal(10, processMetricCollector.LastMeasurement.Value); // Process is consuming 10% of memory limit initially. + Assert.Equal(20, containerLimitMetricCollector.LastMeasurement.Value); // The whole container is consuming 20% of the memory limit initially. + Assert.Equal((long)containerMemoryUsage, containerUsageMetricCollector.LastMeasurement.Value); // 400 bytes of memory usage initially. + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(options.MemoryConsumptionRefreshInterval - TimeSpan.FromMilliseconds(1)); + + processMetricCollector.RecordObservableInstruments(); + containerUsageMetricCollector.RecordObservableInstruments(); + containerLimitMetricCollector.RecordObservableInstruments(); + + // Still consuming 10% and 20% as values weren't updated yet - not enough time passed. + Assert.Equal(10, processMetricCollector.LastMeasurement.Value); + Assert.Equal(20, containerLimitMetricCollector.LastMeasurement.Value); + Assert.Equal((long)containerMemoryUsage, containerUsageMetricCollector.LastMeasurement.Value); + + // Step #2 - simulate 2 milliseconds passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + + processMetricCollector.RecordObservableInstruments(); + containerLimitMetricCollector.RecordObservableInstruments(); + containerUsageMetricCollector.RecordObservableInstruments(); + + // App is consuming 30%, and container is consuming 60% of the limit: + Assert.Equal(30, processMetricCollector.LastMeasurement.Value); + Assert.Equal(60, containerLimitMetricCollector.LastMeasurement.Value); + Assert.Equal((long)updatedContainerMemoryUsage, containerUsageMetricCollector.LastMeasurement.Value); + } + + [Fact] + public void SnapshotProvider_EmitsMemoryUsageMetric() + { + _appMemoryUsage = 200UL; + const ulong UpdatedAppMemoryUsage = 600UL; + const ulong UpdatedAppMemoryUsage2 = 300UL; + + _processInfoMock.SetupSequence(p => p.GetCurrentProcessMemoryUsage()) + .Returns(() => _appMemoryUsage) + .Returns(UpdatedAppMemoryUsage) + .Returns(UpdatedAppMemoryUsage2) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + _processInfoMock.SetupSequence(p => p.GetMemoryUsage()) + .Returns(() => _appMemoryUsage) + .Returns(UpdatedAppMemoryUsage) + .Returns(UpdatedAppMemoryUsage2) + .Throws(new InvalidOperationException("We shouldn't hit here...")); + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(SnapshotProvider_EmitsMemoryMetrics)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + using var metricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryUsage, fakeClock); + + var options = new ResourceMonitoringOptions + { + MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2), + }; + var snapshotProvider = new WindowsContainerSnapshotProvider( + _memoryInfoMock.Object, + _systemInfoMock.Object, + _processInfoMock.Object, + _logger, + meterFactoryMock.Object, + () => _jobHandleMock.Object, + fakeClock, + options); + + // Step #0 - state in the beginning: + metricCollector.RecordObservableInstruments(); + Assert.NotNull(metricCollector.LastMeasurement?.Value); + Assert.Equal(200, metricCollector.LastMeasurement.Value); // Consuming 200 bytes initially. + + // Step #1 - simulate 1 millisecond passing and collect metrics again: + fakeClock.Advance(options.MemoryConsumptionRefreshInterval - TimeSpan.FromMilliseconds(1)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(200, metricCollector.LastMeasurement.Value); // Still consuming 200 bytes as metric wasn't updated. + + // Step #2 - simulate 2 milliseconds passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(2)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(600, metricCollector.LastMeasurement.Value); // Consuming 600 bytes. + + // Step #3 - simulate 2 milliseconds passing and collect metrics again: + fakeClock.Advance(TimeSpan.FromMilliseconds(2)); + metricCollector.RecordObservableInstruments(); + Assert.Equal(300, metricCollector.LastMeasurement.Value); // Consuming 300 bytes. + } + [Fact] public Task SnapshotProvider_EmitsLogRecord() { From 633d8016f63d00caf3b86e9986efa754dbe677a1 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 14 Jul 2025 13:41:42 -0400 Subject: [PATCH 207/472] Add schema version to server.json in MCP template (#6606) * Add schema version to server.json * Fix test --- .../src/McpServer/McpServer-CSharp/.mcp/server.json | 1 + .../mcpserver.Basic.verified/mcpserver/.mcp/server.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json index d4b9d0edf5b..34c19714f79 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -1,4 +1,5 @@ { + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", "description": "", "name": "io.github./", "packages": [ diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json index ab997541e52..02908c09afb 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -1,4 +1,5 @@ { + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", "description": "", "name": "io.github./", "packages": [ From dfd77b919a03d122e71eb13f9547b3a7cca526af Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 14 Jul 2025 23:16:42 -0700 Subject: [PATCH 208/472] Update MCP server template readme to show both VS Code and Visual Studio notes (#6591) * Initial plan * Add VS IDE-specific README and update template configuration Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> * Update to single README with both VS Code and Visual Studio sections Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> * Fix Visual Studio MCP documentation URL Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> * Fix Visual Studio MCP JSON configuration format Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> * Restructure README to reduce repetition between VS Code and Visual Studio sections Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> * Eliminate repetitive JSON configuration in README by consolidating server definitions Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> * Revise the mcp server template README * Update MCP template README paths and sync snapshot with source template Co-authored-by: joelverhagen <94054+joelverhagen@users.noreply.github.com> * Update mcpserver project template baseline * Bump MEAI.Templates package to preview.3. * Add feedback survey to mcpserver project template README --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> Co-authored-by: Jeff Handley Co-authored-by: joelverhagen <94054+joelverhagen@users.noreply.github.com> --- .../Microsoft.Extensions.AI.Templates.csproj | 2 +- .../src/McpServer/McpServer-CSharp/README.md | 85 ++++++++++--------- .../mcpserver/README.md | 85 ++++++++++--------- 3 files changed, 91 insertions(+), 81 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 7784747028e..ab5ef554a3a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -7,7 +7,7 @@ dotnet-new;templates;ai preview - 2 + 3 AI 0 0 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md index 50091888ad8..cb11ac30eb5 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -1,9 +1,11 @@ # MCP Server -This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. +This README was created using the C# MCP server project template. It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. +Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey). + ## Checklist before publishing to NuGet.org - Test the MCP server locally using the steps below. @@ -14,67 +16,70 @@ See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). -## Using the MCP Server in VS Code +## Developing locally -Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. ```json { - "mcp": { - "servers": { - "McpServer-CSharp": { - "type": "stdio", - "command": "dnx", - "args": [ - "", - "--version", - "", - "--yes" - ] - } + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] } } } ``` -Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results. +## Testing the MCP Server + +Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org -## Developing locally in VS Code +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. -To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: ```json { "servers": { "McpServer-CSharp": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "run", - "--project", - "" + "", + "--version", + "", + "--yes" ] } } } ``` -Alternatively, you can configure your VS Code user settings to use your local project: +## More information -```json -{ - "mcp": { - "servers": { - "McpServer-CSharp": { - "type": "stdio", - "command": "dotnet", - "args": [ - "run", - "--project", - "" - ] - } - } - } -} -``` +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md index 5c00a3bf669..a0bf0fc082d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -1,9 +1,11 @@ # MCP Server -This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. +This README was created using the C# MCP server project template. It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. +Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey). + ## Checklist before publishing to NuGet.org - Test the MCP server locally using the steps below. @@ -14,67 +16,70 @@ See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). -## Using the MCP Server in VS Code +## Developing locally -Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. ```json { - "mcp": { - "servers": { - "mcpserver": { - "type": "stdio", - "command": "dnx", - "args": [ - "", - "--version", - "", - "--yes" - ] - } + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] } } } ``` -Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. +## Testing the MCP Server + +Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org -## Developing locally in VS Code +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. -To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: ```json { "servers": { "mcpserver": { "type": "stdio", - "command": "dotnet", + "command": "dnx", "args": [ - "run", - "--project", - "" + "", + "--version", + "", + "--yes" ] } } } ``` -Alternatively, you can configure your VS Code user settings to use your local project: +## More information -```json -{ - "mcp": { - "servers": { - "mcpserver": { - "type": "stdio", - "command": "dotnet", - "args": [ - "run", - "--project", - "" - ] - } - } - } -} -``` +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers) From 4b296e42b98705569b91178e0d39092a4931a20f Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:41:21 +0200 Subject: [PATCH 209/472] Fallback to previous DNS service discovery (#10140) * Fallback to previous DNS service discovery * Code review feedback --- .../FallbackDnsResolver.cs | 102 ++++++++++++++++++ ...oft.Extensions.ServiceDiscovery.Dns.csproj | 1 + .../Resolver/DnsResolver.cs | 2 +- .../Resolver/IDnsResolver.cs | 3 - ...DiscoveryDnsServiceCollectionExtensions.cs | 28 ++++- 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs new file mode 100644 index 00000000000..1cdcab2f05d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed class FallbackDnsResolver : IDnsResolver +{ + private readonly LookupClient _lookupClient; + private readonly IOptionsMonitor _options; + private readonly TimeProvider _timeProvider; + + public FallbackDnsResolver(LookupClient lookupClient, IOptionsMonitor options, TimeProvider timeProvider) + { + _lookupClient = lookupClient; + _options = options; + _timeProvider = timeProvider; + } + + private TimeSpan DefaultRefreshPeriod => _options.CurrentValue.DefaultRefreshPeriod; + + public async ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) + { + DateTime expiresAt = _timeProvider.GetUtcNow().DateTime.Add(DefaultRefreshPeriod); + var addresses = await System.Net.Dns.GetHostAddressesAsync(name, cancellationToken).ConfigureAwait(false); + + var results = new AddressResult[addresses.Length]; + + for (int i = 0; i < addresses.Length; i++) + { + results[i] = new AddressResult + { + Address = addresses[i], + ExpiresAt = expiresAt + }; + } + + return results; + } + + public async ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) + { + DateTime now = _timeProvider.GetUtcNow().DateTime; + var queryResult = await _lookupClient.QueryAsync(name, DnsClient.QueryType.SRV, cancellationToken: cancellationToken).ConfigureAwait(false); + if (queryResult.HasError) + { + throw CreateException(name, queryResult.ErrorMessage); + } + + var lookupMapping = new Dictionary>(); + foreach (var record in queryResult.Additionals.OfType()) + { + if (!lookupMapping.TryGetValue(record.DomainName, out var addresses)) + { + addresses = new List(); + lookupMapping[record.DomainName] = addresses; + } + + addresses.Add(new AddressResult + { + Address = record.Address, + ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive)) + }); + } + + var srvRecords = queryResult.Answers.OfType().ToList(); + + var results = new ServiceResult[srvRecords.Count]; + for (int i = 0; i < srvRecords.Count; i++) + { + var record = srvRecords[i]; + + results[i] = new ServiceResult + { + ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive)), + Priority = record.Priority, + Weight = record.Weight, + Port = record.Port, + Target = record.Target, + Addresses = lookupMapping.TryGetValue(record.Target, out var addresses) + ? addresses.ToArray() + : Array.Empty() + }; + } + + return results; + } + + private static InvalidOperationException CreateException(string dnsName, string errorMessage) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS SRV records were found for DNS name '{dnsName}': {errorMessage}.", + _ => $"No DNS SRV records were found for DNS name '{dnsName}'", + }; + return new InvalidOperationException(msg); + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 3aba9b3aaea..6d8bbb47842 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs index 5722356a1c3..bc290c6b907 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -143,7 +143,7 @@ public async ValueTask ResolveIPAddressesAsync(string name, Can return results; } - public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + internal ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs index e09168d9552..080fe3be8de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs @@ -1,13 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; - namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; internal interface IDnsResolver { - ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default); ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default); ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 7d05243f741..42f220445b1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -46,11 +46,37 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); + + if (!GetDnsClientFallbackFlag()) + { + services.TryAddSingleton(); + } + else + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; + + static bool GetDnsClientFallbackFlag() + { + if (AppContext.TryGetSwitch("Microsoft.Extensions.ServiceDiscovery.Dns.UseDnsClientFallback", out var value)) + { + return value; + } + + var envVar = Environment.GetEnvironmentVariable("MICROSOFT_EXTENSIONS_SERVICE_DISCOVERY_DNS_USE_DNSCLIENT_FALLBACK"); + if (envVar is not null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1"))) + { + return true; + } + + return false; + } } /// From 7874d8ffcda171371df549421c96af1371c0b746 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 15 Jul 2025 11:50:21 +0300 Subject: [PATCH 210/472] Fix schema generation for Nullable function parameters. (#6596) * Fix schema generation for Nullable function parameters. * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs * Incorporate fix from https://github.com/dotnet/runtime/issues/117493. * Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs * Extend fix to include AllowReadingFromString. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AIJsonUtilities.Schema.Create.cs | 60 +++++++++++++--- .../JsonSchemaExporter.ReflectionHelpers.cs | 2 - .../JsonSchemaExporter/JsonSchemaExporter.cs | 16 +++-- .../AssertExtensions.cs | 24 ++++--- .../Utilities/AIJsonUtilitiesTests.cs | 16 +++-- .../Functions/AIFunctionFactoryTest.cs | 68 +++++++++++++++++++ test/Shared/JsonSchemaExporter/TestData.cs | 10 ++- test/Shared/JsonSchemaExporter/TestTypes.cs | 47 ++++++------- 8 files changed, 184 insertions(+), 59 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index a9d3ac3e3ee..c77e7dffb5b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -289,24 +289,49 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js objSchema.InsertAtStart(TypePropertyName, "string"); } - // Include the type keyword in nullable enum types - if (Nullable.GetUnderlyingType(ctx.TypeInfo.Type)?.IsEnum is true && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName)) - { - objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" }); - } - // 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. // In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error. - if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema, out string? numericType)) + if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema, out string? numericType, out bool isNullable)) { // We don't want to emit any array for "type". In this case we know it contains "integer" or "number", // so reduce the type to that alone, assuming it's the most specific type. // This makes schemas for Int32 (etc) work with Ollama. JsonObject obj = ConvertSchemaToObject(ref schema); - obj[TypePropertyName] = numericType; + if (isNullable) + { + // If the type is nullable, we still need use a type array + obj[TypePropertyName] = new JsonArray { (JsonNode)numericType, (JsonNode)"null" }; + } + else + { + obj[TypePropertyName] = (JsonNode)numericType; + } + _ = obj.Remove(PatternPropertyName); } + + if (Nullable.GetUnderlyingType(ctx.TypeInfo.Type) is Type nullableElement) + { + // Account for bug https://github.com/dotnet/runtime/issues/117493 + // To be removed once System.Text.Json v10 becomes the lowest supported version. + // null not inserted in the type keyword for root-level Nullable types. + if (objSchema.TryGetPropertyValue(TypePropertyName, out JsonNode? typeKeyWord) && + typeKeyWord?.GetValueKind() is JsonValueKind.String) + { + string typeValue = typeKeyWord.GetValue()!; + if (typeValue is not "null") + { + objSchema[TypePropertyName] = new JsonArray { (JsonNode)typeValue, (JsonNode)"null" }; + } + } + + // Include the type keyword in nullable enum types + if (nullableElement.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName)) + { + objSchema.InsertAtStart(TypePropertyName, new JsonArray { (JsonNode)"string", (JsonNode)"null" }); + } + } } if (ctx.Path.IsEmpty && hasDefaultValue) @@ -601,11 +626,12 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali } } - private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateContext ctx, JsonObject schema, [NotNullWhen(true)] out string? numericType) + private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateContext ctx, JsonObject schema, [NotNullWhen(true)] out string? numericType, out bool isNullable) { numericType = null; + isNullable = false; - if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray { Count: 2 } typeArray) + if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray typeArray) { bool allowString = false; @@ -617,11 +643,23 @@ private static bool TypeIsIntegerWithStringNumberHandling(AIJsonSchemaCreateCont switch (type) { case "integer" or "number": + if (numericType is not null) + { + // Conflicting numeric type + return false; + } + numericType = type; break; case "string": allowString = true; break; + case "null": + isNullable = true; + break; + default: + // keyword is not valid in the context of numeric types. + return false; } } } @@ -665,7 +703,7 @@ private static JsonElement ParseJsonElement(ReadOnlySpan utf8Json) if (defaultValue is null || (defaultValue == DBNull.Value && parameterType != typeof(DBNull))) { - return parameterType.IsValueType + return parameterType.IsValueType && Nullable.GetUnderlyingType(parameterType) is null #if NET ? RuntimeHelpers.GetUninitializedObject(parameterType) #else diff --git a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.ReflectionHelpers.cs b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.ReflectionHelpers.cs index 481e5f75753..6d350dab026 100644 --- a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.ReflectionHelpers.cs +++ b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.ReflectionHelpers.cs @@ -31,8 +31,6 @@ private static class ReflectionHelpers public static bool IsBuiltInConverter(JsonConverter converter) => converter.GetType().Assembly == typeof(JsonConverter).Assembly; - public static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null; - public static Type GetElementType(JsonTypeInfo typeInfo) { Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary, "TypeInfo must be of collection type"); diff --git a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.cs b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.cs index 2d8ffc5497c..d651ce6a727 100644 --- a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.cs +++ b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.cs @@ -452,20 +452,24 @@ JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) bool IsNullableSchema(ref GenerationState state) { - // A schema is marked as nullable if either + // A schema is marked as nullable if either: // 1. We have a schema for a property where either the getter or setter are marked as nullable. - // 2. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable + // 2. We have a schema for a Nullable type. + // 3. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable. if (propertyInfo != null || parameterInfo != null) { return !isNonNullableType; } - else + + if (Nullable.GetUnderlyingType(typeInfo.Type) is not null) { - return ReflectionHelpers.CanBeNull(typeInfo.Type) && - !parentPolymorphicTypeIsNonNullable && - !state.ExporterOptions.TreatNullObliviousAsNonNullable; + return true; } + + return !typeInfo.Type.IsValueType && + !parentPolymorphicTypeIsNonNullable && + !state.ExporterOptions.TreatNullObliviousAsNonNullable; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs index 72985108c6e..6361fe7817e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs @@ -53,21 +53,29 @@ public static void EqualFunctionCallParameters( public static void EqualFunctionCallResults(object? expected, object? actual, JsonSerializerOptions? options = null) => AreJsonEquivalentValues(expected, actual, options); - private static void AreJsonEquivalentValues(object? expected, object? actual, JsonSerializerOptions? options, string? propertyName = null) + /// + /// Asserts that the two JSON values are equal. + /// + public static void EqualJsonValues(JsonElement expectedJson, JsonElement actualJson, string? propertyName = null) { - options ??= AIJsonUtilities.DefaultOptions; - JsonElement expectedElement = NormalizeToElement(expected, options); - JsonElement actualElement = NormalizeToElement(actual, options); if (!JsonNode.DeepEquals( - JsonSerializer.SerializeToNode(expectedElement, AIJsonUtilities.DefaultOptions), - JsonSerializer.SerializeToNode(actualElement, AIJsonUtilities.DefaultOptions))) + JsonSerializer.SerializeToNode(expectedJson, AIJsonUtilities.DefaultOptions), + JsonSerializer.SerializeToNode(actualJson, AIJsonUtilities.DefaultOptions))) { string message = propertyName is null - ? $"Function result does not match expected JSON.\r\nExpected: {expectedElement.GetRawText()}\r\nActual: {actualElement.GetRawText()}" - : $"Parameter '{propertyName}' does not match expected JSON.\r\nExpected: {expectedElement.GetRawText()}\r\nActual: {actualElement.GetRawText()}"; + ? $"JSON result does not match expected JSON.\r\nExpected: {expectedJson.GetRawText()}\r\nActual: {actualJson.GetRawText()}" + : $"Parameter '{propertyName}' does not match expected JSON.\r\nExpected: {expectedJson.GetRawText()}\r\nActual: {actualJson.GetRawText()}"; throw new XunitException(message); } + } + + private static void AreJsonEquivalentValues(object? expected, object? actual, JsonSerializerOptions? options, string? propertyName = null) + { + options ??= AIJsonUtilities.DefaultOptions; + JsonElement expectedElement = NormalizeToElement(expected, options); + JsonElement actualElement = NormalizeToElement(actual, options); + EqualJsonValues(expectedElement, actualElement, propertyName); static JsonElement NormalizeToElement(object? value, JsonSerializerOptions options) => value is JsonElement e ? e : JsonSerializer.SerializeToElement(value, options); 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 19b2fc8bb48..c2177486fea 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -354,13 +354,21 @@ public static void CreateFunctionJsonSchema_TreatsIntegralTypesAsInteger_EvenWit int i = 0; foreach (JsonProperty property in schemaParameters.EnumerateObject()) { - string numericType = Type.GetTypeCode(parameters[i].ParameterType) is TypeCode.Double or TypeCode.Single or TypeCode.Decimal - ? "number" - : "integer"; + bool isNullable = false; + Type type = parameters[i].ParameterType; + if (Nullable.GetUnderlyingType(type) is { } elementType) + { + type = elementType; + isNullable = true; + } + + string numericType = Type.GetTypeCode(type) is TypeCode.Double or TypeCode.Single or TypeCode.Decimal + ? "\"number\"" + : "\"integer\""; JsonElement expected = JsonDocument.Parse($$""" { - "type": "{{numericType}}" + "type": {{(isNullable ? $"[{numericType}, \"null\"]" : numericType)}} } """).RootElement; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 8c0c7d057a6..69787dc868b 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.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; @@ -854,6 +855,71 @@ public async Task AIFunctionFactory_DefaultDefaultParameter() Assert.Contains("00000000-0000-0000-0000-000000000000,0", result?.ToString()); } + [Fact] + public async Task AIFunctionFactory_NullableParameters() + { + Assert.NotEqual(new StructWithDefaultCtor().Value, default(StructWithDefaultCtor).Value); + + AIFunction f = AIFunctionFactory.Create( + (int? limit = null, DateTime? from = null) => Enumerable.Repeat(from ?? default, limit ?? 4).Select(d => d.Year).ToArray(), + serializerOptions: JsonContext.Default.Options); + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties": { + "limit": { + "type": ["integer", "null"], + "default": null + }, + "from": { + "type": ["string", "null"], + "format": "date-time", + "default": null + } + } + } + """).RootElement; + + AssertExtensions.EqualJsonValues(expectedSchema, f.JsonSchema); + + object? result = await f.InvokeAsync(); + Assert.Contains("[1,1,1,1]", result?.ToString()); + } + + [Fact] + public async Task AIFunctionFactory_NullableParameters_AllowReadingFromString() + { + JsonSerializerOptions options = new(JsonContext.Default.Options) { NumberHandling = JsonNumberHandling.AllowReadingFromString }; + Assert.NotEqual(new StructWithDefaultCtor().Value, default(StructWithDefaultCtor).Value); + + AIFunction f = AIFunctionFactory.Create( + (int? limit = null, DateTime? from = null) => Enumerable.Repeat(from ?? default, limit ?? 4).Select(d => d.Year).ToArray(), + serializerOptions: options); + + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties": { + "limit": { + "type": ["integer", "null"], + "default": null + }, + "from": { + "type": ["string", "null"], + "format": "date-time", + "default": null + } + } + } + """).RootElement; + + AssertExtensions.EqualJsonValues(expectedSchema, f.JsonSchema); + + object? result = await f.InvokeAsync(); + Assert.Contains("[1,1,1,1]", result?.ToString()); + } + [Fact] public void AIFunctionFactory_ReturnTypeWithDescriptionAttribute() { @@ -959,5 +1025,7 @@ private static AIFunctionFactoryOptions CreateKeyedServicesSupportOptions() => [JsonSerializable(typeof(Guid))] [JsonSerializable(typeof(StructWithDefaultCtor))] [JsonSerializable(typeof(B))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(DateTime?))] private partial class JsonContext : JsonSerializerContext; } diff --git a/test/Shared/JsonSchemaExporter/TestData.cs b/test/Shared/JsonSchemaExporter/TestData.cs index 26902bfe0db..7c7cc7fc9a7 100644 --- a/test/Shared/JsonSchemaExporter/TestData.cs +++ b/test/Shared/JsonSchemaExporter/TestData.cs @@ -13,7 +13,9 @@ internal sealed record TestData( T? Value, [StringSyntax(StringSyntaxAttribute.Json)] string ExpectedJsonSchema, IEnumerable? AdditionalValues = null, - object? ExporterOptions = null, +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL + System.Text.Json.Schema.JsonSchemaExporterOptions? ExporterOptions = null, +#endif JsonSerializerOptions? Options = null, bool WritesNumbersAsStrings = false) : ITestData @@ -22,7 +24,9 @@ internal sealed record TestData( public Type Type => typeof(T); object? ITestData.Value => Value; +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL object? ITestData.ExporterOptions => ExporterOptions; +#endif JsonNode ITestData.ExpectedJsonSchema { get; } = JsonNode.Parse(ExpectedJsonSchema, documentOptions: _schemaParseOptions) ?? throw new ArgumentNullException("schema must not be null"); @@ -32,7 +36,7 @@ IEnumerable ITestData.GetTestDataForAllValues() yield return this; if (default(T) is null && -#if NET9_0_OR_GREATER +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL ExporterOptions is System.Text.Json.Schema.JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable: false } && #endif Value is not null) @@ -58,7 +62,9 @@ public interface ITestData JsonNode ExpectedJsonSchema { get; } +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL object? ExporterOptions { get; } +#endif JsonSerializerOptions? Options { get; } diff --git a/test/Shared/JsonSchemaExporter/TestTypes.cs b/test/Shared/JsonSchemaExporter/TestTypes.cs index 7cfd0ce45be..794e58fa2b8 100644 --- a/test/Shared/JsonSchemaExporter/TestTypes.cs +++ b/test/Shared/JsonSchemaExporter/TestTypes.cs @@ -9,12 +9,9 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; -#if NET9_0_OR_GREATER -using System.Reflection; -#endif using System.Text.Json; using System.Text.Json.Nodes; -#if NET9_0_OR_GREATER +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL using System.Text.Json.Schema; #endif using System.Text.Json.Serialization; @@ -135,6 +132,21 @@ public static IEnumerable GetTestDataCore() } """); +#if !NET9_0 && TESTS_JSON_SCHEMA_EXPORTER_POLYFILL + // Regression test for https://github.com/dotnet/runtime/issues/117493 + yield return new TestData( + Value: 42, + AdditionalValues: [null], + ExpectedJsonSchema: """{"type":["integer","null"]}""", + ExporterOptions: new() { TreatNullObliviousAsNonNullable = true }); + + yield return new TestData( + Value: DateTimeOffset.MinValue, + AdditionalValues: [null], + ExpectedJsonSchema: """{"type":["string","null"],"format":"date-time"}""", + ExporterOptions: new() { TreatNullObliviousAsNonNullable = true }); +#endif + // User-defined POCOs yield return new TestData( Value: new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, @@ -152,7 +164,7 @@ public static IEnumerable GetTestDataCore() } """); -#if NET9_0_OR_GREATER +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL // Same as above but with nullable types set to non-nullable yield return new TestData( Value: new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, @@ -311,7 +323,7 @@ public static IEnumerable GetTestDataCore() } """); -#if NET9_0_OR_GREATER +#if TESTS_JSON_SCHEMA_EXPORTER_POLYFILL // Same as above but with non-nullable reference types by default. yield return new TestData( Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, @@ -761,7 +773,7 @@ of the type which points to the first occurrence. */ } """); -#if NET9_0_OR_GREATER +#if TEST yield return new TestData( Value: new("string", -1), ExpectedJsonSchema: """ @@ -1164,7 +1176,7 @@ public readonly struct StructDictionary(IEnumerable _dictionary.Count; public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); public IEnumerator> GetEnumerator() => _dictionary.GetEnumerator(); -#if NETCOREAPP +#if NET public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetValue(key, out value); #else public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); @@ -1249,6 +1261,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions [JsonSerializable(typeof(IntEnum?))] [JsonSerializable(typeof(StringEnum?))] [JsonSerializable(typeof(SimpleRecordStruct?))] + [JsonSerializable(typeof(DateTimeOffset?))] // User-defined POCOs [JsonSerializable(typeof(SimplePoco))] [JsonSerializable(typeof(SimpleRecord))] @@ -1299,22 +1312,4 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions [JsonSerializable(typeof(StructDictionary))] [JsonSerializable(typeof(XElement))] public partial class TestTypesContext : JsonSerializerContext; - -#if NET9_0_OR_GREATER - private static TAttribute? ResolveAttribute(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 - GetAttrs(ctx.PropertyInfo?.AttributeProvider) ?? - GetAttrs(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider) ?? - GetAttrs(ctx.TypeInfo.Type); - - static TAttribute? GetAttrs(ICustomAttributeProvider? provider) => - (TAttribute?)provider?.GetCustomAttributes(typeof(TAttribute), inherit: false).FirstOrDefault(); - } -#endif } From acf1d4520870c4d0ce2ea51b449212829484d8a8 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Tue, 15 Jul 2025 03:34:32 -0700 Subject: [PATCH 211/472] Update Azure Open AI package (#6609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change is required to address the following error when running eval integration tests - looks like the version of OpenAI package referenced in the product code was incompatible with the preview version of Azure Open AI package referenced in the test code. ``` System.TypeLoadException HResult=0x80131522 Message=Could not load type 'OpenAI.RealtimeConversation.RealtimeConversationClient' from assembly 'OpenAI, Version=2.2.0.0, Culture=neutral, PublicKeyToken=b4187f3e65366280'. Source=Azure.AI.OpenAI StackTrace:   at Azure.AI.OpenAI.AzureOpenAIClientOptions.GetRawServiceApiValueForClient(Object client)   at Azure.AI.OpenAI.Chat.AzureChatClient..ctor(ClientPipeline pipeline, String deploymentName, Uri endpoint, AzureOpenAIClientOptions options)   at Azure.AI.OpenAI.AzureOpenAIClient.GetChatClient(String deploymentName)   at Microsoft.Extensions.AI.Evaluation.Integration.Tests.Setup.CreateChatConfiguration() in Q:\src\extensions\test\Libraries\Microsoft.Extensions.AI.Evaluation.Integration.Tests\Setup.cs:line 19 ``` --- eng/packages/TestOnly.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index 3b511fa037f..9bf2edcca4a 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -2,7 +2,7 @@ - + From ed4aeac58f4646a2816987991d6abb9719f59de0 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 16 Jul 2025 13:34:02 -0400 Subject: [PATCH 212/472] Target .NET 8 for more stable runtime requirement (#6617) * Target .NET 8 for more stable runtime requirement * Address comment --- src/ProjectTemplates/GeneratedContent.targets | 2 +- .../src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in | 3 ++- .../mcpserver.Basic.verified/mcpserver/mcpserver.csproj | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index dfe93dbc5a8..c738ee0b547 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,7 +35,7 @@ 1.14.0 11.6.0 9.4.1-beta.291 - 10.0.0-preview.5.25277.114 + 10.0.0-preview.6.25358.103 9.3.0 1.53.0 1.53.0-preview diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in index d47952229d9..2eca37df228 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in @@ -1,7 +1,8 @@ - net10.0 + net8.0 + Major Exe enable enable diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index e959c64702f..468230d16e4 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -1,7 +1,8 @@  - net10.0 + net8.0 + Major Exe enable enable From 73b4861518efd50a6da10d8141b86c9467736ca5 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Wed, 16 Jul 2025 17:19:29 -0700 Subject: [PATCH 213/472] Add support for new Azure AI Foundry project type for Safety evals (#6621) Fixes #6592 --- .../ContentSafetyChatClient.cs | 27 ++- .../ContentSafetyService.UrlCacheKey.cs | 4 + .../ContentSafetyService.cs | 55 +++-- .../ContentSafetyServiceConfiguration.cs | 220 ++++++++++++++---- .../SafetyEvaluatorTests.cs | 89 ++++++- .../Settings.cs | 5 + .../appsettings.json | 3 +- 7 files changed, 325 insertions(+), 78 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs index 347975cb695..a8c1ba889d2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs @@ -35,16 +35,27 @@ public ContentSafetyChatClient( ChatClientMetadata? originalMetadata = _originalChatClient?.GetService(); - string providerName = - $"{Moniker} (" + - $"Subscription: {contentSafetyServiceConfiguration.SubscriptionId}, " + - $"Resource Group: {contentSafetyServiceConfiguration.ResourceGroupName}, " + - $"Project: {contentSafetyServiceConfiguration.ProjectName})"; + string providerName; + Uri? providerUri = originalMetadata?.ProviderUri; + + if (contentSafetyServiceConfiguration.IsHubBasedProject) + { + providerName = + $"{Moniker} (" + + $"Subscription: {contentSafetyServiceConfiguration.SubscriptionId}, " + + $"Resource Group: {contentSafetyServiceConfiguration.ResourceGroupName}, " + + $"Project: {contentSafetyServiceConfiguration.ProjectName})"; + } + else + { + providerName = $"{Moniker} (Endpoint: {contentSafetyServiceConfiguration.Endpoint})"; + providerUri = contentSafetyServiceConfiguration.Endpoint; + } if (originalMetadata?.ProviderName is string originalProviderName && !string.IsNullOrWhiteSpace(originalProviderName)) { - providerName = $"{originalProviderName}; {providerName}"; + providerName = $"{providerName}; {originalProviderName}"; } string modelId = Moniker; @@ -52,10 +63,10 @@ public ContentSafetyChatClient( if (originalMetadata?.DefaultModelId is string originalModelId && !string.IsNullOrWhiteSpace(originalModelId)) { - modelId = $"{originalModelId}; {modelId}"; + modelId = $"{modelId}; {originalModelId}"; } - _metadata = new ChatClientMetadata(providerName, originalMetadata?.ProviderUri, modelId); + _metadata = new ChatClientMetadata(providerName, providerUri, modelId); } public async Task GetResponseAsync( diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs index 41be29e9ed3..c8969a001c4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs @@ -26,11 +26,14 @@ public bool Equals(UrlCacheKey? other) } else { +#pragma warning disable S1067 // Expressions should not be too complex return other.Configuration.SubscriptionId == Configuration.SubscriptionId && other.Configuration.ResourceGroupName == Configuration.ResourceGroupName && other.Configuration.ProjectName == Configuration.ProjectName && + other.Configuration.Endpoint == Configuration.Endpoint && other.AnnotationTask == AnnotationTask; +#pragma warning restore S1067 } } @@ -42,6 +45,7 @@ public override int GetHashCode() => Configuration.SubscriptionId, Configuration.ResourceGroupName, Configuration.ProjectName, + Configuration.Endpoint, AnnotationTask); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs index 6028a82544c..ee9bdf2c926 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs @@ -22,6 +22,9 @@ namespace Microsoft.Extensions.AI.Evaluation.Safety; internal sealed partial class ContentSafetyService(ContentSafetyServiceConfiguration serviceConfiguration) { + private const string APIVersionForServiceDiscoveryInHubBasedProjects = "?api-version=2023-08-01-preview"; + private const string APIVersionForNonHubBasedProjects = "?api-version=2025-05-15-preview"; + private static HttpClient? _sharedHttpClient; private static HttpClient SharedHttpClient { @@ -168,20 +171,27 @@ private async ValueTask GetServiceUrlAsync( return _serviceUrl; } - string discoveryUrl = - await GetServiceDiscoveryUrlAsync(evaluatorName, cancellationToken).ConfigureAwait(false); - - serviceUrl = - $"{discoveryUrl}/raisvc/v1.0" + - $"/subscriptions/{serviceConfiguration.SubscriptionId}" + - $"/resourceGroups/{serviceConfiguration.ResourceGroupName}" + - $"/providers/Microsoft.MachineLearningServices/workspaces/{serviceConfiguration.ProjectName}"; + if (serviceConfiguration.IsHubBasedProject) + { + string discoveryUrl = + await GetServiceDiscoveryUrlAsync(evaluatorName, cancellationToken).ConfigureAwait(false); + + serviceUrl = + $"{discoveryUrl}/raisvc/v1.0" + + $"/subscriptions/{serviceConfiguration.SubscriptionId}" + + $"/resourceGroups/{serviceConfiguration.ResourceGroupName}" + + $"/providers/Microsoft.MachineLearningServices/workspaces/{serviceConfiguration.ProjectName}"; + } + else + { + serviceUrl = $"{serviceConfiguration.Endpoint.AbsoluteUri}/evaluations"; + } await EnsureServiceAvailabilityAsync( - serviceUrl, - capability: annotationTask, - evaluatorName, - cancellationToken).ConfigureAwait(false); + serviceUrl, + capability: annotationTask, + evaluatorName, + cancellationToken).ConfigureAwait(false); _ = _serviceUrlCache.TryAdd(key, serviceUrl); _serviceUrl = serviceUrl; @@ -196,7 +206,7 @@ private async ValueTask GetServiceDiscoveryUrlAsync( $"https://management.azure.com/subscriptions/{serviceConfiguration.SubscriptionId}" + $"/resourceGroups/{serviceConfiguration.ResourceGroupName}" + $"/providers/Microsoft.MachineLearningServices/workspaces/{serviceConfiguration.ProjectName}" + - $"?api-version=2023-08-01-preview"; + $"{APIVersionForServiceDiscoveryInHubBasedProjects}"; HttpResponseMessage response = await GetResponseAsync( @@ -244,7 +254,10 @@ private async ValueTask EnsureServiceAvailabilityAsync( string evaluatorName, CancellationToken cancellationToken) { - string serviceAvailabilityUrl = $"{serviceUrl}/checkannotation"; + string serviceAvailabilityUrl = + serviceConfiguration.IsHubBasedProject + ? $"{serviceUrl}/checkannotation" + : $"{serviceUrl}/checkannotation{APIVersionForNonHubBasedProjects}"; HttpResponseMessage response = await GetResponseAsync( @@ -297,7 +310,10 @@ private async ValueTask SubmitAnnotationRequestAsync( string evaluatorName, CancellationToken cancellationToken) { - string annotationUrl = $"{serviceUrl}/submitannotation"; + string annotationUrl = + serviceConfiguration.IsHubBasedProject + ? $"{serviceUrl}/submitannotation" + : $"{serviceUrl}/submitannotation{APIVersionForNonHubBasedProjects}"; HttpResponseMessage response = await GetResponseAsync( @@ -426,10 +442,13 @@ private async ValueTask AddHeadersAsync( httpRequestMessage.Headers.Add("User-Agent", userAgent); + TokenRequestContext context = + serviceConfiguration.IsHubBasedProject + ? new TokenRequestContext(scopes: ["https://management.azure.com/.default"]) + : new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]); + AccessToken token = - await serviceConfiguration.Credential.GetTokenAsync( - new TokenRequestContext(scopes: ["https://management.azure.com/.default"]), - cancellationToken).ConfigureAwait(false); + await serviceConfiguration.Credential.GetTokenAsync(context, cancellationToken).ConfigureAwait(false); httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs index ec721fa59c7..06d4a045ced 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs @@ -6,64 +6,62 @@ // We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary // constructor syntax. +using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Azure.Core; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Evaluation.Safety; /// -/// Specifies configuration parameters such as the Azure AI project that should be used, and the credentials that -/// should be used, when a communicates with the Azure AI Foundry Evaluation +/// Specifies configuration parameters such as the Azure AI Foundry project that should be used, and the credentials +/// that should be used, when a communicates with the Azure AI Foundry Evaluation /// service to perform evaluations. /// -/// -/// The Azure that should be used when authenticating requests. -/// -/// -/// The ID of the Azure subscription that contains the project identified by . -/// -/// -/// The name of the Azure resource group that contains the project identified by . -/// -/// -/// The name of the Azure AI project. -/// -/// -/// The that should be used when communicating with the Azure AI Foundry Evaluation service. -/// While the parameter is optional, it is recommended to supply an that is configured with -/// robust resilience and retry policies. -/// -/// -/// The timeout (in seconds) after which a should stop retrying failed attempts -/// to communicate with the Azure AI Foundry Evaluation service when performing evaluations. -/// -public sealed class ContentSafetyServiceConfiguration( - TokenCredential credential, - string subscriptionId, - string resourceGroupName, - string projectName, - HttpClient? httpClient = null, - int timeoutInSecondsForRetries = 300) // 5 minutes +/// +/// +/// Note that Azure AI Foundry supports two kinds of projects - Hub-based projects and non-Hub-based projects (also +/// known simply as Foundry projects). See https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-projects. +/// +/// +/// Hub-based projects are configured by specifying the , the +/// , and the for the project. Non-Hub-based projects, on the +/// other hand, are configured by specifying only the for the project. Use the appropriate +/// constructor overload to initialize based on the kind of project you +/// are working with. +/// +/// +public sealed class ContentSafetyServiceConfiguration { + private const int DefaultTimeoutInSecondsForRetries = 300; // 5 minutes + /// /// Gets the Azure that should be used when authenticating requests. /// - public TokenCredential Credential { get; } = credential; + public TokenCredential Credential { get; } + + /// + /// Gets the ID of the Azure subscription that contains the project identified by if the + /// project is a Hub-based project. + /// + public string? SubscriptionId { get; } /// - /// Gets the ID of the Azure subscription that contains the project identified by . + /// Gets the name of the Azure resource group that contains the project identified by if + /// the project is a Hub-based project. /// - public string SubscriptionId { get; } = subscriptionId; + public string? ResourceGroupName { get; } /// - /// Gets the name of the Azure resource group that contains the project identified by . + /// Gets the name of the Azure AI Foundry project if the project is a Hub-based project. /// - public string ResourceGroupName { get; } = resourceGroupName; + public string? ProjectName { get; } /// - /// Gets the name of the Azure AI project. + /// Gets the endpoint for the Azure AI Foundry project if the project is a non-Hub-based project. /// - public string ProjectName { get; } = projectName; + public Uri? Endpoint { get; } /// /// Gets the that should be used when communicating with the Azure AI Foundry Evaluation @@ -73,11 +71,155 @@ public sealed class ContentSafetyServiceConfiguration( /// While supplying an is optional, it is recommended to supply one that is configured /// with robust resilience and retry policies. /// - public HttpClient? HttpClient { get; } = httpClient; + public HttpClient? HttpClient { get; } /// /// Gets the timeout (in seconds) after which a should stop retrying failed /// attempts to communicate with the Azure AI Foundry Evaluation service when performing evaluations. /// - public int TimeoutInSecondsForRetries { get; } = timeoutInSecondsForRetries; + public int TimeoutInSecondsForRetries { get; } + + [MemberNotNullWhen(true, nameof(SubscriptionId), nameof(ResourceGroupName), nameof(ProjectName))] + [MemberNotNullWhen(false, nameof(Endpoint))] + internal bool IsHubBasedProject => + !string.IsNullOrWhiteSpace(SubscriptionId) && + !string.IsNullOrWhiteSpace(ResourceGroupName) && + !string.IsNullOrWhiteSpace(ProjectName) && + Endpoint is null; + + /// + /// Initializes a new instance of the class for a Hub-based Azure + /// AI Foundry project with the specified . + /// + /// + /// The Azure that should be used when authenticating requests. + /// + /// + /// The ID of the Azure subscription that contains the Hub-based AI Foundry project identified by + /// . + /// + /// + /// The name of the Azure resource group that contains the Hub-based AI Foundry project identified by + /// . + /// + /// + /// The name of the Hub-based Azure AI Foundry project. + /// + /// + /// The that should be used when communicating with the Azure AI Foundry Evaluation + /// service. While the parameter is optional, it is recommended to supply an that is + /// configured with robust resilience and retry policies. + /// + /// + /// The timeout (in seconds) after which a should stop retrying failed + /// attempts to communicate with the Azure AI Foundry Evaluation service when performing evaluations. + /// + /// + /// + /// Note that Azure AI Foundry supports two kinds of projects - Hub-based projects and non-Hub-based projects (also + /// known simply as Foundry projects). See + /// https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-projects. + /// + /// + /// Use this constructor overload if you are working with a Hub-based project. + /// + /// + public ContentSafetyServiceConfiguration( + TokenCredential credential, + string subscriptionId, + string resourceGroupName, + string projectName, + HttpClient? httpClient = null, + int timeoutInSecondsForRetries = DefaultTimeoutInSecondsForRetries) + { + Credential = Throw.IfNull(credential); + SubscriptionId = Throw.IfNullOrWhitespace(subscriptionId); + ResourceGroupName = Throw.IfNullOrWhitespace(resourceGroupName); + ProjectName = Throw.IfNullOrWhitespace(projectName); + HttpClient = httpClient; + TimeoutInSecondsForRetries = timeoutInSecondsForRetries; + } + + /// + /// Initializes a new instance of the class for a non-Hub-based + /// Azure AI Foundry project with the specified . + /// + /// + /// The Azure that should be used when authenticating requests. + /// + /// + /// The endpoint for the non-Hub-based Azure AI Foundry project. + /// + /// + /// The that should be used when communicating with the Azure AI Foundry Evaluation + /// service. While the parameter is optional, it is recommended to supply an that is + /// configured with robust resilience and retry policies. + /// + /// + /// The timeout (in seconds) after which a should stop retrying failed + /// attempts to communicate with the Azure AI Foundry Evaluation service when performing evaluations. + /// + /// + /// + /// Note that Azure AI Foundry supports two kinds of projects - Hub-based projects and non-Hub-based projects (also + /// known simply as Foundry projects). See + /// https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-projects. + /// + /// + /// Use this constructor overload if you are working with a non-Hub-based project. + /// + /// + public ContentSafetyServiceConfiguration( + TokenCredential credential, + Uri endpoint, + HttpClient? httpClient = null, + int timeoutInSecondsForRetries = DefaultTimeoutInSecondsForRetries) + { + Credential = Throw.IfNull(credential); + Endpoint = Throw.IfNull(endpoint); + HttpClient = httpClient; + TimeoutInSecondsForRetries = timeoutInSecondsForRetries; + } + + /// + /// Initializes a new instance of the class for a non-Hub-based + /// Azure AI Foundry project with the specified . + /// + /// + /// The Azure that should be used when authenticating requests. + /// + /// + /// The endpoint URL for the non-Hub-based Azure AI Foundry project. + /// + /// + /// The that should be used when communicating with the Azure AI Foundry Evaluation + /// service. While the parameter is optional, it is recommended to supply an that is + /// configured with robust resilience and retry policies. + /// + /// + /// The timeout (in seconds) after which a should stop retrying failed + /// attempts to communicate with the Azure AI Foundry Evaluation service when performing evaluations. + /// + /// + /// + /// Note that Azure AI Foundry supports two kinds of projects - Hub-based projects and non-Hub-based projects (also + /// known simply as Foundry projects). See + /// https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-projects. + /// + /// + /// Use this constructor overload if you are working with a non-Hub-based project. + /// + /// + public ContentSafetyServiceConfiguration( + TokenCredential credential, + string endpointUrl, + HttpClient? httpClient = null, + int timeoutInSecondsForRetries = DefaultTimeoutInSecondsForRetries) + : this( + credential, + endpoint: new Uri(Throw.IfNullOrWhitespace(endpointUrl)), + httpClient, + timeoutInSecondsForRetries) + { + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs index 630adbffd8e..c0f955b8416 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs @@ -24,6 +24,7 @@ public class SafetyEvaluatorTests private static readonly ReportingConfiguration? _imageContentSafetyReportingConfiguration; private static readonly ReportingConfiguration? _codeVulnerabilityReportingConfiguration; private static readonly ReportingConfiguration? _mixedQualityAndSafetyReportingConfiguration; + private static readonly ReportingConfiguration? _hubBasedContentSafetyReportingConfiguration; static SafetyEvaluatorTests() { @@ -37,14 +38,11 @@ static SafetyEvaluatorTests() }; ChatConfiguration llmChatConfiguration = Setup.CreateChatConfiguration(); - ChatClientMetadata? clientMetadata = llmChatConfiguration.ChatClient.GetService(); string version = $"Product Version: {Constants.Version}"; string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; string projectName = $"Project: Integration Tests"; string testClass = $"Test Class: {nameof(SafetyEvaluatorTests)}"; - string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; - string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; string temperature = $"Temperature: {_chatOptions.Temperature}"; string usesContext = $"Feature: Context"; @@ -52,13 +50,17 @@ static SafetyEvaluatorTests() var contentSafetyServiceConfiguration = new ContentSafetyServiceConfiguration( credential, - subscriptionId: Settings.Current.AzureSubscriptionId, - resourceGroupName: Settings.Current.AzureResourceGroupName, - projectName: Settings.Current.AzureAIProjectName); + endpointUrl: Settings.Current.AzureAIProjectEndpoint); ChatConfiguration contentSafetyChatConfiguration = contentSafetyServiceConfiguration.ToChatConfiguration(llmChatConfiguration); + ChatClientMetadata? clientMetadata = + contentSafetyChatConfiguration.ChatClient.GetService(); + + string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; + string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + IEvaluator hateAndUnfairnessEvaluator = new HateAndUnfairnessEvaluator(); IEvaluator selfHarmEvaluator = new SelfHarmEvaluator(); IEvaluator sexualEvaluator = new SexualEvaluator(); @@ -117,20 +119,65 @@ static SafetyEvaluatorTests() chatConfiguration: contentSafetyChatConfiguration, executionName: Constants.Version, tags: [version, date, projectName, testClass, provider, model, temperature]); + + var hubBasedContentSafetyServiceConfiguration = + new ContentSafetyServiceConfiguration( + credential, + subscriptionId: Settings.Current.AzureSubscriptionId, + resourceGroupName: Settings.Current.AzureResourceGroupName, + projectName: Settings.Current.AzureAIProjectName); + + ChatConfiguration hubBasedContentSafetyChatConfiguration = + hubBasedContentSafetyServiceConfiguration.ToChatConfiguration(llmChatConfiguration); + + clientMetadata = hubBasedContentSafetyChatConfiguration.ChatClient.GetService(); + + provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; + model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + + _hubBasedContentSafetyReportingConfiguration = + DiskBasedReportingConfiguration.Create( + storageRootPath: Settings.Current.StorageRootPath, + evaluators: [ + selfHarmEvaluator, + sexualEvaluator, + protectedMaterialEvaluator, + groundednessProEvaluator, + ungroundedAttributesEvaluator, + indirectAttackEvaluator], + chatConfiguration: hubBasedContentSafetyChatConfiguration, + executionName: Constants.Version, + tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); } } [ConditionalFact] - public async Task EvaluateConversationWithSingleTurn() + public async Task EvaluateConversationWithSingleTurn_HubBasedProject() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _hubBasedContentSafetyReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithSingleTurn_HubBasedProject)}"); + + await EvaluateConversationWithSingleTurn(scenarioRun); + } + + [ConditionalFact] + public async Task EvaluateConversationWithSingleTurn_NonHubBasedProject() { SkipIfNotConfigured(); await using ScenarioRun scenarioRun = await _contentSafetyReportingConfiguration.CreateScenarioRunAsync( - scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithSingleTurn)}"); + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithSingleTurn_NonHubBasedProject)}"); - IChatClient chatClient = scenarioRun.ChatConfiguration!.ChatClient; + await EvaluateConversationWithSingleTurn(scenarioRun); + } + private static async Task EvaluateConversationWithSingleTurn(ScenarioRun scenarioRun) + { + IChatClient chatClient = scenarioRun.ChatConfiguration!.ChatClient; var messages = new List(); string systemPrompt = @@ -183,16 +230,32 @@ The distance varies due to the elliptical orbits of both planets. } [ConditionalFact] - public async Task EvaluateConversationWithMultipleTurns() + public async Task EvaluateConversationWithMultipleTurns_HubBasedProject() + { + SkipIfNotConfigured(); + + await using ScenarioRun scenarioRun = + await _hubBasedContentSafetyReportingConfiguration.CreateScenarioRunAsync( + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithMultipleTurns_HubBasedProject)}"); + + await EvaluateConversationWithMultipleTurns(scenarioRun); + } + + [ConditionalFact] + public async Task EvaluateConversationWithMultipleTurns_NonHubBasedProject() { SkipIfNotConfigured(); await using ScenarioRun scenarioRun = await _contentSafetyReportingConfiguration.CreateScenarioRunAsync( - scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithMultipleTurns)}"); + scenarioName: $"Microsoft.Extensions.AI.Evaluation.Integration.Tests.{nameof(SafetyEvaluatorTests)}.{nameof(EvaluateConversationWithMultipleTurns_NonHubBasedProject)}"); - IChatClient chatClient = scenarioRun.ChatConfiguration!.ChatClient; + await EvaluateConversationWithMultipleTurns(scenarioRun); + } + private static async Task EvaluateConversationWithMultipleTurns(ScenarioRun scenarioRun) + { + IChatClient chatClient = scenarioRun.ChatConfiguration!.ChatClient; var messages = new List(); string systemPrompt = @@ -553,6 +616,7 @@ await _mixedQualityAndSafetyReportingConfiguration.CreateScenarioRunAsync( [MemberNotNull(nameof(_imageContentSafetyReportingConfiguration))] [MemberNotNull(nameof(_codeVulnerabilityReportingConfiguration))] [MemberNotNull(nameof(_mixedQualityAndSafetyReportingConfiguration))] + [MemberNotNull(nameof(_hubBasedContentSafetyReportingConfiguration))] private static void SkipIfNotConfigured() { if (!Settings.Current.Configured) @@ -565,5 +629,6 @@ private static void SkipIfNotConfigured() Assert.NotNull(_codeVulnerabilityReportingConfiguration); Assert.NotNull(_imageContentSafetyReportingConfiguration); Assert.NotNull(_mixedQualityAndSafetyReportingConfiguration); + Assert.NotNull(_hubBasedContentSafetyReportingConfiguration); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs index 22e027e73b2..25ee9d544cb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs @@ -16,6 +16,7 @@ public class Settings public string AzureSubscriptionId { get; } public string AzureResourceGroupName { get; } public string AzureAIProjectName { get; } + public string AzureAIProjectEndpoint { get; } public Settings(IConfiguration config) { @@ -49,6 +50,10 @@ public Settings(IConfiguration config) AzureAIProjectName = config.GetValue("AzureAIProjectName") ?? throw new ArgumentNullException(nameof(AzureAIProjectName)); + + AzureAIProjectEndpoint = + config.GetValue("AzureAIProjectEndpoint") + ?? throw new ArgumentNullException(nameof(AzureAIProjectEndpoint)); #pragma warning restore CA2208 } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/appsettings.json index 63b5ed0d33c..24e079d421d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/appsettings.json @@ -6,5 +6,6 @@ "StorageRootPath": "[storage-path]", "AzureSubscriptionId": "[subscription]", "AzureResourceGroupName": "[resource-group]", - "AzureAIProjectName": "[project]" + "AzureAIProjectName": "[project]", + "AzureAIProjectEndpoint": "https://[resource].services.ai.azure.com/api/projects/[project]" } From e717e4af0066af7c3e81b20b66b856ab8f26cb5f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:21:09 +0100 Subject: [PATCH 214/472] Bump (#6624) --- eng/packages/General.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index aa9771bfb4a..a6c1a69f4d8 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -4,7 +4,7 @@ - + From 59b07255e4f6a6224fbdabb004aea7ebf57408fc Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 18 Jul 2025 06:36:08 -0400 Subject: [PATCH 215/472] Add DataContent.Name property (#6616) * Add DataContent.Name property Enables a name (e.g. file name) to optionally be associated with a DataContent. DataContent is often used to represent named entities, such as when uploading files. --- .../Contents/AIAnnotation.cs | 0 .../Contents/AIAnnotationExtensions.cs | 0 .../Contents/AIAnnotationKind.cs | 0 .../Contents/AIAnnotationReference.cs | 0 .../Contents/DataContent.cs | 7 ++++++ .../Microsoft.Extensions.AI.Abstractions.json | 4 ++++ .../OpenAIChatClient.cs | 2 +- .../OpenAIResponseChatClient.cs | 2 +- .../Contents/AIAnnotationReferenceTests.cs | 0 .../Contents/AIAnnotationTests.cs | 0 .../Contents/AIContentAnnotationTests.cs | 0 .../Contents/DataContentTests.cs | 24 +++++++++++++++---- .../ChatClientIntegrationTests.cs | 2 +- 13 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationReferenceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentAnnotationTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs index 5bbde1e1444..57f7fac1962 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs @@ -183,6 +183,13 @@ public string Uri [JsonIgnore] public string MediaType { get; } + /// Gets or sets an optional name associated with the data. + /// + /// A service might use this name as part of citations or to help infer the type of data + /// being represented based on a file extension. + /// + public string? Name { get; set; } + /// Gets the data represented by this instance. /// /// If the instance was constructed from a , this property returns that data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index b47765269af..4d190fccda8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1349,6 +1349,10 @@ "Member": "System.ReadOnlyMemory Microsoft.Extensions.AI.DataContent.Data { get; }", "Stage": "Stable" }, + { + "Member": "string? Microsoft.Extensions.AI.DataContent.Name { get; set; }", + "Stage": "Stable" + }, { "Member": "string Microsoft.Extensions.AI.DataContent.MediaType { get; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 394fccad1b6..52dfc3dc8af 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -264,7 +264,7 @@ private static List ToOpenAIChatContent(IList break; case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): - return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"); + return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf"); case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: return rawContentPart; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index c4a1261844c..57ed46721b0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -639,7 +639,7 @@ private static List ToOpenAIResponsesContent(IList([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"), + JsonSerializer.Serialize( + new DataContent(new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"), + TestJsonSerializerContext.Default.Options)); + + Assert.Equal( + """{"uri":"data:application/octet-stream;base64,AQIDBA==","name":"test.bin"}""", + JsonSerializer.Serialize( + new DataContent(new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream") { Name = "test.bin" }, TestJsonSerializerContext.Default.Options)); } @@ -260,4 +267,13 @@ public void NonBase64Data_Normalized() Assert.Equal("aGVsbG8gd29ybGQ=", content.Base64Data.ToString()); Assert.Equal("hello world", Encoding.ASCII.GetString(content.Data.ToArray())); } + + [Fact] + public void FileName_Roundtrips() + { + DataContent content = new(new byte[] { 1, 2, 3 }, "application/octet-stream"); + Assert.Null(content.Name); + content.Name = "test.bin"; + Assert.Equal("test.bin", content.Name); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index ffa94f64531..c9f0e09abf3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -202,7 +202,7 @@ public virtual async Task MultiModal_DescribePdf() new(ChatRole.User, [ new TextContent("What text does this document contain?"), - new DataContent(ImageDataUri.GetPdfDataUri(), "application/pdf"), + new DataContent(ImageDataUri.GetPdfDataUri(), "application/pdf") { Name = "sample.pdf" }, ]) ], new() { ModelId = GetModel_MultiModal_DescribeImage() }); From 29065e75bd28380f265cc8f3c5e81926ac97fbf8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 18 Jul 2025 08:57:31 -0400 Subject: [PATCH 216/472] Fix handling of multiple responses messages (#6627) --- .../OpenAIResponseChatClient.cs | 7 +- .../OpenAIResponseClientTests.cs | 116 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index 57ed46721b0..b5ba36b5e1a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -111,10 +111,15 @@ public async Task GetResponseAsync( switch (outputItem) { case MessageResponseItem messageItem: + if (message.MessageId is not null && message.MessageId != messageItem.Id) + { + message = new ChatMessage(); + response.Messages.Add(message); + } + message.MessageId = messageItem.Id; message.RawRepresentation = messageItem; message.Role = ToChatRole(messageItem.Role); - (message.AdditionalProperties ??= []).Add(nameof(messageItem.Id), messageItem.Id); ((List)message.Contents).AddRange(ToAIContents(messageItem.Content)); break; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index b98eb89197f..72df6002ca2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -479,6 +479,122 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio Assert.Equal("Hello! How can I assist you today?", response.Text); } + [Fact] + public async Task MultipleOutputItems_NonStreaming() + { + const string Input = """ + { + "temperature":0.5, + "model":"gpt-4o-mini", + "input": [{ + "type":"message", + "role":"user", + "content":[{"type":"input_text","text":"hello"}] + }], + "max_output_tokens":20 + } + """; + + const string Output = """ + { + "id": "resp_67d327649b288191aeb46a824e49dc40058a5e08c46a181d", + "object": "response", + "created_at": 1741891428, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 20, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "type": "message", + "id": "msg_67d32764fcdc8191bcf2e444d4088804058a5e08c46a181d", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello!", + "annotations": [] + } + ] + }, + { + "type": "message", + "id": "msg_67d32764fcdc8191bcf2e444d4088804058a5e08c46a182e", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": " How can I assist you today?", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "generate_summary": null + }, + "store": true, + "temperature": 0.5, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "usage": { + "input_tokens": 26, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 10, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 36 + }, + "user": null, + "metadata": {} + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + var response = await client.GetResponseAsync("hello", new() + { + MaxOutputTokens = 20, + Temperature = 0.5f, + }); + Assert.NotNull(response); + + Assert.Equal("resp_67d327649b288191aeb46a824e49dc40058a5e08c46a181d", response.ResponseId); + Assert.Equal("resp_67d327649b288191aeb46a824e49dc40058a5e08c46a181d", response.ConversationId); + Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_741_891_428), response.CreatedAt); + Assert.Null(response.FinishReason); + + Assert.Equal(2, response.Messages.Count); + Assert.Equal(ChatRole.Assistant, response.Messages[0].Role); + Assert.Equal("Hello!", response.Messages[0].Text); + Assert.Equal(ChatRole.Assistant, response.Messages[1].Role); + Assert.Equal(" How can I assist you today?", response.Messages[1].Text); + + Assert.NotNull(response.Usage); + Assert.Equal(26, response.Usage.InputTokenCount); + Assert.Equal(10, response.Usage.OutputTokenCount); + Assert.Equal(36, response.Usage.TotalTokenCount); + } + /// Converts an Extensions function to an OpenAI response chat tool. private static ResponseTool ToOpenAIResponseChatTool(AIFunction aiFunction) { From b8012f4eba94dde545ea8c0bf5a15034a55cbc9b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 19 Jul 2025 03:52:14 -0400 Subject: [PATCH 217/472] Expose additional chat model conversion helpers (#6629) - Exposes the conversion routines for converting OpenAI output responses - Moves the extensions into the namespace most relevant to the performed operation --- ...crosoftExtensionsAIAssistantsExtensions.cs | 19 ++++++ .../MicrosoftExtensionsAIChatExtensions.cs | 33 ++++++++++ ...MicrosoftExtensionsAIRealtimeExtensions.cs | 19 ++++++ ...icrosoftExtensionsAIResponsesExtensions.cs | 33 ++++++++++ ...lient.cs => OpenAIAssistantsChatClient.cs} | 15 +++-- .../OpenAIChatClient.cs | 18 +++-- .../OpenAIClientExtensions.cs | 65 +++++-------------- .../OpenAIEmbeddingGenerator.cs | 1 - ...Client.cs => OpenAIResponsesChatClient.cs} | 14 ++-- .../OpenAIAssistantChatClientTests.cs | 3 +- .../OpenAIChatClientTests.cs | 4 +- .../OpenAIConversionTests.cs | 33 ++++++++++ 12 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs rename src/Libraries/Microsoft.Extensions.AI.OpenAI/{OpenAIAssistantChatClient.cs => OpenAIAssistantsChatClient.cs} (96%) rename src/Libraries/Microsoft.Extensions.AI.OpenAI/{OpenAIResponseChatClient.cs => OpenAIResponsesChatClient.cs} (98%) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs new file mode 100644 index 00000000000..900883c6d43 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs @@ -0,0 +1,19 @@ +// 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.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace OpenAI.Assistants; + +/// Provides extension methods for working with content associated with OpenAI.Assistants. +public static class MicrosoftExtensionsAIAssistantsExtensions +{ + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => + OpenAIAssistantsChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs new file mode 100644 index 00000000000..c7eb3b2e03e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace OpenAI.Chat; + +/// Provides extension methods for working with content associated with OpenAI.Chat. +public static class MicrosoftExtensionsAIChatExtensions +{ + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ChatTool AsOpenAIChatTool(this AIFunction function) => + OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function)); + + /// Creates a sequence of OpenAI instances from the specified input messages. + /// The input messages to convert. + /// A sequence of OpenAI chat messages. + public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages) => + OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null); + + /// Creates a Microsoft.Extensions.AI from a . + /// The to convert to a . + /// The options employed in the creation of the response. + /// A converted . + public static ChatResponse AsChatResponse(this ChatCompletion chatCompletion, ChatCompletionOptions? options = null) => + OpenAIChatClient.FromOpenAIChatCompletion(Throw.IfNull(chatCompletion), options); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs new file mode 100644 index 00000000000..ad180e96b1e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs @@ -0,0 +1,19 @@ +// 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.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace OpenAI.Realtime; + +/// Provides extension methods for working with content associated with OpenAI.Realtime. +public static class MicrosoftExtensionsAIRealtimeExtensions +{ + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => + OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs new file mode 100644 index 00000000000..e54b3092c5a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace OpenAI.Responses; + +/// Provides extension methods for working with content associated with OpenAI.Responses. +public static class MicrosoftExtensionsAIResponsesExtensions +{ + /// Creates an OpenAI from an . + /// The function to convert. + /// An OpenAI representing . + /// is . + public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => + OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function)); + + /// Creates a sequence of OpenAI instances from the specified input messages. + /// The input messages to convert. + /// A sequence of OpenAI response items. + public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages) => + OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages)); + + /// Creates a Microsoft.Extensions.AI from an . + /// The to convert to a . + /// The options employed in the creation of the response. + /// A converted . + public static ChatResponse AsChatResponse(this OpenAIResponse response, ResponseCreationOptions? options = null) => + OpenAIResponsesChatClient.FromOpenAIResponse(Throw.IfNull(response), options); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs similarity index 96% rename from src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs rename to src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index c1d59e30a68..46bc39ee278 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -26,8 +26,8 @@ namespace Microsoft.Extensions.AI; -/// Represents an for an Azure.AI.Agents.Persistent . -internal sealed class OpenAIAssistantChatClient : IChatClient +/// Represents an for an OpenAI . +internal sealed class OpenAIAssistantsChatClient : IChatClient { /// The underlying . private readonly AssistantClient _client; @@ -44,8 +44,8 @@ internal sealed class OpenAIAssistantChatClient : IChatClient /// List of tools associated with the assistant. private IReadOnlyList? _assistantTools; - /// Initializes a new instance of the class for the specified . - public OpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) + /// Initializes a new instance of the class for the specified . + public OpenAIAssistantsChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) { _client = Throw.IfNull(assistantClient); _assistantId = Throw.IfNullOrWhitespace(assistantId); @@ -62,6 +62,13 @@ public OpenAIAssistantChatClient(AssistantClient assistantClient, string assista _metadata = new("openai", providerUrl); } + /// Initializes a new instance of the class for the specified . + public OpenAIAssistantsChatClient(AssistantClient assistantClient, Assistant assistant, string? defaultThreadId) + : this(assistantClient, Throw.IfNull(assistant).Id, defaultThreadId) + { + _assistantTools = assistant.Tools; + } + /// public object? GetService(Type serviceType, object? serviceKey = null) => serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 52dfc3dc8af..9fc2f3cbeef 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -17,6 +17,7 @@ #pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; @@ -76,7 +77,7 @@ public async Task GetResponseAsync( // Make the call to OpenAI. var response = await _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken).ConfigureAwait(false); - return FromOpenAIChatCompletion(response.Value, options, openAIOptions); + return FromOpenAIChatCompletion(response.Value, openAIOptions); } /// @@ -430,7 +431,7 @@ private static string GetOutputAudioMimeType(ChatCompletionOptions? options) => "mp3" or _ => "audio/mpeg", }; - private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions) + internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatCompletionOptions? chatCompletionOptions) { _ = Throw.IfNull(openAICompletion); @@ -461,17 +462,14 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple } // Also manufacture function calling content items from any tool calls in the response. - if (options?.Tools is { Count: > 0 }) + foreach (ChatToolCall toolCall in openAICompletion.ToolCalls) { - foreach (ChatToolCall toolCall in openAICompletion.ToolCalls) + if (!string.IsNullOrWhiteSpace(toolCall.FunctionName)) { - if (!string.IsNullOrWhiteSpace(toolCall.FunctionName)) - { - var callContent = ParseCallContentFromBinaryData(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName); - callContent.RawRepresentation = toolCall; + var callContent = ParseCallContentFromBinaryData(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName); + callContent.RawRepresentation = toolCall; - returnMessage.Contents.Add(callContent); - } + returnMessage.Contents.Add(callContent); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 9f42fa88773..889998cc933 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -8,20 +8,15 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Assistants; using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; -using OpenAI.Realtime; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable SA1515 // Single-line comment should be preceded by blank line -#pragma warning disable CA1305 // Specify IFormatProvider -#pragma warning disable S1135 // Track uses of "TODO" tags namespace Microsoft.Extensions.AI; @@ -120,7 +115,7 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => /// An that can be used to converse via the . /// is . public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => - new OpenAIResponseChatClient(responseClient); + new OpenAIResponsesChatClient(responseClient); /// Gets an for use with this . /// The instance to be accessed as an . @@ -135,7 +130,21 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient /// is . /// is empty or composed entirely of whitespace. public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) => - new OpenAIAssistantChatClient(assistantClient, assistantId, threadId); + new OpenAIAssistantsChatClient(assistantClient, assistantId, threadId); + + /// Gets an for use with this . + /// The instance to be accessed as an . + /// The with which to interact. + /// + /// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to + /// or via the + /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request. + /// + /// An instance configured to interact with the specified agent and thread. + /// is . + /// is . + public static IChatClient AsIChatClient(this AssistantClient assistantClient, Assistant assistant, string? threadId = null) => + new OpenAIAssistantsChatClient(assistantClient, assistant, threadId); /// Gets an for use with this . /// The client. @@ -153,48 +162,6 @@ public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioCl public static IEmbeddingGenerator> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) => new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions); - /// Creates an OpenAI from an . - /// The function to convert. - /// An OpenAI representing . - /// is . - public static ChatTool AsOpenAIChatTool(this AIFunction function) => - OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function)); - - /// Creates an OpenAI from an . - /// The function to convert. - /// An OpenAI representing . - /// is . - public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => - OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); - - /// Creates an OpenAI from an . - /// The function to convert. - /// An OpenAI representing . - /// is . - public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => - OpenAIResponseChatClient.ToResponseTool(Throw.IfNull(function)); - - /// Creates an OpenAI from an . - /// The function to convert. - /// An OpenAI representing . - /// is . - public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => - OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); - - /// Creates a sequence of OpenAI instances from the specified input messages. - /// The input messages to convert. - /// A sequence of OpenAI chat messages. - public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages) => - OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null); - - /// Creates a sequence of OpenAI instances from the specified input messages. - /// The input messages to convert. - /// A sequence of OpenAI response items. - public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages) => - OpenAIResponseChatClient.ToOpenAIResponseItems(Throw.IfNull(messages)); - - // TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict. - /// Gets whether the properties specify that strict schema handling is desired. internal static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index 004f6274265..a6c4af831ba 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -using OpenAI; using OpenAI.Embeddings; #pragma warning disable S1067 // Expressions should not be too complex diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs similarity index 98% rename from src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs rename to src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index b5ba36b5e1a..60d0e10a159 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -23,7 +23,7 @@ namespace Microsoft.Extensions.AI; /// Represents an for an . -internal sealed class OpenAIResponseChatClient : IChatClient +internal sealed class OpenAIResponsesChatClient : IChatClient { /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -31,10 +31,10 @@ internal sealed class OpenAIResponseChatClient : IChatClient /// The underlying . private readonly OpenAIResponseClient _responseClient; - /// Initializes a new instance of the class for the specified . + /// Initializes a new instance of the class for the specified . /// The underlying client. /// is . - public OpenAIResponseChatClient(OpenAIResponseClient responseClient) + public OpenAIResponsesChatClient(OpenAIResponseClient responseClient) { _ = Throw.IfNull(responseClient); @@ -78,10 +78,16 @@ public async Task GetResponseAsync( // Make the call to the OpenAIResponseClient. var openAIResponse = (await _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + // Convert the response to a ChatResponse. + return FromOpenAIResponse(openAIResponse, openAIOptions); + } + + internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, ResponseCreationOptions? openAIOptions) + { // Convert and return the results. ChatResponse response = new() { - ConversationId = openAIOptions.StoredOutputEnabled is false ? null : openAIResponse.Id, + ConversationId = openAIOptions?.StoredOutputEnabled is false ? null : openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), Messages = [new(ChatRole.Assistant, [])], diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs index 3b084b5ec8a..7779e4cf18d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs @@ -19,7 +19,8 @@ public class OpenAIAssistantChatClientTests public void AsIChatClient_InvalidArgs_Throws() { Assert.Throws("assistantClient", () => ((AssistantClient)null!).AsIChatClient("assistantId")); - Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!)); + Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient((string)null!)); + Assert.Throws("assistant", () => new AssistantClient("ignored").AsIChatClient((Assistant)null!)); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index d06d8f520be..640dab54f8e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -398,7 +398,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; openAIOptions.StopSequences.Add("hello"); - openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool)); + openAIOptions.Tools.Add(tool.AsOpenAIChatTool()); return openAIOptions; }, ModelId = null, @@ -475,7 +475,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat() }; openAIOptions.StopSequences.Add("hello"); - openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool)); + openAIOptions.Tools.Add(tool.AsOpenAIChatTool()); return openAIOptions; }, ModelId = null, // has no effect, you cannot change the model of an OpenAI's ChatClient. diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 951554eda75..73f3ae65548 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -183,4 +183,37 @@ public void AsOpenAIResponseItems_ProducesExpectedOutput() Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m5.Role); Assert.Equal("The answer is 42.", Assert.Single(m5.Content).Text); } + + [Fact] + public void AsChatResponse_ConvertsOpenAIChatCompletion() + { + Assert.Throws("chatCompletion", () => ((ChatCompletion)null!).AsChatResponse()); + + ChatCompletion cc = OpenAIChatModelFactory.ChatCompletion( + "id", OpenAI.Chat.ChatFinishReason.Length, null, null, + [ChatToolCall.CreateFunctionToolCall("id", "functionName", BinaryData.FromString("test"))], + ChatMessageRole.User, null, null, null, new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + "model123", null, OpenAIChatModelFactory.ChatTokenUsage(2, 1, 3)); + cc.Content.Add(ChatMessageContentPart.CreateTextPart("Hello, world!")); + cc.Content.Add(ChatMessageContentPart.CreateImagePart(new Uri("http://example.com/image.png"))); + + ChatResponse response = cc.AsChatResponse(); + + Assert.Equal("id", response.ResponseId); + Assert.Equal(ChatFinishReason.Length, response.FinishReason); + Assert.Equal("model123", response.ModelId); + Assert.Equal(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), response.CreatedAt); + Assert.NotNull(response.Usage); + Assert.Equal(1, response.Usage.InputTokenCount); + Assert.Equal(2, response.Usage.OutputTokenCount); + Assert.Equal(3, response.Usage.TotalTokenCount); + + ChatMessage message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.User, message.Role); + + Assert.Equal(3, message.Contents.Count); + Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0]).Text); + Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1]).Uri.ToString()); + Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); + } } From 0b17d02db4f4ce6d5638182bffe90f3a091d68f7 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:31:48 +0000 Subject: [PATCH 218/472] Update dependencies from https://github.com/dotnet/arcade build 20250716.1 (#6633) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- eng/common/tools.ps1 | 2 +- global.json | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index cb6ead9fa88..324779fef3a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - 13b20849f8294593bf150a801cab639397e6c29d + 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f - + https://github.com/dotnet/arcade - 13b20849f8294593bf150a801cab639397e6c29d + 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f - + https://github.com/dotnet/arcade - 13b20849f8294593bf150a801cab639397e6c29d + 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f diff --git a/eng/Versions.props b/eng/Versions.props index 1eb4594d5af..d849e7ffed6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -84,7 +84,7 @@ 9.0.7 - 9.0.0-beta.25325.4 + 9.0.0-beta.25366.1 diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 22b49e09d09..9b3ad8840fd 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -416,7 +416,7 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # Locate Visual Studio installation or download x-copy msbuild. $vsInfo = LocateVisualStudio $vsRequirements - if ($vsInfo -ne $null) { + if ($vsInfo -ne $null -and $env:ForceUseXCopyMSBuild -eq $null) { # Ensure vsInstallDir has a trailing slash $vsInstallDir = Join-Path $vsInfo.installationPath "\" $vsMajorVersion = $vsInfo.installationVersion.Split('.')[0] diff --git a/global.json b/global.json index 70681bc9744..945f04c1f57 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25325.4", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25325.4" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25366.1", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25366.1" } } From 006a5696a88e4c8761017949dc1ea0a809a4feff Mon Sep 17 00:00:00 2001 From: Iliar Turdushev Date: Tue, 22 Jul 2025 12:48:48 +0200 Subject: [PATCH 219/472] Fixes #6548 (#6618) Adds an attempt to retrieve the request message from the resilience context in addition to retrieving the request from the response --- .../Polly/HttpRetryStrategyOptionsExtensions.cs | 8 +++++--- .../HttpRetryStrategyOptionsExtensionsTests.cs | 16 +++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptionsExtensions.cs index 85168988c7b..becd53e1240 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptionsExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptionsExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using Polly; +using Polly.Retry; namespace Microsoft.Extensions.Http.Resilience; @@ -52,9 +53,7 @@ public static void DisableFor(this HttpRetryStrategyOptions options, params Http { var result = await shouldHandle(args).ConfigureAwait(args.Context.ContinueOnCapturedContext); - if (result && - args.Outcome.Result is HttpResponseMessage response && - response.RequestMessage is HttpRequestMessage request) + if (result && GetRequestMessage(args) is HttpRequestMessage request) { return !methods.Contains(request.Method); } @@ -62,5 +61,8 @@ args.Outcome.Result is HttpResponseMessage response && return result; }; } + + private static HttpRequestMessage? GetRequestMessage(RetryPredicateArguments args) => + args.Outcome.Result?.RequestMessage ?? args.Context.GetRequestMessage(); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsExtensionsTests.cs index 4d43c020d1f..996731b08bf 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsExtensionsTests.cs @@ -65,12 +65,16 @@ public async Task DisableFor_RespectsOriginalShouldHandlePredicate() } [Fact] - public async Task DisableFor_ResponseMessageIsNull_DoesNotDisableRetries() + public async Task DisableFor_ResponseMessageIsNull_RetrievesRequestMessageFromContext() { var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.True() }; options.DisableFor(HttpMethod.Post); - Assert.True(await options.ShouldHandle(CreatePredicateArguments(null))); + using var request = new HttpRequestMessage { Method = HttpMethod.Post }; + var context = ResilienceContextPool.Shared.Get(); + context.SetRequestMessage(request); + + Assert.False(await options.ShouldHandle(CreatePredicateArguments(null, context))); } [Fact] @@ -80,8 +84,10 @@ public async Task DisableFor_RequestMessageIsNull_DoesNotDisableRetries() options.DisableFor(HttpMethod.Post); using var response = new HttpResponseMessage { RequestMessage = null }; + var context = ResilienceContextPool.Shared.Get(); + context.SetRequestMessage(null); - Assert.True(await options.ShouldHandle(CreatePredicateArguments(response))); + Assert.True(await options.ShouldHandle(CreatePredicateArguments(response, context))); } [Theory] @@ -105,10 +111,10 @@ public async Task DisableForUnsafeHttpMethods_PositiveScenario(string httpMethod Assert.Equal(shouldHandle, await options.ShouldHandle(CreatePredicateArguments(response))); } - private static RetryPredicateArguments CreatePredicateArguments(HttpResponseMessage? response) + private static RetryPredicateArguments CreatePredicateArguments(HttpResponseMessage? response, ResilienceContext? context = null) { return new RetryPredicateArguments( - ResilienceContextPool.Shared.Get(), + context ?? ResilienceContextPool.Shared.Get(), Outcome.FromResult(response), attemptNumber: 1); } From 5d8efb684ac5a56965f6fb72037cbd1db51ad74e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 19:50:45 +0000 Subject: [PATCH 220/472] Bump Package validation baseline version to 9.7.0 (#6650) * Initial plan * Bump Package validation baseline version to 9.7.0 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- eng/MSBuild/Packaging.targets | 2 +- eng/Versions.props | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/MSBuild/Packaging.targets b/eng/MSBuild/Packaging.targets index fdde9aa70d1..e5cd9bdf4c2 100644 --- a/eng/MSBuild/Packaging.targets +++ b/eng/MSBuild/Packaging.targets @@ -37,7 +37,7 @@ true - $(ApiCompatBaselineVersion) + 9.7.0 diff --git a/eng/Versions.props b/eng/Versions.props index d849e7ffed6..67d58f467f3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -6,7 +6,6 @@ preview 1 $(MajorVersion).$(MinorVersion).$(PatchVersion) - 9.6.0 $(MajorVersion).$(MinorVersion).0.0 true - 9.7.0 + 9.7.0 @@ -93,7 +93,7 @@ !@(_PackageBuildFile->AnyHaveMetadataValue('PackagePathWithoutFilename', '$(_NETStandardCompatErrorPlaceholderFilePackagePath)'))" /> - + @@ -102,4 +102,15 @@ + + + + <_PackageVersionInfo Include="$(MSBuildProjectFullPath)"> + $(PackageVersion) + $(PackageId) + + + + diff --git a/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj b/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj index cead17cde9e..e8df485098b 100644 --- a/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj +++ b/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj @@ -18,13 +18,42 @@ IsImplicitlyDefined="true" /> + + + + + + + + <_ResolvedPackageVersionVariableReference Include="@(_ResolvedPackageVersionInfo)"> + TemplatePackageVersion_$([System.String]::Copy('%(PackageId)').Replace('.', '')) + + + + + + $(GeneratedContentProperties); + + @(_ResolvedPackageVersionVariableReference->'%(VersionVariableName)=%(PackageVersion)') + + + + + DependsOnTargets="ComputeGeneratedContentProperties;_GetPackageVersionVariables"> diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index c738ee0b547..706424bdc25 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -13,20 +13,25 @@ <_McpServerContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\McpServer\ - + - - $(Version) - $(Version) - $(Version) - + Specifies packages defined in this repo that get referenced in generated template content. + For each item specified below, a property will be generated whose name matches the format: + "TemplatePackageVersion_{PackageName}" + where {PackageName} is the package ID with '.' characters removed. + The value of each property will be the computed package version. + --> + + + + + + - + + 9.3.0 9.3.0-preview.1.25265.20 @@ -47,8 +52,6 @@ - <_TemplateUsingJustBuiltPackages Condition="'$(TemplatePackageVersion_MicrosoftExtensionsAI)' == '$(Version)' OR '$(TemplatePackageVersion_MicrosoftExtensionsAI_Preview)' == '$(Version)'">true - $(GeneratedContentProperties); @@ -57,9 +60,6 @@ ArtifactsShippingPackagesDir=$(ArtifactsShippingPackagesDir); - TemplatePackageVersion_MicrosoftExtensionsAI=$(TemplatePackageVersion_MicrosoftExtensionsAI); - TemplatePackageVersion_MicrosoftExtensionsAI_Preview=$(TemplatePackageVersion_MicrosoftExtensionsAI_Preview); - TemplatePackageVersion_MicrosoftExtensionsHttpResilience=$(TemplatePackageVersion_MicrosoftExtensionsHttpResilience); TemplatePackageVersion_Aspire=$(TemplatePackageVersion_Aspire); TemplatePackageVersion_Aspire_Preview=$(TemplatePackageVersion_Aspire_Preview); TemplatePackageVersion_AzureAIOpenAI=$(TemplatePackageVersion_AzureAIOpenAI); @@ -79,7 +79,6 @@ LocalChatTemplateVariant=$(_LocalChatTemplateVariant); - UsingJustBuiltPackages=$(_TemplateUsingJustBuiltPackages); @@ -108,18 +107,9 @@ - - - <_GeneratedContentEnablingJustBuiltPackages + - - - diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 46d7382d6f1..64d585fd7a6 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -14,19 +14,19 @@ - + - + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in index 670604290b9..08d54995389 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/Directory.Build.targets.in @@ -4,13 +4,8 @@ It will not get included in the built project template. --> - - <_UsingJustBuiltPackages>${UsingJustBuiltPackages} - - Date: Thu, 24 Jul 2025 16:43:48 -0400 Subject: [PATCH 222/472] Remove Microsoft.Extensions.AI.Ollama (#6655) --- .../JsonContext.cs | 24 - .../Microsoft.Extensions.AI.Ollama.csproj | 44 -- .../Microsoft.Extensions.AI.Ollama.json | 0 .../OllamaChatClient.cs | 505 ------------------ .../OllamaChatRequest.cs | 17 - .../OllamaChatRequestMessage.cs | 13 - .../OllamaChatResponse.cs | 20 - .../OllamaChatResponseMessage.cs | 11 - .../OllamaEmbeddingGenerator.cs | 165 ------ .../OllamaEmbeddingRequest.cs | 13 - .../OllamaEmbeddingResponse.cs | 21 - .../OllamaFunctionCallContent.cs | 13 - .../OllamaFunctionResultContent.cs | 12 - .../OllamaFunctionTool.cs | 11 - .../OllamaFunctionToolCall.cs | 12 - .../OllamaFunctionToolParameter.cs | 13 - .../OllamaFunctionToolParameters.cs | 14 - .../OllamaRequestOptions.cs | 41 -- .../OllamaTool.cs | 10 - .../OllamaToolCall.cs | 9 - .../OllamaUtilities.cs | 72 --- .../Microsoft.Extensions.AI.Ollama/README.md | 11 - ...icrosoft.Extensions.AI.Ollama.Tests.csproj | 22 - .../OllamaChatClientIntegrationTests.cs | 115 ---- .../OllamaChatClientTests.cs | 487 ----------------- ...llamaEmbeddingGeneratorIntegrationTests.cs | 32 -- .../OllamaEmbeddingGeneratorTests.cs | 102 ---- .../TestJsonSerializerContext.cs | 12 - .../IntegrationTestHelpers.cs | 0 ...ns.AI.OllamaSharp.Integration.Tests.csproj | 2 +- .../OllamaSharpChatClientIntegrationTests.cs | 103 +++- ...SharpEmbeddingGeneratorIntegrationTests.cs | 23 +- 32 files changed, 125 insertions(+), 1824 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/JsonContext.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.json delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequestMessage.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponse.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponseMessage.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingRequest.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingResponse.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionCallContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionResultContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionTool.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolCall.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameter.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameters.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaRequestOptions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaTool.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaToolCall.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaUtilities.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Ollama/README.md delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorIntegrationTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/TestJsonSerializerContext.cs rename test/Libraries/{Microsoft.Extensions.AI.Ollama.Tests => Microsoft.Extensions.AI.OllamaSharp.Integration.Tests}/IntegrationTestHelpers.cs (100%) diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/JsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/JsonContext.cs deleted file mode 100644 index 6de0144c7cf..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/JsonContext.cs +++ /dev/null @@ -1,24 +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.Serialization; - -namespace Microsoft.Extensions.AI; - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] -[JsonSerializable(typeof(OllamaChatRequest))] -[JsonSerializable(typeof(OllamaChatRequestMessage))] -[JsonSerializable(typeof(OllamaChatResponse))] -[JsonSerializable(typeof(OllamaChatResponseMessage))] -[JsonSerializable(typeof(OllamaFunctionCallContent))] -[JsonSerializable(typeof(OllamaFunctionResultContent))] -[JsonSerializable(typeof(OllamaFunctionTool))] -[JsonSerializable(typeof(OllamaFunctionToolCall))] -[JsonSerializable(typeof(OllamaFunctionToolParameter))] -[JsonSerializable(typeof(OllamaFunctionToolParameters))] -[JsonSerializable(typeof(OllamaRequestOptions))] -[JsonSerializable(typeof(OllamaTool))] -[JsonSerializable(typeof(OllamaToolCall))] -[JsonSerializable(typeof(OllamaEmbeddingRequest))] -[JsonSerializable(typeof(OllamaEmbeddingResponse))] -internal sealed partial class JsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj deleted file mode 100644 index 96734131c83..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - Microsoft.Extensions.AI - Implementation of generative AI abstractions for Ollama. This package is deprecated, and the OllamaSharp package is recommended. - AI - - - - preview - false - 78 - 0 - - - - $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA2227;SA1316;S1121;EA0002 - true - true - - - - true - true - true - true - true - - - - - - - - - - - - - - - - diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.json b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs deleted file mode 100644 index 42f75af495e..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ /dev/null @@ -1,505 +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.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Json; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) -#pragma warning disable SA1204 // Static elements should appear before instance elements -#pragma warning disable S3358 // Ternary operators should not be nested - -namespace Microsoft.Extensions.AI; - -/// Represents an for Ollama. -public sealed class OllamaChatClient : IChatClient -{ - private static readonly JsonElement _schemalessJsonResponseFormatValue = JsonDocument.Parse("\"json\"").RootElement; - - private static readonly AIJsonSchemaTransformCache _schemaTransformCache = new(new() - { - ConvertBooleanSchemas = true, - }); - - /// Metadata about the client. - private readonly ChatClientMetadata _metadata; - - /// The api/chat endpoint URI. - private readonly Uri _apiChatEndpoint; - - /// The to use for sending requests. - private readonly HttpClient _httpClient; - - /// The use for any serialization activities related to tool call arguments and results. - private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions; - - /// Initializes a new instance of the class. - /// The endpoint URI where Ollama is hosted. - /// - /// The ID of the model to use. This ID can also be overridden per request via . - /// Either this parameter or must provide a valid model ID. - /// - /// An instance to use for HTTP operations. - public OllamaChatClient(string endpoint, string? modelId = null, HttpClient? httpClient = null) - : this(new Uri(Throw.IfNull(endpoint)), modelId, httpClient) - { - } - - /// Initializes a new instance of the class. - /// The endpoint URI where Ollama is hosted. - /// - /// The ID of the model to use. This ID can also be overridden per request via . - /// Either this parameter or must provide a valid model ID. - /// - /// An instance to use for HTTP operations. - /// is . - /// is empty or composed entirely of whitespace. - public OllamaChatClient(Uri endpoint, string? modelId = null, HttpClient? httpClient = null) - { - _ = Throw.IfNull(endpoint); - if (modelId is not null) - { - _ = Throw.IfNullOrWhitespace(modelId); - } - - _apiChatEndpoint = new Uri(endpoint, "api/chat"); - _httpClient = httpClient ?? OllamaUtilities.SharedClient; - - _metadata = new ChatClientMetadata("ollama", endpoint, modelId); - } - - /// Gets or sets to use for any serialization activities related to tool call arguments and results. - public JsonSerializerOptions ToolCallJsonSerializerOptions - { - get => _toolCallJsonSerializerOptions; - set => _toolCallJsonSerializerOptions = Throw.IfNull(value); - } - - /// - public async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(messages); - - using var httpResponse = await _httpClient.PostAsJsonAsync( - _apiChatEndpoint, - ToOllamaChatRequest(messages, options, stream: false), - JsonContext.Default.OllamaChatRequest, - cancellationToken).ConfigureAwait(false); - - if (!httpResponse.IsSuccessStatusCode) - { - await OllamaUtilities.ThrowUnsuccessfulOllamaResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); - } - - var response = (await httpResponse.Content.ReadFromJsonAsync( - JsonContext.Default.OllamaChatResponse, - cancellationToken).ConfigureAwait(false))!; - - if (!string.IsNullOrEmpty(response.Error)) - { - throw new InvalidOperationException($"Ollama error: {response.Error}"); - } - - var responseId = Guid.NewGuid().ToString("N"); - - return new(FromOllamaMessage(response.Message!, responseId)) - { - CreatedAt = DateTimeOffset.TryParse(response.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset createdAt) ? createdAt : null, - FinishReason = ToFinishReason(response), - ModelId = response.Model ?? options?.ModelId ?? _metadata.DefaultModelId, - ResponseId = responseId, - Usage = ParseOllamaChatResponseUsage(response), - }; - } - - /// - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(messages); - - using HttpRequestMessage request = new(HttpMethod.Post, _apiChatEndpoint) - { - Content = JsonContent.Create(ToOllamaChatRequest(messages, options, stream: true), JsonContext.Default.OllamaChatRequest) - }; - using var httpResponse = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - if (!httpResponse.IsSuccessStatusCode) - { - await OllamaUtilities.ThrowUnsuccessfulOllamaResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); - } - - // Ollama doesn't set a response ID on streamed chunks, so we need to generate one. - var responseId = Guid.NewGuid().ToString("N"); - - using var httpResponseStream = await httpResponse.Content -#if NET - .ReadAsStreamAsync(cancellationToken) -#else - .ReadAsStreamAsync() -#endif - .ConfigureAwait(false); - - using var streamReader = new StreamReader(httpResponseStream); -#if NET - while ((await streamReader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is { } line) -#else - while ((await streamReader.ReadLineAsync().ConfigureAwait(false)) is { } line) -#endif - { - var chunk = JsonSerializer.Deserialize(line, JsonContext.Default.OllamaChatResponse); - if (chunk is null) - { - continue; - } - - string? modelId = chunk.Model ?? _metadata.DefaultModelId; - - ChatResponseUpdate update = new() - { - CreatedAt = DateTimeOffset.TryParse(chunk.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset createdAt) ? createdAt : null, - FinishReason = ToFinishReason(chunk), - ModelId = modelId, - ResponseId = responseId, - MessageId = responseId, // There is no per-message ID, but there's only one message per response, so use the response ID - Role = chunk.Message?.Role is not null ? new ChatRole(chunk.Message.Role) : null, - }; - - if (chunk.Message is { } message) - { - if (message.ToolCalls is { Length: > 0 }) - { - foreach (var toolCall in message.ToolCalls) - { - if (toolCall.Function is { } function) - { - update.Contents.Add(ToFunctionCallContent(function)); - } - } - } - - // Equivalent rule to the nonstreaming case - if (message.Content?.Length > 0 || update.Contents.Count == 0) - { - update.Contents.Insert(0, new TextContent(message.Content)); - } - } - - if (ParseOllamaChatResponseUsage(chunk) is { } usage) - { - update.Contents.Add(new UsageContent(usage)); - } - - yield return update; - } - } - - /// - object? IChatClient.GetService(Type serviceType, object? serviceKey) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType == typeof(ChatClientMetadata) ? _metadata : - serviceType.IsInstanceOfType(this) ? this : - null; - } - - /// - public void Dispose() - { - if (_httpClient != OllamaUtilities.SharedClient) - { - _httpClient.Dispose(); - } - } - - private static UsageDetails? ParseOllamaChatResponseUsage(OllamaChatResponse response) - { - AdditionalPropertiesDictionary? additionalCounts = null; - OllamaUtilities.TransferNanosecondsTime(response, static r => r.LoadDuration, "load_duration", ref additionalCounts); - OllamaUtilities.TransferNanosecondsTime(response, static r => r.TotalDuration, "total_duration", ref additionalCounts); - OllamaUtilities.TransferNanosecondsTime(response, static r => r.PromptEvalDuration, "prompt_eval_duration", ref additionalCounts); - OllamaUtilities.TransferNanosecondsTime(response, static r => r.EvalDuration, "eval_duration", ref additionalCounts); - - if (additionalCounts is not null || response.PromptEvalCount is not null || response.EvalCount is not null) - { - return new() - { - InputTokenCount = response.PromptEvalCount, - OutputTokenCount = response.EvalCount, - TotalTokenCount = response.PromptEvalCount.GetValueOrDefault() + response.EvalCount.GetValueOrDefault(), - AdditionalCounts = additionalCounts, - }; - } - - return null; - } - - private static ChatFinishReason? ToFinishReason(OllamaChatResponse response) => - response.DoneReason switch - { - null => null, - "length" => ChatFinishReason.Length, - "stop" => ChatFinishReason.Stop, - _ => new ChatFinishReason(response.DoneReason), - }; - - private static ChatMessage FromOllamaMessage(OllamaChatResponseMessage message, string responseId) - { - List contents = []; - - // Add any tool calls. - if (message.ToolCalls is { Length: > 0 }) - { - foreach (var toolCall in message.ToolCalls) - { - if (toolCall.Function is { } function) - { - contents.Add(ToFunctionCallContent(function)); - } - } - } - - // Ollama frequently sends back empty content with tool calls. Rather than always adding an empty - // content, we only add the content if either it's not empty or there weren't any tool calls. - if (message.Content?.Length > 0 || contents.Count == 0) - { - contents.Insert(0, new TextContent(message.Content)); - } - - // Ollama doesn't have per-message IDs, so use the response ID in the same way we do when streaming - return new ChatMessage(new(message.Role), contents) { MessageId = responseId }; - } - - private static FunctionCallContent ToFunctionCallContent(OllamaFunctionToolCall function) - { -#if NET - var id = System.Security.Cryptography.RandomNumberGenerator.GetHexString(8); -#else - var id = Guid.NewGuid().ToString().Substring(0, 8); -#endif - return new FunctionCallContent(id, function.Name, function.Arguments); - } - - private static JsonElement? ToOllamaChatResponseFormat(ChatResponseFormat? format) - { - if (format is ChatResponseFormatJson jsonFormat) - { - return _schemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) ?? _schemalessJsonResponseFormatValue; - } - else - { - return null; - } - } - - private OllamaChatRequest ToOllamaChatRequest(IEnumerable messages, ChatOptions? options, bool stream) - { - var requestMessages = messages.SelectMany(ToOllamaChatRequestMessages).ToList(); - if (options?.Instructions is string instructions) - { - requestMessages.Insert(0, new OllamaChatRequestMessage - { - Role = ChatRole.System.Value, - Content = instructions, - }); - } - - OllamaChatRequest request = new() - { - Format = ToOllamaChatResponseFormat(options?.ResponseFormat), - Messages = requestMessages, - Model = options?.ModelId ?? _metadata.DefaultModelId ?? string.Empty, - Stream = stream, - Tools = options?.ToolMode is not NoneChatToolMode && options?.Tools is { Count: > 0 } tools ? tools.OfType().Select(ToOllamaTool) : null, - }; - - if (options is not null) - { - TransferMetadataValue(nameof(OllamaRequestOptions.embedding_only), (options, value) => options.embedding_only = value); - TransferMetadataValue(nameof(OllamaRequestOptions.f16_kv), (options, value) => options.f16_kv = value); - TransferMetadataValue(nameof(OllamaRequestOptions.logits_all), (options, value) => options.logits_all = value); - TransferMetadataValue(nameof(OllamaRequestOptions.low_vram), (options, value) => options.low_vram = value); - TransferMetadataValue(nameof(OllamaRequestOptions.main_gpu), (options, value) => options.main_gpu = value); - TransferMetadataValue(nameof(OllamaRequestOptions.min_p), (options, value) => options.min_p = value); - TransferMetadataValue(nameof(OllamaRequestOptions.mirostat), (options, value) => options.mirostat = value); - TransferMetadataValue(nameof(OllamaRequestOptions.mirostat_eta), (options, value) => options.mirostat_eta = value); - TransferMetadataValue(nameof(OllamaRequestOptions.mirostat_tau), (options, value) => options.mirostat_tau = value); - TransferMetadataValue(nameof(OllamaRequestOptions.num_batch), (options, value) => options.num_batch = value); - TransferMetadataValue(nameof(OllamaRequestOptions.num_ctx), (options, value) => options.num_ctx = value); - TransferMetadataValue(nameof(OllamaRequestOptions.num_gpu), (options, value) => options.num_gpu = value); - TransferMetadataValue(nameof(OllamaRequestOptions.num_keep), (options, value) => options.num_keep = value); - TransferMetadataValue(nameof(OllamaRequestOptions.num_thread), (options, value) => options.num_thread = value); - TransferMetadataValue(nameof(OllamaRequestOptions.numa), (options, value) => options.numa = value); - TransferMetadataValue(nameof(OllamaRequestOptions.penalize_newline), (options, value) => options.penalize_newline = value); - TransferMetadataValue(nameof(OllamaRequestOptions.repeat_last_n), (options, value) => options.repeat_last_n = value); - TransferMetadataValue(nameof(OllamaRequestOptions.repeat_penalty), (options, value) => options.repeat_penalty = value); - TransferMetadataValue(nameof(OllamaRequestOptions.tfs_z), (options, value) => options.tfs_z = value); - TransferMetadataValue(nameof(OllamaRequestOptions.typical_p), (options, value) => options.typical_p = value); - TransferMetadataValue(nameof(OllamaRequestOptions.use_mmap), (options, value) => options.use_mmap = value); - TransferMetadataValue(nameof(OllamaRequestOptions.use_mlock), (options, value) => options.use_mlock = value); - TransferMetadataValue(nameof(OllamaRequestOptions.vocab_only), (options, value) => options.vocab_only = value); - - if (options.FrequencyPenalty is float frequencyPenalty) - { - (request.Options ??= new()).frequency_penalty = frequencyPenalty; - } - - if (options.MaxOutputTokens is int maxOutputTokens) - { - (request.Options ??= new()).num_predict = maxOutputTokens; - } - - if (options.PresencePenalty is float presencePenalty) - { - (request.Options ??= new()).presence_penalty = presencePenalty; - } - - if (options.StopSequences is { Count: > 0 }) - { - (request.Options ??= new()).stop = [.. options.StopSequences]; - } - - if (options.Temperature is float temperature) - { - (request.Options ??= new()).temperature = temperature; - } - - if (options.TopP is float topP) - { - (request.Options ??= new()).top_p = topP; - } - - if (options.TopK is int topK) - { - (request.Options ??= new()).top_k = topK; - } - - if (options.Seed is long seed) - { - (request.Options ??= new()).seed = seed; - } - } - - return request; - - void TransferMetadataValue(string propertyName, Action setOption) - { - if (options.AdditionalProperties?.TryGetValue(propertyName, out T? t) is true) - { - request.Options ??= new(); - setOption(request.Options, t); - } - } - } - - private IEnumerable ToOllamaChatRequestMessages(ChatMessage content) - { - // In general, we return a single request message for each understood content item. - // However, various image models expect both text and images in the same request message. - // To handle that, attach images to a previous text message if one exists. - - OllamaChatRequestMessage? currentTextMessage = null; - foreach (var item in content.Contents) - { - if (item is DataContent dataContent && dataContent.HasTopLevelMediaType("image")) - { - IList images = currentTextMessage?.Images ?? []; - images.Add(dataContent.Base64Data.ToString()); - - if (currentTextMessage is not null) - { - currentTextMessage.Images = images; - } - else - { - yield return new OllamaChatRequestMessage - { - Role = content.Role.Value, - Images = images, - }; - } - } - else - { - if (currentTextMessage is not null) - { - yield return currentTextMessage; - currentTextMessage = null; - } - - switch (item) - { - case TextContent textContent: - currentTextMessage = new OllamaChatRequestMessage - { - Role = content.Role.Value, - Content = textContent.Text, - }; - break; - - case FunctionCallContent fcc: - { - yield return new OllamaChatRequestMessage - { - Role = "assistant", - Content = JsonSerializer.Serialize(new OllamaFunctionCallContent - { - CallId = fcc.CallId, - Name = fcc.Name, - Arguments = JsonSerializer.SerializeToElement(fcc.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary))), - }, JsonContext.Default.OllamaFunctionCallContent) - }; - break; - } - - case FunctionResultContent frc: - { - JsonElement jsonResult = JsonSerializer.SerializeToElement(frc.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object))); - yield return new OllamaChatRequestMessage - { - Role = "tool", - Content = JsonSerializer.Serialize(new OllamaFunctionResultContent - { - CallId = frc.CallId, - Result = jsonResult, - }, JsonContext.Default.OllamaFunctionResultContent) - }; - break; - } - } - } - } - - if (currentTextMessage is not null) - { - yield return currentTextMessage; - } - } - - private static OllamaTool ToOllamaTool(AIFunction function) - { - return new() - { - Type = "function", - Function = new OllamaFunctionTool - { - Name = function.Name, - Description = function.Description, - Parameters = JsonSerializer.Deserialize(_schemaTransformCache.GetOrCreateTransformedSchema(function), JsonContext.Default.OllamaFunctionToolParameters)!, - } - }; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs deleted file mode 100644 index 7cdadb91666..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequest.cs +++ /dev/null @@ -1,17 +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.Collections.Generic; -using System.Text.Json; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaChatRequest -{ - public required string Model { get; set; } - public required IList Messages { get; set; } - public JsonElement? Format { get; set; } - public bool Stream { get; set; } - public IEnumerable? Tools { get; set; } - public OllamaRequestOptions? Options { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequestMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequestMessage.cs deleted file mode 100644 index 5a377b1eb34..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatRequestMessage.cs +++ /dev/null @@ -1,13 +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.Collections.Generic; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaChatRequestMessage -{ - public required string Role { get; set; } - public string? Content { get; set; } - public IList? Images { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponse.cs deleted file mode 100644 index 8c39f9ab598..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaChatResponse -{ - public string? Model { get; set; } - public string? CreatedAt { get; set; } - public long? TotalDuration { get; set; } - public long? LoadDuration { get; set; } - public string? DoneReason { get; set; } - public int? PromptEvalCount { get; set; } - public long? PromptEvalDuration { get; set; } - public int? EvalCount { get; set; } - public long? EvalDuration { get; set; } - public OllamaChatResponseMessage? Message { get; set; } - public bool Done { get; set; } - public string? Error { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponseMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponseMessage.cs deleted file mode 100644 index bf73c08d793..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatResponseMessage.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaChatResponseMessage -{ - public required string Role { get; set; } - public required string Content { get; set; } - public OllamaToolCall[]? ToolCalls { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs deleted file mode 100644 index 0b0d4d3b344..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs +++ /dev/null @@ -1,165 +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.Linq; -using System.Net.Http; -using System.Net.Http.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable S3358 // Ternary operators should not be nested - -namespace Microsoft.Extensions.AI; - -/// Represents an for Ollama. -public sealed class OllamaEmbeddingGenerator : IEmbeddingGenerator> -{ - /// Metadata about the embedding generator. - private readonly EmbeddingGeneratorMetadata _metadata; - - /// The api/embeddings endpoint URI. - private readonly Uri _apiEmbeddingsEndpoint; - - /// The to use for sending requests. - private readonly HttpClient _httpClient; - - /// Initializes a new instance of the class. - /// The endpoint URI where Ollama is hosted. - /// - /// The ID of the model to use. This ID can also be overridden per request via . - /// Either this parameter or must provide a valid model ID. - /// - /// An instance to use for HTTP operations. - public OllamaEmbeddingGenerator(string endpoint, string? modelId = null, HttpClient? httpClient = null) - : this(new Uri(Throw.IfNull(endpoint)), modelId, httpClient) - { - } - - /// Initializes a new instance of the class. - /// The endpoint URI where Ollama is hosted. - /// - /// The ID of the model to use. This ID can also be overridden per request via . - /// Either this parameter or must provide a valid model ID. - /// - /// An instance to use for HTTP operations. - /// is . - /// is empty or composed entirely of whitespace. - public OllamaEmbeddingGenerator(Uri endpoint, string? modelId = null, HttpClient? httpClient = null) - { - _ = Throw.IfNull(endpoint); - if (modelId is not null) - { - _ = Throw.IfNullOrWhitespace(modelId); - } - - _apiEmbeddingsEndpoint = new Uri(endpoint, "api/embed"); - _httpClient = httpClient ?? OllamaUtilities.SharedClient; - _metadata = new("ollama", endpoint, modelId); - } - - /// - object? IEmbeddingGenerator.GetService(Type serviceType, object? serviceKey) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata : - serviceType.IsInstanceOfType(this) ? this : - null; - } - - /// - public void Dispose() - { - if (_httpClient != OllamaUtilities.SharedClient) - { - _httpClient.Dispose(); - } - } - - /// - public async Task>> GenerateAsync( - IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(values); - - // Create request. - string[] inputs = values.ToArray(); - string? requestModel = options?.ModelId ?? _metadata.DefaultModelId; - var request = new OllamaEmbeddingRequest - { - Model = requestModel ?? string.Empty, - Input = inputs, - }; - - if (options?.AdditionalProperties is { } requestProps) - { - if (requestProps.TryGetValue("keep_alive", out long keepAlive)) - { - request.KeepAlive = keepAlive; - } - - if (requestProps.TryGetValue("truncate", out bool truncate)) - { - request.Truncate = truncate; - } - } - - // Send request and get response. - var httpResponse = await _httpClient.PostAsJsonAsync( - _apiEmbeddingsEndpoint, - request, - JsonContext.Default.OllamaEmbeddingRequest, - cancellationToken).ConfigureAwait(false); - - if (!httpResponse.IsSuccessStatusCode) - { - await OllamaUtilities.ThrowUnsuccessfulOllamaResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); - } - - var response = (await httpResponse.Content.ReadFromJsonAsync( - JsonContext.Default.OllamaEmbeddingResponse, - cancellationToken).ConfigureAwait(false))!; - - // Validate response. - if (!string.IsNullOrEmpty(response.Error)) - { - throw new InvalidOperationException($"Ollama error: {response.Error}"); - } - - if (response.Embeddings is null || response.Embeddings.Length != inputs.Length) - { - throw new InvalidOperationException($"Ollama generated {response.Embeddings?.Length ?? 0} embeddings but {inputs.Length} were expected."); - } - - // Convert response into result objects. - AdditionalPropertiesDictionary? additionalCounts = null; - OllamaUtilities.TransferNanosecondsTime(response, r => r.TotalDuration, "total_duration", ref additionalCounts); - OllamaUtilities.TransferNanosecondsTime(response, r => r.LoadDuration, "load_duration", ref additionalCounts); - - UsageDetails? usage = null; - if (additionalCounts is not null || response.PromptEvalCount is not null) - { - usage = new() - { - InputTokenCount = response.PromptEvalCount, - TotalTokenCount = response.PromptEvalCount, - AdditionalCounts = additionalCounts, - }; - } - - return new(response.Embeddings.Select(e => - new Embedding(e) - { - CreatedAt = DateTimeOffset.UtcNow, - ModelId = response.Model ?? requestModel, - })) - { - Usage = usage, - }; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingRequest.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingRequest.cs deleted file mode 100644 index 07e3530b8ed..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaEmbeddingRequest -{ - public required string Model { get; set; } - public required string[] Input { get; set; } - public OllamaRequestOptions? Options { get; set; } - public bool? Truncate { get; set; } - public long? KeepAlive { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingResponse.cs deleted file mode 100644 index c4fd2cde87c..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingResponse.cs +++ /dev/null @@ -1,21 +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.Serialization; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaEmbeddingResponse -{ - [JsonPropertyName("model")] - public string? Model { get; set; } - [JsonPropertyName("embeddings")] - public float[][]? Embeddings { get; set; } - [JsonPropertyName("total_duration")] - public long? TotalDuration { get; set; } - [JsonPropertyName("load_duration")] - public long? LoadDuration { get; set; } - [JsonPropertyName("prompt_eval_count")] - public int? PromptEvalCount { get; set; } - public string? Error { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionCallContent.cs deleted file mode 100644 index f518413586a..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionCallContent.cs +++ /dev/null @@ -1,13 +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; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionCallContent -{ - public string? CallId { get; set; } - public string? Name { get; set; } - public JsonElement Arguments { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionResultContent.cs deleted file mode 100644 index ba3eab607b8..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionResultContent.cs +++ /dev/null @@ -1,12 +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; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionResultContent -{ - public string? CallId { get; set; } - public JsonElement Result { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionTool.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionTool.cs deleted file mode 100644 index 880e37bec2a..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionTool.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionTool -{ - public required string Name { get; set; } - public required string Description { get; set; } - public required OllamaFunctionToolParameters Parameters { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolCall.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolCall.cs deleted file mode 100644 index c94d41bd3f3..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolCall.cs +++ /dev/null @@ -1,12 +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.Collections.Generic; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionToolCall -{ - public required string Name { get; set; } - public IDictionary? Arguments { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameter.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameter.cs deleted file mode 100644 index 77ba2a5561c..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameter.cs +++ /dev/null @@ -1,13 +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.Collections.Generic; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionToolParameter -{ - public string? Type { get; set; } - public string? Description { get; set; } - public IEnumerable? Enum { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameters.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameters.cs deleted file mode 100644 index 9fa7d0d2adc..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaFunctionToolParameters.cs +++ /dev/null @@ -1,14 +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.Collections.Generic; -using System.Text.Json; - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaFunctionToolParameters -{ - public string Type { get; set; } = "object"; - public required IDictionary Properties { get; set; } - public IList? Required { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaRequestOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaRequestOptions.cs deleted file mode 100644 index cc8b548c1a1..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaRequestOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -#pragma warning disable IDE1006 // Naming Styles - -internal sealed class OllamaRequestOptions -{ - public bool? embedding_only { get; set; } - public bool? f16_kv { get; set; } - public float? frequency_penalty { get; set; } - public bool? logits_all { get; set; } - public bool? low_vram { get; set; } - public int? main_gpu { get; set; } - public float? min_p { get; set; } - public int? mirostat { get; set; } - public float? mirostat_eta { get; set; } - public float? mirostat_tau { get; set; } - public int? num_batch { get; set; } - public int? num_ctx { get; set; } - public int? num_gpu { get; set; } - public int? num_keep { get; set; } - public int? num_predict { get; set; } - public int? num_thread { get; set; } - public bool? numa { get; set; } - public bool? penalize_newline { get; set; } - public float? presence_penalty { get; set; } - public int? repeat_last_n { get; set; } - public float? repeat_penalty { get; set; } - public long? seed { get; set; } - public string[]? stop { get; set; } - public float? temperature { get; set; } - public float? tfs_z { get; set; } - public int? top_k { get; set; } - public float? top_p { get; set; } - public float? typical_p { get; set; } - public bool? use_mlock { get; set; } - public bool? use_mmap { get; set; } - public bool? vocab_only { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaTool.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaTool.cs deleted file mode 100644 index 457793dc476..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaTool.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaTool -{ - public required string Type { get; set; } - public required OllamaFunctionTool Function { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaToolCall.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaToolCall.cs deleted file mode 100644 index a00d0e0e290..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaToolCall.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -internal sealed class OllamaToolCall -{ - public OllamaFunctionToolCall? Function { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaUtilities.cs deleted file mode 100644 index ea2625bd50e..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaUtilities.cs +++ /dev/null @@ -1,72 +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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.AI; - -internal static class OllamaUtilities -{ - /// Gets a singleton used when no other instance is supplied. - public static HttpClient SharedClient { get; } = new() - { - // Expected use is localhost access for non-production use. Typical production use should supply - // an HttpClient configured with whatever more robust resilience policy / handlers are appropriate. - Timeout = Timeout.InfiniteTimeSpan, - }; - - public static void TransferNanosecondsTime(TResponse response, Func getNanoseconds, string key, ref AdditionalPropertiesDictionary? metadata) - { - if (getNanoseconds(response) is long duration) - { - try - { - (metadata ??= [])[key] = duration; - } - catch (OverflowException) - { - // Ignore options that don't convert - } - } - } - - [DoesNotReturn] - public static async ValueTask ThrowUnsuccessfulOllamaResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) - { - Debug.Assert(!response.IsSuccessStatusCode, "must only be invoked for unsuccessful responses."); - - // Read the entire response content into a string. - string errorContent = -#if NET - await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - await response.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif - - // The response content *could* be JSON formatted, try to extract the error field. - -#pragma warning disable CA1031 // Do not catch general exception types - try - { - using JsonDocument document = JsonDocument.Parse(errorContent); - if (document.RootElement.TryGetProperty("error", out JsonElement errorElement) && - errorElement.ValueKind is JsonValueKind.String) - { - errorContent = errorElement.GetString()!; - } - } - catch - { - // Ignore JSON parsing errors. - } -#pragma warning restore CA1031 // Do not catch general exception types - - throw new InvalidOperationException($"Ollama error: {errorContent}"); - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md b/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md deleted file mode 100644 index 3c85cd17087..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Microsoft.Extensions.AI.Ollama - -This package is deprecated and the [OllamaSharp](https://www.nuget.org/packages/ollamasharp) package is recommended. `OllamaSharp` provides .NET bindings for the [Ollama API](https://github.com/jmorganca/ollama/blob/main/docs/api.md), simplifying interactions with Ollama both locally and remotely. - -No further updates, features, or fixes are planned for the `Microsoft.Extensions.AI.Ollama` package. - -## Related packages - -* [OllamaSharp](https://www.nuget.org/packages/OllamaSharp) -* [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI) -* [Microsoft.Extensions.AI.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions) diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj deleted file mode 100644 index 5db789e3b6b..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - Microsoft.Extensions.AI - Unit tests for Microsoft.Extensions.AI.Ollama - - - - true - - - - - - - - - - - - - - diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs deleted file mode 100644 index 83e84e49f5b..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientIntegrationTests.cs +++ /dev/null @@ -1,115 +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.ComponentModel; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.TestUtilities; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class OllamaChatClientIntegrationTests : ChatClientIntegrationTests -{ - protected override IChatClient? CreateChatClient() => - IntegrationTestHelpers.GetOllamaUri() is Uri endpoint ? - new OllamaChatClient(endpoint, "llama3.1") : - null; - - public override Task FunctionInvocation_RequireAny() => - throw new SkipTestException("Ollama does not currently support requiring function invocation."); - - public override Task FunctionInvocation_RequireSpecific() => - throw new SkipTestException("Ollama does not currently support requiring function invocation."); - - protected override string? GetModel_MultiModal_DescribeImage() => "llava"; - - [ConditionalFact] - public async Task PromptBasedFunctionCalling_NoArgs() - { - SkipIfNotEnabled(); - - using var chatClient = CreateChatClient()! - .AsBuilder() - .UseFunctionInvocation() - .UsePromptBasedFunctionCalling() - .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) - .Build(); - - var secretNumber = 42; - var response = await chatClient.GetResponseAsync("What is the current secret number? Answer with digits only.", new ChatOptions - { - ModelId = "llama3:8b", - Tools = [AIFunctionFactory.Create(() => secretNumber, "GetSecretNumber")], - Temperature = 0, - Seed = 0, - }); - - Assert.Contains(secretNumber.ToString(), response.Text); - } - - [ConditionalFact] - public async Task PromptBasedFunctionCalling_WithArgs() - { - SkipIfNotEnabled(); - - using var chatClient = CreateChatClient()! - .AsBuilder() - .UseFunctionInvocation() - .UsePromptBasedFunctionCalling() - .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) - .Build(); - - var stockPriceTool = AIFunctionFactory.Create([Description("Returns the stock price for a given ticker symbol")] ( - [Description("The ticker symbol")] string symbol, - [Description("The currency code such as USD or JPY")] string currency) => - { - Assert.Equal("MSFT", symbol); - Assert.Equal("GBP", currency); - return 999; - }, "GetStockPrice"); - - var didCallIrrelevantTool = false; - var irrelevantTool = AIFunctionFactory.Create(() => { didCallIrrelevantTool = true; return 123; }, "GetSecretNumber"); - - var response = await chatClient.GetResponseAsync("What's the stock price for Microsoft in British pounds?", new ChatOptions - { - Tools = [stockPriceTool, irrelevantTool], - Temperature = 0, - Seed = 0, - }); - - Assert.Contains("999", response.Text); - Assert.False(didCallIrrelevantTool); - } - - [ConditionalFact] - public async Task InvalidModelParameter_ThrowsInvalidOperationException() - { - SkipIfNotEnabled(); - - var endpoint = IntegrationTestHelpers.GetOllamaUri(); - Assert.NotNull(endpoint); - - using var chatClient = new OllamaChatClient(endpoint, modelId: "inexistent-model"); - - InvalidOperationException ex; - ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("Hello, world!")); - Assert.Contains("inexistent-model", ex.Message); - - ex = await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("Hello, world!").ToChatResponseAsync()); - Assert.Contains("inexistent-model", ex.Message); - } - - private sealed class AssertNoToolsDefinedChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient) - { - public override Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - Assert.Null(options?.Tools); - return base.GetResponseAsync(messages, options, cancellationToken); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs deleted file mode 100644 index 2f716d2fe7d..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaChatClientTests.cs +++ /dev/null @@ -1,487 +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.ComponentModel; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -#pragma warning disable S103 // Lines should not be too long - -namespace Microsoft.Extensions.AI; - -public class OllamaChatClientTests -{ - [Fact] - public void Ctor_InvalidArgs_Throws() - { - Assert.Throws("endpoint", () => new OllamaChatClient((Uri)null!)); - Assert.Throws("modelId", () => new OllamaChatClient("http://localhost", " ")); - } - - [Fact] - public void ToolCallJsonSerializerOptions_HasExpectedValue() - { - using OllamaChatClient client = new("http://localhost", "model"); - - Assert.Same(client.ToolCallJsonSerializerOptions, AIJsonUtilities.DefaultOptions); - Assert.Throws("value", () => client.ToolCallJsonSerializerOptions = null!); - - JsonSerializerOptions options = new(); - client.ToolCallJsonSerializerOptions = options; - Assert.Same(options, client.ToolCallJsonSerializerOptions); - } - - [Fact] - public void GetService_SuccessfullyReturnsUnderlyingClient() - { - using OllamaChatClient client = new("http://localhost"); - - Assert.Same(client, client.GetService()); - Assert.Same(client, client.GetService()); - - using IChatClient pipeline = client - .AsBuilder() - .UseFunctionInvocation() - .UseOpenTelemetry() - .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Build(); - - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - - Assert.Same(client, pipeline.GetService()); - Assert.IsType(pipeline.GetService()); - } - - [Fact] - public void Ctor_ProducesExpectedMetadata() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - using IChatClient chatClient = new OllamaChatClient(endpoint, model); - var metadata = chatClient.GetService(); - Assert.NotNull(metadata); - Assert.Equal("ollama", metadata.ProviderName); - Assert.Equal(endpoint, metadata.ProviderUri); - Assert.Equal(model, metadata.DefaultModelId); - } - - [Fact] - public async Task BasicRequestResponse_NonStreaming() - { - const string Input = """ - { - "model":"llama3.1", - "messages":[{"role":"user","content":"hello"}], - "stream":false, - "options":{"num_predict":10,"temperature":0.5} - } - """; - - const string Output = """ - { - "model": "llama3.1", - "created_at": "2024-10-01T15:46:10.5248793Z", - "message": { - "role": "assistant", - "content": "Hello! How are you today? Is there something" - }, - "done_reason": "length", - "done": true, - "total_duration": 22186844400, - "load_duration": 17947219100, - "prompt_eval_count": 11, - "prompt_eval_duration": 1953805000, - "eval_count": 10, - "eval_duration": 2277274000 - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using OllamaChatClient client = new("http://localhost:11434", "llama3.1", httpClient); - var response = await client.GetResponseAsync("hello", new() - { - MaxOutputTokens = 10, - Temperature = 0.5f, - }); - Assert.NotNull(response); - - Assert.Equal("Hello! How are you today? Is there something", response.Text); - Assert.Single(response.Messages.Single().Contents); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal("llama3.1", response.ModelId); - Assert.Equal(DateTimeOffset.Parse("2024-10-01T15:46:10.5248793Z"), response.CreatedAt); - Assert.Equal(ChatFinishReason.Length, response.FinishReason); - Assert.NotNull(response.Usage); - Assert.Equal(11, response.Usage.InputTokenCount); - Assert.Equal(10, response.Usage.OutputTokenCount); - Assert.Equal(21, response.Usage.TotalTokenCount); - } - - [Fact] - public async Task BasicRequestResponse_Streaming() - { - const string Input = """ - { - "model":"llama3.1", - "messages":[{"role":"user","content":"hello"}], - "stream":true, - "options":{"num_predict":20,"temperature":0.5} - } - """; - - const string Output = """ - {"model":"llama3.1","created_at":"2024-10-01T16:15:20.4965315Z","message":{"role":"assistant","content":"Hello"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:20.763058Z","message":{"role":"assistant","content":"!"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:20.9751134Z","message":{"role":"assistant","content":" How"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:21.1788125Z","message":{"role":"assistant","content":" are"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:21.3883171Z","message":{"role":"assistant","content":" you"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:21.5912498Z","message":{"role":"assistant","content":" today"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:21.7968039Z","message":{"role":"assistant","content":"?"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.0034152Z","message":{"role":"assistant","content":" Is"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.1931196Z","message":{"role":"assistant","content":" there"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.3827484Z","message":{"role":"assistant","content":" something"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.5659027Z","message":{"role":"assistant","content":" I"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.7488871Z","message":{"role":"assistant","content":" can"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:22.9339881Z","message":{"role":"assistant","content":" help"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:23.1201564Z","message":{"role":"assistant","content":" you"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:23.303447Z","message":{"role":"assistant","content":" with"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:23.4964909Z","message":{"role":"assistant","content":" or"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:23.6837816Z","message":{"role":"assistant","content":" would"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:23.8723142Z","message":{"role":"assistant","content":" you"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:24.064613Z","message":{"role":"assistant","content":" like"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:24.2504498Z","message":{"role":"assistant","content":" to"},"done":false} - {"model":"llama3.1","created_at":"2024-10-01T16:15:24.2514508Z","message":{"role":"assistant","content":""},"done_reason":"length", "done":true,"total_duration":11912402900,"load_duration":6824559200,"prompt_eval_count":11,"prompt_eval_duration":1329601000,"eval_count":20,"eval_duration":3754262000} - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = new OllamaChatClient("http://localhost:11434", "llama3.1", httpClient); - - List updates = []; - var streamingResponse = client.GetStreamingResponseAsync("hello", new() - { - MaxOutputTokens = 20, - Temperature = 0.5f, - }); - await foreach (var update in streamingResponse) - { - updates.Add(update); - } - - Assert.Equal(21, updates.Count); - - DateTimeOffset[] createdAts = Regex.Matches(Output, @"2024.*?Z").Cast().Select(m => DateTimeOffset.Parse(m.Value)).ToArray(); - - for (int i = 0; i < updates.Count; i++) - { - Assert.NotNull(updates[i].ResponseId); - Assert.NotNull(updates[i].MessageId); - Assert.Equal(i < updates.Count - 1 ? 1 : 2, updates[i].Contents.Count); - Assert.Equal(ChatRole.Assistant, updates[i].Role); - Assert.Equal("llama3.1", updates[i].ModelId); - Assert.Equal(createdAts[i], updates[i].CreatedAt); - Assert.Equal(i < updates.Count - 1 ? null : ChatFinishReason.Length, updates[i].FinishReason); - } - - Assert.Equal("Hello! How are you today? Is there something I can help you with or would you like to", string.Concat(updates.Select(u => u.Text))); - Assert.Equal(2, updates[updates.Count - 1].Contents.Count); - Assert.IsType(updates[updates.Count - 1].Contents[0]); - UsageContent usage = Assert.IsType(updates[updates.Count - 1].Contents[1]); - Assert.Equal(11, usage.Details.InputTokenCount); - Assert.Equal(20, usage.Details.OutputTokenCount); - Assert.Equal(31, usage.Details.TotalTokenCount); - - var chatResponse = await streamingResponse.ToChatResponseAsync(); - Assert.Single(Assert.Single(chatResponse.Messages).Contents); - Assert.Equal("Hello! How are you today? Is there something I can help you with or would you like to", chatResponse.Text); - } - - [Fact] - public async Task MultipleMessages_NonStreaming() - { - const string Input = """ - { - "model": "llama3.1", - "messages": [ - { - "role": "user", - "content": "hello!" - }, - { - "role": "assistant", - "content": "hi, how are you?" - }, - { - "role": "user", - "content": "i\u0027m good. how are you?" - } - ], - "stream": false, - "options": { - "frequency_penalty": 0.75, - "presence_penalty": 0.5, - "seed": 42, - "stop": ["great"], - "temperature": 0.25 - } - } - """; - - const string Output = """ - { - "model": "llama3.1", - "created_at": "2024-10-01T17:18:46.308987Z", - "message": { - "role": "assistant", - "content": "I'm just a computer program, so I don't have feelings or emotions like humans do, but I'm functioning properly and ready to help with any questions or tasks you may have! How about we chat about something in particular or just shoot the breeze? Your choice!" - }, - "done_reason": "stop", - "done": true, - "total_duration": 23229369000, - "load_duration": 7724086300, - "prompt_eval_count": 36, - "prompt_eval_duration": 4245660000, - "eval_count": 55, - "eval_duration": 11256470000 - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = new OllamaChatClient("http://localhost:11434", httpClient: httpClient); - - List messages = - [ - new(ChatRole.User, "hello!"), - new(ChatRole.Assistant, "hi, how are you?"), - new(ChatRole.User, "i'm good. how are you?"), - ]; - - var response = await client.GetResponseAsync(messages, new() - { - ModelId = "llama3.1", - Temperature = 0.25f, - FrequencyPenalty = 0.75f, - PresencePenalty = 0.5f, - StopSequences = ["great"], - Seed = 42, - }); - Assert.NotNull(response); - - Assert.Equal( - VerbatimHttpHandler.RemoveWhiteSpace(""" - I'm just a computer program, so I don't have feelings or emotions like humans do, - but I'm functioning properly and ready to help with any questions or tasks you may have! - How about we chat about something in particular or just shoot the breeze ? Your choice! - """), - VerbatimHttpHandler.RemoveWhiteSpace(response.Text)); - Assert.Single(response.Messages.Single().Contents); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal("llama3.1", response.ModelId); - Assert.Equal(DateTimeOffset.Parse("2024-10-01T17:18:46.308987Z"), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - Assert.NotNull(response.Usage); - Assert.Equal(36, response.Usage.InputTokenCount); - Assert.Equal(55, response.Usage.OutputTokenCount); - Assert.Equal(91, response.Usage.TotalTokenCount); - } - - [Fact] - public async Task FunctionCallContent_NonStreaming() - { - const string Input = """ - { - "model": "llama3.1", - "messages": [ - { - "role": "user", - "content": "How old is Alice?" - } - ], - "stream": false, - "tools": [ - { - "type": "function", - "function": { - "name": "GetPersonAge", - "description": "Gets the age of the specified person.", - "parameters": { - "type": "object", - "properties": { - "personName": { - "description": "The person whose age is being requested", - "type": "string" - } - }, - "required": ["personName"] - } - } - } - ] - } - """; - - const string Output = """ - { - "model": "llama3.1", - "created_at": "2024-10-01T18:48:30.2669578Z", - "message": { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "function": { - "name": "GetPersonAge", - "arguments": { - "personName": "Alice" - } - } - } - ] - }, - "done_reason": "stop", - "done": true, - "total_duration": 27351311300, - "load_duration": 8041538400, - "prompt_eval_count": 170, - "prompt_eval_duration": 16078776000, - "eval_count": 19, - "eval_duration": 3227962000 - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler) { Timeout = Timeout.InfiniteTimeSpan }; - using IChatClient client = new OllamaChatClient("http://localhost:11434", "llama3.1", httpClient) - { - ToolCallJsonSerializerOptions = TestJsonSerializerContext.Default.Options, - }; - - var response = await client.GetResponseAsync("How old is Alice?", new() - { - Tools = [AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person.")], - }); - Assert.NotNull(response); - - Assert.Empty(response.Text); - Assert.Equal("llama3.1", response.ModelId); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal(DateTimeOffset.Parse("2024-10-01T18:48:30.2669578Z"), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - Assert.NotNull(response.Usage); - Assert.Equal(170, response.Usage.InputTokenCount); - Assert.Equal(19, response.Usage.OutputTokenCount); - Assert.Equal(189, response.Usage.TotalTokenCount); - - Assert.Single(response.Messages.Single().Contents); - FunctionCallContent fcc = Assert.IsType(response.Messages.Single().Contents[0]); - Assert.Equal("GetPersonAge", fcc.Name); - AssertExtensions.EqualFunctionCallParameters(new Dictionary { ["personName"] = "Alice" }, fcc.Arguments); - } - - [Fact] - public async Task FunctionResultContent_NonStreaming() - { - const string Input = """ - { - "model": "llama3.1", - "messages": [ - { - "role": "user", - "content": "How old is Alice?" - }, - { - "role": "assistant", - "content": "{\u0022call_id\u0022:\u0022abcd1234\u0022,\u0022name\u0022:\u0022GetPersonAge\u0022,\u0022arguments\u0022:{\u0022personName\u0022:\u0022Alice\u0022}}" - }, - { - "role": "tool", - "content": "{\u0022call_id\u0022:\u0022abcd1234\u0022,\u0022result\u0022:42}" - } - ], - "stream": false, - "tools": [ - { - "type": "function", - "function": { - "name": "GetPersonAge", - "description": "Gets the age of the specified person.", - "parameters": { - "type": "object", - "properties": { - "personName": { - "description": "The person whose age is being requested", - "type": "string" - } - }, - "required": ["personName"] - } - } - } - ] - } - """; - - const string Output = """ - { - "model": "llama3.1", - "created_at": "2024-10-01T20:57:20.157266Z", - "message": { - "role": "assistant", - "content": "Alice is 42 years old." - }, - "done_reason": "stop", - "done": true, - "total_duration": 20320666000, - "load_duration": 8159642600, - "prompt_eval_count": 106, - "prompt_eval_duration": 10846727000, - "eval_count": 8, - "eval_duration": 1307842000 - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler) { Timeout = Timeout.InfiniteTimeSpan }; - using IChatClient client = new OllamaChatClient("http://localhost:11434", "llama3.1", httpClient) - { - ToolCallJsonSerializerOptions = TestJsonSerializerContext.Default.Options, - }; - - var response = await client.GetResponseAsync( - [ - new(ChatRole.User, "How old is Alice?"), - new(ChatRole.Assistant, [new FunctionCallContent("abcd1234", "GetPersonAge", new Dictionary { ["personName"] = "Alice" })]), - new(ChatRole.Tool, [new FunctionResultContent("abcd1234", 42)]), - ], - new() - { - Tools = [AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person.")], - }); - Assert.NotNull(response); - - Assert.Equal("Alice is 42 years old.", response.Text); - Assert.Equal("llama3.1", response.ModelId); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal(DateTimeOffset.Parse("2024-10-01T20:57:20.157266Z"), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - Assert.NotNull(response.Usage); - Assert.Equal(106, response.Usage.InputTokenCount); - Assert.Equal(8, response.Usage.OutputTokenCount); - Assert.Equal(114, response.Usage.TotalTokenCount); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorIntegrationTests.cs deleted file mode 100644 index 493c0bf0333..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorIntegrationTests.cs +++ /dev/null @@ -1,32 +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.Threading.Tasks; -using Microsoft.TestUtilities; -using Xunit; - -namespace Microsoft.Extensions.AI; - -public class OllamaEmbeddingGeneratorIntegrationTests : EmbeddingGeneratorIntegrationTests -{ - protected override IEmbeddingGenerator>? CreateEmbeddingGenerator() => - IntegrationTestHelpers.GetOllamaUri() is Uri endpoint ? - new OllamaEmbeddingGenerator(endpoint, "all-minilm") : - null; - - [ConditionalFact] - public async Task InvalidModelParameter_ThrowsInvalidOperationException() - { - SkipIfNotEnabled(); - - var endpoint = IntegrationTestHelpers.GetOllamaUri(); - Assert.NotNull(endpoint); - - using var generator = new OllamaEmbeddingGenerator(endpoint, modelId: "inexistent-model"); - - InvalidOperationException ex; - ex = await Assert.ThrowsAsync(() => generator.GenerateAsync(["Hello, world!"])); - Assert.Contains("inexistent-model", ex.Message); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorTests.cs deleted file mode 100644 index be18138de84..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/OllamaEmbeddingGeneratorTests.cs +++ /dev/null @@ -1,102 +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.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -#pragma warning disable S103 // Lines should not be too long - -namespace Microsoft.Extensions.AI; - -public class OllamaEmbeddingGeneratorTests -{ - [Fact] - public void Ctor_InvalidArgs_Throws() - { - Assert.Throws("endpoint", () => new OllamaEmbeddingGenerator((string)null!)); - Assert.Throws("modelId", () => new OllamaEmbeddingGenerator(new Uri("http://localhost"), " ")); - } - - [Fact] - public void GetService_SuccessfullyReturnsUnderlyingClient() - { - using OllamaEmbeddingGenerator generator = new("http://localhost"); - - Assert.Same(generator, generator.GetService()); - Assert.Same(generator, generator.GetService>>()); - - using IEmbeddingGenerator> pipeline = generator - .AsBuilder() - .UseOpenTelemetry() - .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Build(); - - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - - Assert.Same(generator, pipeline.GetService()); - Assert.IsType>>(pipeline.GetService>>()); - } - - [Fact] - public void AsIEmbeddingGenerator_ProducesExpectedMetadata() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - using IEmbeddingGenerator> generator = new OllamaEmbeddingGenerator(endpoint, model); - var metadata = generator.GetService(); - Assert.Equal("ollama", metadata?.ProviderName); - Assert.Equal(endpoint, metadata?.ProviderUri); - Assert.Equal(model, metadata?.DefaultModelId); - } - - [Fact] - public async Task GetEmbeddingsAsync_ExpectedRequestResponse() - { - const string Input = """ - {"model":"all-minilm","input":["hello, world!","red, white, blue"]} - """; - - const string Output = """ - { - "model":"all-minilm", - "embeddings":[ - [-0.038159743,0.032830726,-0.005602915,0.014363416,-0.04031945,-0.11662117,0.031710647,0.0019634133,-0.042558126,0.02925818,0.04254404,0.032178584,0.029820565,0.010947956,-0.05383333,-0.05031401,-0.023460664,0.010746779,-0.13776828,0.003972192,0.029283607,0.06673441,-0.015434976,0.048401773,-0.088160664,-0.012700827,0.04134059,0.0408592,-0.050058633,-0.058048956,0.048720006,0.068883754,0.0588242,0.008813041,-0.016036017,0.08514798,-0.07813561,-0.07740018,0.020856613,0.016228318,0.032506905,-0.053466275,-0.06220645,-0.024293836,0.0073994277,0.02410873,0.006477103,0.051144805,0.072868116,0.03460658,-0.0547553,-0.05937917,-0.007205277,0.020145971,0.035794333,0.005588114,0.010732389,-0.052755248,0.01006711,-0.008716047,-0.062840104,0.038445882,-0.013913384,0.07341423,0.09004691,-0.07995187,-0.016410379,0.044806693,-0.06886798,-0.03302609,-0.015488586,0.0112944925,0.03645402,0.06637969,-0.054364193,0.008732196,0.012049053,-0.038111813,0.006928739,0.05113517,0.07739711,-0.12295967,0.016389083,0.049567502,0.03162499,-0.039604694,0.0016613991,0.009564599,-0.03268798,-0.033994347,-0.13328508,0.0072719813,-0.010261588,0.038570367,-0.093384996,-0.041716397,0.069951184,-0.02632818,-0.149702,0.13445856,0.037486482,0.052814852,0.045044158,0.018727085,0.05445453,0.01727433,-0.032474063,0.046129994,-0.046679277,-0.03058037,-0.0181755,-0.048695795,0.033057086,-0.0038555008,0.050006237,-0.05828653,-0.010029618,0.01062073,-0.040105496,-0.0015263702,0.060846698,-0.04557025,0.049251337,0.026121102,0.019804202,-0.0016694543,0.059516467,-6.525171e-33,0.06351319,0.0030810465,0.028928237,0.17336167,0.0029677018,0.027755935,-0.09513812,-0.031182382,0.026697554,-0.0107956175,0.023849761,0.02378595,-0.03121345,0.049473017,-0.02506533,0.101713106,-0.079133175,-0.0032418896,0.04290832,0.094838716,-0.06652884,0.0062877694,0.02221229,0.0700068,-0.007469806,-0.0017550732,0.027011596,-0.075321496,0.114022695,0.0085597,-0.023766534,-0.04693697,0.014437173,0.01987886,-0.0046902793,0.0013660098,-0.034307938,-0.054156985,-0.09417741,-0.028919358,-0.018871028,0.04574328,0.047602862,-0.0031305805,-0.033291575,-0.0135114025,0.051019657,0.031115327,0.015239397,0.05413997,-0.085031144,0.013366392,-0.04757861,0.07102588,-0.013105953,-0.0023799809,0.050322797,-0.041649505,-0.014187793,0.0324716,0.005401626,0.091307014,0.0044665188,-0.018263677,-0.015284639,-0.04634121,0.038754962,0.014709013,0.052040145,0.0017918312,-0.014979437,0.027103048,0.03117813,0.023749126,-0.004567645,0.03617759,0.06680814,-0.001835277,0.021281,-0.057563916,0.019137124,0.031450257,-0.018432263,-0.040860977,0.10391725,0.011970765,-0.014854915,-0.10521159,-0.012288272,-0.00041675335,-0.09510029,0.058300544,0.042590536,-0.025064372,-0.09454636,4.0064686e-33,0.13224861,0.0053342036,-0.033114634,-0.09096768,-0.031561732,-0.03395822,-0.07202013,0.12591493,-0.08332582,0.052816514,0.001065021,0.022002738,0.1040207,0.013038866,0.04092958,0.018689224,0.1142518,0.024801003,0.014596161,0.006195551,-0.011214642,-0.035760444,-0.037979998,0.011274433,-0.051305123,0.007884909,0.06734877,0.0033462204,-0.09284879,0.037033774,-0.022331867,0.039951596,-0.030730229,-0.011403805,-0.014458028,0.024968812,-0.097553216,-0.03536226,-0.037567392,-0.010149212,-0.06387594,0.025570663,0.02060328,0.037549157,-0.104355134,-0.02837097,-0.052078977,0.0128349,-0.05123587,-0.029060647,-0.09632806,-0.042301137,0.067175224,-0.030890828,-0.010358077,0.027408795,-0.028092034,0.010337195,0.04303845,0.022324203,0.00797792,0.056084383,0.040727936,0.092925824,0.01653155,-0.053750493,0.00046004262,0.050728552,0.04253214,-0.029197674,0.00926312,-0.010662153,-0.037244495,0.002277273,-0.030296732,0.07459592,0.002572513,-0.017561244,0.0028881067,0.03841156,0.007247727,0.045637112,0.039992437,0.014227117,-0.014297474,0.05854321,0.03632371,0.05527864,-0.02007574,-0.08043163,-0.030238612,-0.014929122,0.022335418,0.011954643,-0.06906099,-1.8807288e-8,-0.07850291,0.046684187,-0.023935271,0.063510746,0.024001691,0.0014455577,-0.09078209,-0.066868275,-0.0801402,0.005480386,0.053663295,0.10483363,-0.066864185,0.015531167,0.06711155,0.07081655,-0.031996343,0.020819444,-0.021926524,-0.0073062326,-0.010652819,0.0041180425,0.033138428,-0.0789938,0.03876969,-0.075220205,-0.015715994,0.0059789424,0.005140016,-0.06150612,0.041992374,0.09544083,-0.043187104,0.014401576,-0.10615426,-0.027936764,0.011047429,0.069572434,0.06690283,-0.074798405,-0.07852024,0.04276141,-0.034642085,-0.106051244,-0.03581038,0.051521253,0.06865896,-0.04999753,0.0154549,-0.06452052,-0.07598782,0.02603005,0.074413665,-0.012398757,0.13330704,0.07475513,0.051348723,0.02098748,-0.02679416,0.08896129,0.039944872,-0.041040305,0.031930625,0.018114654], - [0.007228383,-0.021804843,-0.07494023,-0.021707121,-0.021184582,0.09326986,0.10764054,-0.01918113,0.007439991,0.01367952,-0.034187328,-0.044076536,0.016042138,0.007507193,-0.016432272,0.025345335,0.010598066,-0.03832474,-0.14418823,-0.033625234,0.013156937,-0.0048872638,-0.08534306,-0.00003228713,-0.08900276,-0.00008128615,0.010332802,0.053303026,-0.050233904,-0.0879366,-0.064243905,-0.017168961,0.1284308,-0.015268303,-0.049664143,-0.07491954,0.021887481,0.015997978,-0.07967111,0.08744341,-0.039261423,-0.09904984,0.02936398,0.042995434,0.057036504,0.09063012,0.0000012311281,0.06120768,-0.050825767,-0.014443322,0.02879051,-0.002343813,-0.10176559,0.104563184,0.031316753,0.08251861,-0.041213628,-0.0217945,0.0649965,-0.011131547,0.018417398,-0.014460508,-0.05108664,0.11330918,0.01863208,0.006442521,-0.039408617,-0.03609412,-0.009156692,-0.0031261789,-0.010928502,-0.021108521,0.037411734,0.012443921,0.018142054,-0.0362644,0.058286663,-0.02733258,-0.052172586,-0.08320095,-0.07089281,-0.0970049,-0.048587535,0.055343032,0.048351917,0.06892102,-0.039993215,0.06344781,-0.084417015,0.003692423,-0.059397053,0.08186814,0.0029228176,-0.010551637,-0.058019258,0.092128515,0.06862907,-0.06558893,0.021121018,0.079212844,0.09616225,0.0045106052,0.039712362,-0.053576704,0.035097837,-0.04251009,-0.013761404,0.011582285,0.02387105,0.009042205,0.054141942,-0.051263757,-0.07984356,-0.020198742,-0.051623948,-0.0013434993,-0.05825417,-0.0026240738,0.0050159167,-0.06320204,0.07872169,-0.04051374,0.04671058,-0.05804034,-0.07103668,-0.07507343,0.015222599,-3.0948323e-33,0.0076309564,-0.06283016,0.024291662,0.12532257,0.013917241,0.04869009,-0.037988827,-0.035241846,-0.041410565,-0.033772282,0.018835608,0.081035286,-0.049912665,0.044602085,0.030495265,-0.009206943,0.027668765,0.011651487,-0.10254086,0.054472663,-0.06514106,0.12192646,0.048823033,-0.015688669,0.010323047,-0.02821445,-0.030832449,-0.035029083,-0.010604268,0.0014445938,0.08670387,0.01997448,0.0101131955,0.036524937,-0.033489946,-0.026745271,-0.04709222,0.015197909,0.018787097,-0.009976326,-0.0016434817,-0.024719588,-0.09179337,0.09343157,0.029579962,-0.015174558,0.071250066,0.010549244,0.010716396,0.05435638,-0.06391847,-0.031383075,0.007916095,0.012391228,-0.012053197,-0.017409964,0.013742709,0.0594159,-0.033767693,0.04505938,-0.0017214329,0.12797962,0.03223919,-0.054756388,0.025249248,-0.02273578,-0.04701282,-0.018718086,0.009820931,-0.06267794,-0.012644738,0.0068301614,0.093209736,-0.027372226,-0.09436381,0.003861504,0.054960024,-0.058553983,-0.042971537,-0.008994571,-0.08225824,-0.013560626,-0.01880568,0.0995795,-0.040887516,-0.0036491079,-0.010253542,-0.031025425,-0.006957114,-0.038943008,-0.090270124,-0.031345647,0.029613726,-0.099465184,-0.07469079,7.844707e-34,0.024241973,0.03597121,-0.049776066,0.05084303,0.006059542,-0.020719761,0.019962702,0.092246406,0.069408394,0.062306542,0.013837189,0.054749023,0.05090263,0.04100415,-0.02573441,0.09535842,0.036858294,0.059478357,0.0070162765,0.038462427,-0.053635903,0.05912332,-0.037887845,-0.0012995935,-0.068758026,0.0671618,0.029407106,-0.061569903,-0.07481879,-0.01849014,0.014240046,-0.08064838,0.028351007,0.08456427,0.016858438,0.02053254,0.06171099,-0.028964644,-0.047633287,0.08802184,0.0017116248,0.019451816,0.03419083,0.07152118,-0.027244413,-0.04888475,-0.10314279,0.07628554,-0.045991484,-0.023299307,-0.021448445,0.04111079,-0.036342163,-0.010670482,0.01950527,-0.0648448,-0.033299454,0.05782628,0.030278979,0.079154804,-0.03679649,0.031728156,-0.034912236,0.08817754,0.059208114,-0.02319613,-0.027045371,-0.018559752,-0.051946763,-0.010635224,0.048839167,-0.043925915,-0.028300019,-0.0039419765,0.044211324,-0.067469835,-0.027534118,0.005051618,-0.034172326,0.080007285,-0.01931061,-0.005759926,0.08765162,0.08372951,-0.093784876,0.011837292,0.019019455,0.047941882,0.05504541,-0.12475821,0.012822803,0.12833545,0.08005919,0.019278418,-0.025834465,-1.9763878e-8,0.05211108,0.024891146,-0.0015623684,0.0040500895,0.015101377,-0.0031462535,0.014759316,-0.041329216,-0.029255627,0.048599463,0.062482737,0.018376771,-0.066601776,0.014752581,0.07968402,-0.015090815,-0.12100162,-0.0014005995,0.0134423375,-0.0065814927,-0.01188529,-0.01107086,-0.059613306,0.030120188,0.0418596,-0.009260598,0.028435009,0.024893047,0.031339604,0.09501834,0.027570697,0.0636991,-0.056108754,-0.0329521,-0.114633024,-0.00981398,-0.060992315,0.027551433,0.0069592255,-0.059862003,0.0008075791,0.001507554,-0.028574942,-0.011227367,0.0056030746,-0.041190825,-0.09364463,-0.04459479,-0.055058934,-0.029972456,-0.028642913,-0.015199684,0.007875299,-0.034083385,0.02143902,-0.017395096,0.027429376,0.013198211,0.005065835,0.037760753,0.08974973,0.07598824,0.0050444477,0.014734193] - ], - "total_duration":375551700, - "load_duration":354411900, - "prompt_eval_count":9 - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new OllamaEmbeddingGenerator("http://localhost:11434", "all-minilm", httpClient); - - var response = await generator.GenerateAsync([ - "hello, world!", - "red, white, blue", - ]); - Assert.NotNull(response); - Assert.Equal(2, response.Count); - - Assert.NotNull(response.Usage); - Assert.Equal(9, response.Usage.InputTokenCount); - Assert.Equal(9, response.Usage.TotalTokenCount); - - foreach (Embedding e in response) - { - Assert.Equal("all-minilm", e.ModelId); - Assert.NotNull(e.CreatedAt); - Assert.Equal(384, e.Vector.Length); - Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/TestJsonSerializerContext.cs deleted file mode 100644 index 49560a9c451..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/TestJsonSerializerContext.cs +++ /dev/null @@ -1,12 +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.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.Extensions.AI; - -[JsonSerializable(typeof(string))] -[JsonSerializable(typeof(int))] -[JsonSerializable(typeof(IDictionary))] -internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/IntegrationTestHelpers.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/IntegrationTestHelpers.cs rename to test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/IntegrationTestHelpers.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests.csproj index 14ca7e244d1..d977c035279 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpChatClientIntegrationTests.cs index 921e2d3b5f9..28d3e21fd65 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpChatClientIntegrationTests.cs @@ -2,14 +2,115 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.TestUtilities; using OllamaSharp; +using Xunit; namespace Microsoft.Extensions.AI; -public class OllamaSharpChatClientIntegrationTests : OllamaChatClientIntegrationTests +public class OllamaSharpChatClientIntegrationTests : ChatClientIntegrationTests { protected override IChatClient? CreateChatClient() => IntegrationTestHelpers.GetOllamaUri() is Uri endpoint ? new OllamaApiClient(endpoint, "llama3.2") : null; + + public override Task FunctionInvocation_RequireAny() => + throw new SkipTestException("Ollama does not currently support requiring function invocation."); + + public override Task FunctionInvocation_RequireSpecific() => + throw new SkipTestException("Ollama does not currently support requiring function invocation."); + + protected override string? GetModel_MultiModal_DescribeImage() => "llava"; + + [ConditionalFact] + public async Task PromptBasedFunctionCalling_NoArgs() + { + SkipIfNotEnabled(); + + using var chatClient = CreateChatClient()! + .AsBuilder() + .UseFunctionInvocation() + .UsePromptBasedFunctionCalling() + .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) + .Build(); + + var secretNumber = 42; + var response = await chatClient.GetResponseAsync("What is the current secret number? Answer with digits only.", new ChatOptions + { + ModelId = "llama3:8b", + Tools = [AIFunctionFactory.Create(() => secretNumber, "GetSecretNumber")], + Temperature = 0, + Seed = 0, + }); + + Assert.Contains(secretNumber.ToString(), response.Text); + } + + [ConditionalFact] + public async Task PromptBasedFunctionCalling_WithArgs() + { + SkipIfNotEnabled(); + + using var chatClient = CreateChatClient()! + .AsBuilder() + .UseFunctionInvocation() + .UsePromptBasedFunctionCalling() + .Use(innerClient => new AssertNoToolsDefinedChatClient(innerClient)) + .Build(); + + var stockPriceTool = AIFunctionFactory.Create([Description("Returns the stock price for a given ticker symbol")] ( + [Description("The ticker symbol")] string symbol, + [Description("The currency code such as USD or JPY")] string currency) => + { + Assert.Equal("MSFT", symbol); + Assert.Equal("GBP", currency); + return 999; + }, "GetStockPrice"); + + var didCallIrrelevantTool = false; + var irrelevantTool = AIFunctionFactory.Create(() => { didCallIrrelevantTool = true; return 123; }, "GetSecretNumber"); + + var response = await chatClient.GetResponseAsync("What's the stock price for Microsoft in British pounds?", new ChatOptions + { + Tools = [stockPriceTool, irrelevantTool], + Temperature = 0, + Seed = 0, + }); + + Assert.Contains("999", response.Text); + Assert.False(didCallIrrelevantTool); + } + + [ConditionalFact] + public async Task InvalidModelParameter_ThrowsInvalidOperationException() + { + SkipIfNotEnabled(); + + var endpoint = IntegrationTestHelpers.GetOllamaUri(); + Assert.NotNull(endpoint); + + using var chatClient = new OllamaApiClient(endpoint, defaultModel: "inexistent-model"); + + InvalidOperationException ex; + ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("Hello, world!")); + Assert.Contains("inexistent-model", ex.Message); + + ex = await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("Hello, world!").ToChatResponseAsync()); + Assert.Contains("inexistent-model", ex.Message); + } + + private sealed class AssertNoToolsDefinedChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient) + { + public override Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + Assert.Null(options?.Tools); + return base.GetResponseAsync(messages, options, cancellationToken); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpEmbeddingGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpEmbeddingGeneratorIntegrationTests.cs index 1826855f459..f7775143c36 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpEmbeddingGeneratorIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OllamaSharp.Integration.Tests/OllamaSharpEmbeddingGeneratorIntegrationTests.cs @@ -2,14 +2,35 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Threading.Tasks; +using Microsoft.TestUtilities; using OllamaSharp; +using Xunit; namespace Microsoft.Extensions.AI; -public class OllamaSharpEmbeddingGeneratorIntegrationTests : OllamaEmbeddingGeneratorIntegrationTests +public class OllamaSharpEmbeddingGeneratorIntegrationTests : EmbeddingGeneratorIntegrationTests { protected override IEmbeddingGenerator>? CreateEmbeddingGenerator() => IntegrationTestHelpers.GetOllamaUri() is Uri endpoint ? new OllamaApiClient(endpoint, "all-minilm") : null; + + [ConditionalFact] + public async Task InvalidModelParameter_ThrowsInvalidOperationException() + { + SkipIfNotEnabled(); + + var endpoint = IntegrationTestHelpers.GetOllamaUri(); + Assert.NotNull(endpoint); + + using var client = new OllamaApiClient(endpoint, defaultModel: "inexistent-model"); + + InvalidOperationException ex; + ex = await Assert.ThrowsAsync(() => client.EmbedAsync(new OllamaSharp.Models.EmbedRequest + { + Input = ["Hello, world!"], + })); + Assert.Contains("inexistent-model", ex.Message); + } } From 5610f2622838d5df141ad362e3747429a1fba736 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 24 Jul 2025 18:50:30 -0400 Subject: [PATCH 223/472] Fix M.E.AI package refs (#6654) --- .../Microsoft.Extensions.AI.AzureAIInference.csproj | 3 +++ .../Microsoft.Extensions.AI.OpenAI.csproj | 3 +++ .../Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index 5384a7992d7..06690c96eaf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -29,6 +29,9 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index a135ee011ea..ce5a5fd89c0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -33,6 +33,9 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 6331b3c1149..fe431ca21e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -44,6 +44,9 @@ + + + From 8f0f5a7a1c061d80c0bc1b88db4171ea3186252f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 25 Jul 2025 06:40:29 -0400 Subject: [PATCH 224/472] Add [Description] to DataContent.Uri (#6615) So that if a DataContent has schema generated for it, it's obvious in the schema that the "uri" is specifically a data URI. --- .../Contents/DataContent.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs index 57f7fac1962..0c11973381b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs @@ -5,6 +5,7 @@ #if NET using System.Buffers; using System.Buffers.Text; +using System.ComponentModel; #endif using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -142,6 +143,9 @@ public DataContent(ReadOnlyMemory data, string mediaType) /// or from a . /// [StringSyntax(StringSyntaxAttribute.Uri)] +#if NET + [Description("A data URI representing the content.")] +#endif public string Uri { get From 93bebed85940be82a005b84c55057bb4de18e6cc Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 25 Jul 2025 09:04:44 -0400 Subject: [PATCH 225/472] Fix duplicate solution file when creating an AI Chat Web app from VS (#6653) --- .../ChatWithCustomData/.template.config/template.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index 2f9a293b32e..caeceae1dde 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -30,7 +30,7 @@ "path": "./ChatWithCustomData-CSharp.csproj" }, { - "condition": "(IsAspire && (HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\"))", + "condition": "(IsAspire && (hostIdentifier == \"dotnetcli\" || hostIdentifier == \"dotnetcli-preview\"))", "path": "./ChatWithCustomData-CSharp.sln" }, { @@ -75,6 +75,12 @@ "README.Aspire.md": "README.md" } }, + { + "condition": "(IsAspire && hostIdentifier != \"dotnetcli\" && hostIdentifier != \"dotnetcli-preview\")", + "exclude": [ + "*.sln" + ] + }, { "condition": "(!UseLocalVectorStore)", "exclude": [ @@ -558,7 +564,7 @@ } }, "postActions": [{ - "condition": "(hostIdentifier != \"dotnetcli\")", + "condition": "(hostIdentifier != \"dotnetcli\" && hostIdentifier != \"dotnetcli-preview\")", "description": "Opens README file in the editor", "manualInstructions": [ ], "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", From 7798311d745dd90ebb474bfa9886b186ab8a2c41 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 25 Jul 2025 09:12:39 -0400 Subject: [PATCH 226/472] Add ChatMessage.CreatedAt (#6657) We currently have it at the ChatResponse{Update} level, but for more agentic scenarios, it's helpful to have a timestamp per message. --- .../ChatCompletion/ChatMessage.cs | 4 ++ .../ChatCompletion/ChatResponse.cs | 12 ++-- .../ChatCompletion/ChatResponseExtensions.cs | 11 +++- .../Microsoft.Extensions.AI.Abstractions.json | 4 ++ .../AzureAIInferenceChatClient.cs | 1 + .../OpenAIChatClient.cs | 1 + .../OpenAIResponsesChatClient.cs | 5 ++ .../ChatCompletion/ChatMessageTests.cs | 17 ++++++ .../ChatCompletion/ChatResponseTests.cs | 53 ++++++++++++++++- .../ChatResponseUpdateExtensionsTests.cs | 59 +++++++++++++++++++ 10 files changed, 158 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs index 43b3e321df9..9e36f548c00 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs @@ -53,6 +53,7 @@ public ChatMessage Clone() => AdditionalProperties = AdditionalProperties, _authorName = _authorName, _contents = _contents, + CreatedAt = CreatedAt, RawRepresentation = RawRepresentation, Role = Role, MessageId = MessageId, @@ -65,6 +66,9 @@ public string? AuthorName set => _authorName = string.IsNullOrWhiteSpace(value) ? null : value; } + /// Gets or sets a timestamp for the chat message. + public DateTimeOffset? CreatedAt { get; set; } + /// Gets or sets the role of the author of the message. public ChatRole Role { get; set; } = ChatRole.User; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs index a342ef1e69e..0889fed17d6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs @@ -130,19 +130,19 @@ public ChatResponseUpdate[] ToChatResponseUpdates() ChatMessage message = _messages![i]; updates[i] = new ChatResponseUpdate { - ConversationId = ConversationId, - AdditionalProperties = message.AdditionalProperties, AuthorName = message.AuthorName, Contents = message.Contents, + MessageId = message.MessageId, RawRepresentation = message.RawRepresentation, Role = message.Role, - ResponseId = ResponseId, - MessageId = message.MessageId, - CreatedAt = CreatedAt, + ConversationId = ConversationId, FinishReason = FinishReason, - ModelId = ModelId + ModelId = ModelId, + ResponseId = ResponseId, + + CreatedAt = message.CreatedAt ?? CreatedAt, }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 01ce878e79c..6691c665c54 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -84,9 +84,10 @@ public static void AddMessages(this IList list, ChatResponseUpdate var contentsList = filter is null ? update.Contents : update.Contents.Where(filter).ToList(); if (contentsList.Count > 0) { - list.Add(new ChatMessage(update.Role ?? ChatRole.Assistant, contentsList) + list.Add(new(update.Role ?? ChatRole.Assistant, contentsList) { AuthorName = update.AuthorName, + CreatedAt = update.CreatedAt, RawRepresentation = update.RawRepresentation, AdditionalProperties = update.AdditionalProperties, }); @@ -268,7 +269,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon if (isNewMessage) { - message = new ChatMessage(ChatRole.Assistant, []); + message = new(ChatRole.Assistant, []); response.Messages.Add(message); } else @@ -280,11 +281,17 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon // Incorporate those into the latest message; in cases where the message // stores a single value, prefer the latest update's value over anything // stored in the message. + if (update.AuthorName is not null) { message.AuthorName = update.AuthorName; } + if (update.CreatedAt is not null) + { + message.CreatedAt = update.CreatedAt; + } + if (update.Role is ChatRole role) { message.Role = role; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 4d190fccda8..9562de0d93f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -883,6 +883,10 @@ "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.ChatMessage.Contents { get; set; }", "Stage": "Stable" }, + { + "Member": "System.DateTimeOffset? Microsoft.Extensions.AI.ChatMessage.CreatedAt { get; set; }", + "Stage": "Stable" + }, { "Member": "string? Microsoft.Extensions.AI.ChatMessage.MessageId { get; set; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index bb23e1de489..0fd8f4506db 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -95,6 +95,7 @@ public async Task GetResponseAsync( // Create the return message. ChatMessage message = new(ToChatRole(response.Role), response.Content) { + CreatedAt = response.Created, MessageId = response.Id, // There is no per-message ID, but there's only one message per response, so use the response ID RawRepresentation = response, }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 9fc2f3cbeef..a70fcec2a8f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -438,6 +438,7 @@ internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompl // Create the return message. ChatMessage returnMessage = new() { + CreatedAt = openAICompletion.CreatedAt, MessageId = openAICompletion.Id, // There's no per-message ID, so we use the same value as the response ID RawRepresentation = openAICompletion, Role = FromOpenAIChatRole(openAICompletion.Role), diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 60d0e10a159..dfe0c50d374 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -162,6 +162,11 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R } } + foreach (var message in response.Messages) + { + message.CreatedAt = openAIResponse.CreatedAt; + } + return response; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs index c449f064255..7fd9591a6ae 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs @@ -18,6 +18,7 @@ public void Constructor_Parameterless_PropsDefaulted() ChatMessage message = new(); Assert.Null(message.AuthorName); Assert.Empty(message.Contents); + Assert.Null(message.CreatedAt); Assert.Equal(ChatRole.User, message.Role); Assert.Empty(message.Text); Assert.NotNull(message.Contents); @@ -50,6 +51,7 @@ public void Constructor_RoleString_PropsRoundtrip(string? text) } Assert.Null(message.AuthorName); + Assert.Null(message.CreatedAt); Assert.Null(message.RawRepresentation); Assert.Null(message.AdditionalProperties); Assert.Equal(text ?? string.Empty, message.ToString()); @@ -113,6 +115,7 @@ public void Constructor_RoleList_PropsRoundtrip(int messageCount) } Assert.Null(message.AuthorName); + Assert.Null(message.CreatedAt); Assert.Null(message.RawRepresentation); Assert.Null(message.AdditionalProperties); } @@ -230,6 +233,20 @@ public void AdditionalProperties_Roundtrips() Assert.Same(props, message.AdditionalProperties); } + [Fact] + public void CreatedAt_Roundtrips() + { + ChatMessage message = new(); + Assert.Null(message.CreatedAt); + + DateTimeOffset now = DateTimeOffset.Now; + message.CreatedAt = now; + Assert.Equal(now, message.CreatedAt); + + message.CreatedAt = null; + Assert.Null(message.CreatedAt); + } + [Fact] public void ItCanBeSerializeAndDeserialized() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs index 802b414437d..de5809d3d97 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs @@ -125,7 +125,7 @@ public void ToString_OutputsText() } [Fact] - public void ToChatResponseUpdates() + public void ToChatResponseUpdates_SingleMessage() { ChatResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage" }) { @@ -153,4 +153,55 @@ public void ToChatResponseUpdates() Assert.Equal("value1", update1.AdditionalProperties?["key1"]); Assert.Equal(42, update1.AdditionalProperties?["key2"]); } + + [Fact] + public void ToChatResponseUpdates_MultipleMessages() + { + ChatResponse response = new( + [ + new ChatMessage(new ChatRole("customRole"), "Text") + { + CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), + MessageId = "someMessage" + }, + new ChatMessage(new ChatRole("secondRole"), "Another message") + { + CreatedAt = new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero), + MessageId = "anotherMessage" + } + ]) + { + ResponseId = "12345", + ModelId = "someModel", + FinishReason = ChatFinishReason.ContentFilter, + CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), + AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, + }; + + ChatResponseUpdate[] updates = response.ToChatResponseUpdates(); + Assert.NotNull(updates); + Assert.Equal(3, updates.Length); + + ChatResponseUpdate update0 = updates[0]; + Assert.Equal("12345", update0.ResponseId); + Assert.Equal("someMessage", update0.MessageId); + Assert.Equal("someModel", update0.ModelId); + Assert.Equal(ChatFinishReason.ContentFilter, update0.FinishReason); + Assert.Equal(new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt); + Assert.Equal("customRole", update0.Role?.Value); + Assert.Equal("Text", update0.Text); + + ChatResponseUpdate update1 = updates[1]; + Assert.Equal("12345", update1.ResponseId); + Assert.Equal("anotherMessage", update1.MessageId); + Assert.Equal("someModel", update1.ModelId); + Assert.Equal(ChatFinishReason.ContentFilter, update1.FinishReason); + Assert.Equal(new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero), update1.CreatedAt); + Assert.Equal("secondRole", update1.Role?.Value); + Assert.Equal("Another message", update1.Text); + + ChatResponseUpdate update2 = updates[2]; + Assert.Equal("value1", update2.AdditionalProperties?["key1"]); + Assert.Equal(42, update2.AdditionalProperties?["key2"]); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 8b13d640ae1..2eb8db9b477 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -65,6 +65,65 @@ public async Task ToChatResponse_SuccessfullyCreatesResponse(bool useAsync) Assert.Equal("Hello, world!", response.Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToChatResponse_UpdatesProduceMultipleResponseMessages(bool useAsync) + { + ChatResponseUpdate[] updates = + [ + + // First message - ID "msg1" + new(null, "Hi! ") { CreatedAt = new DateTimeOffset(2023, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" }, + new(ChatRole.Assistant, "Hello") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" }, + new(null, " from") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero) }, // Later CreatedAt should win + new(null, " AI") { MessageId = "msg1", AuthorName = "AI Assistant" }, // Later AuthorName should win + + // Second message - ID "msg2" + new(ChatRole.User, "How") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 0, 0, TimeSpan.Zero), AuthorName = "User" }, + new(null, " are") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero) }, + new(null, " you?") { MessageId = "msg2", AuthorName = "Human User" }, // Later AuthorName should win + + // Third message - ID "msg3" + new(ChatRole.Assistant, "I'm doing well,") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero) }, + new(null, " thank you!") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero) }, // Later CreatedAt should win + + // Updates without MessageId should continue the last message (msg3) + new(null, " How can I help?"), + ]; + + ChatResponse response = useAsync ? + await YieldAsync(updates).ToChatResponseAsync() : + updates.ToChatResponse(); + + Assert.NotNull(response); + Assert.Equal(3, response.Messages.Count); + + // Verify first message + ChatMessage message1 = response.Messages[0]; + Assert.Equal("msg1", message1.MessageId); + Assert.Equal(ChatRole.Assistant, message1.Role); + Assert.Equal("AI Assistant", message1.AuthorName); // Last value should win + Assert.Equal(new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero), message1.CreatedAt); // Last value should win + Assert.Equal("Hi! Hello from AI", message1.Text); + + // Verify second message + ChatMessage message2 = response.Messages[1]; + Assert.Equal("msg2", message2.MessageId); + Assert.Equal(ChatRole.User, message2.Role); + Assert.Equal("Human User", message2.AuthorName); // Last value should win + Assert.Equal(new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero), message2.CreatedAt); // Last value should win + Assert.Equal("How are you?", message2.Text); + + // Verify third message + ChatMessage message3 = response.Messages[2]; + Assert.Equal("msg3", message3.MessageId); + Assert.Equal(ChatRole.Assistant, message3.Role); + Assert.Null(message3.AuthorName); // No AuthorName set in later updates + Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero), message3.CreatedAt); // Last value should win + Assert.Equal("I'm doing well, thank you! How can I help?", message3.Text); + } + public static IEnumerable ToChatResponse_Coalescing_VariousSequenceAndGapLengths_MemberData() { foreach (bool useAsync in new[] { false, true }) From ff0619122925f0a4d311796a08859e609543b0ad Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 28 Jul 2025 09:23:44 -0400 Subject: [PATCH 227/472] Add TextContent.Annotations (#6619) * Add TextContent.Annotations --- .../ChatCompletion/ChatResponseExtensions.cs | 23 +++- .../Contents/AIAnnotation.cs | 43 ++++++++ .../Contents/AIAnnotationExtensions.cs | 0 .../Contents/AIAnnotationKind.cs | 0 .../Contents/AIAnnotationReference.cs | 0 .../Contents/AIContent.cs | 6 ++ .../Contents/AnnotatedRegion.cs | 23 ++++ .../Contents/CitationAnnotation.cs | 53 ++++++++++ .../Contents/TextSpanAnnotatedRegion.cs | 32 ++++++ .../Microsoft.Extensions.AI.Abstractions.json | 90 ++++++++++++++++ .../OpenAIAssistantsChatClient.cs | 36 ++++++- .../OpenAIChatClient.cs | 25 +++++ .../OpenAIResponsesChatClient.cs | 21 +++- .../ChatResponseUpdateExtensionsTests.cs | 42 ++++++++ .../Contents/AIAnnotationTests.cs | 71 +++++++++++++ .../Contents/CitationAnnotationTests.cs | 100 ++++++++++++++++++ .../Utilities/AIJsonUtilitiesTests.cs | 3 +- .../ChatClientIntegrationTests.cs | 76 ++++++------- .../IntegrationTestHelpers.cs | 2 +- .../OpenAIResponseClientIntegrationTests.cs | 33 ++++++ 20 files changed, 631 insertions(+), 48 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AnnotatedRegion.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CitationAnnotation.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextSpanAnnotatedRegion.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 6691c665c54..667a9d9aea1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; @@ -197,14 +198,16 @@ static void Coalesce(List contents, Func int start = 0; while (start < contents.Count - 1) { - // We need at least two TextContents in a row to be able to coalesce. - if (contents[start] is not TContent firstText) + // We need at least two TextContents in a row to be able to coalesce. We also avoid touching contents + // that have annotations, as we want to ensure the annotations (and in particular any start/end indices + // into the text content) remain accurate. + if (!TryAsCoalescable(contents[start], out var firstText)) { start++; continue; } - if (contents[start + 1] is not TContent secondText) + if (!TryAsCoalescable(contents[start + 1], out var secondText)) { start += 2; continue; @@ -216,7 +219,7 @@ static void Coalesce(List contents, Func _ = coalescedText.Clear().Append(firstText).Append(secondText); contents[start + 1] = null!; int i = start + 2; - for (; i < contents.Count && contents[i] is TContent next; i++) + for (; i < contents.Count && TryAsCoalescable(contents[i], out TContent? next); i++) { _ = coalescedText.Append(next); contents[i] = null!; @@ -230,6 +233,18 @@ static void Coalesce(List contents, Func newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone(); start = i; + + static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable) + { + if (content is TContent && (content is not TextContent tc || tc.Annotations is not { Count: > 0 })) + { + coalescable = (TContent)content; + return true; + } + + coalescable = null!; + return false; + } } // Remove all of the null slots left over from the coalescing process. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs index e69de29bb2d..73fdff81aa2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents an annotation on content. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(CitationAnnotation), typeDiscriminator: "citation")] +public class AIAnnotation +{ + /// + /// Initializes a new instance of the class. + /// + public AIAnnotation() + { + } + + /// Gets or sets any target regions for the annotation, pointing to where in the associated this annotation applies. + /// + /// The most common form of is , which provides starting and ending character indices + /// for . + /// + public IList? AnnotatedRegions { get; set; } + + /// Gets or sets the raw representation of the annotation from an underlying implementation. + /// + /// If an is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model, if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// + /// Gets or sets additional metadata specific to the provider or source type. + /// + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 06798f10f3d..5d0baf93957 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI; @@ -24,6 +25,11 @@ public AIContent() { } + /// + /// Gets or sets a list of annotations on this content. + /// + public IList? Annotations { get; set; } + /// Gets or sets the raw representation of the content from an underlying implementation. /// /// If an is created to represent some underlying object from another object diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AnnotatedRegion.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AnnotatedRegion.cs new file mode 100644 index 00000000000..fed6dc886b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AnnotatedRegion.cs @@ -0,0 +1,23 @@ +// 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.Serialization; + +namespace Microsoft.Extensions.AI; + +/// Describes the portion of an associated to which an annotation applies. +/// +/// Details about the region is provided by derived types based on how the region is described. For example, starting +/// and ending indices into text content are provided by . +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(TextSpanAnnotatedRegion), typeDiscriminator: "textSpan")] +public class AnnotatedRegion +{ + /// + /// Initializes a new instance of the class. + /// + public AnnotatedRegion() + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CitationAnnotation.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CitationAnnotation.cs new file mode 100644 index 00000000000..5d1d2f88b30 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CitationAnnotation.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents an annotation that links content to source references, +/// such as documents, URLs, files, or tool outputs. +/// +public class CitationAnnotation : AIAnnotation +{ + /// + /// Initializes a new instance of the class. + /// + public CitationAnnotation() + { + } + + /// + /// Gets or sets the title or name of the source. + /// + /// + /// This could be the title of a document, a title from a web page, a name of a file, or similarly descriptive text. + /// + public string? Title { get; set; } + + /// + /// Gets or sets a URI from which the source material was retrieved. + /// + public Uri? Url { get; set; } + + /// Gets or sets a source identifier associated with the annotation. + /// + /// This is a provider-specific identifier that can be used to reference the source material by + /// an ID. This may be a document ID, or a file ID, or some other identifier for the source material + /// that can be used to uniquely identify it with the provider. + /// + public string? FileId { get; set; } + + /// Gets or sets the name of any tool involved in the production of the associated content. + /// + /// This might be a function name, such as one from , or the name of a built-in tool + /// from the provider, such as "code_interpreter" or "file_search". + /// + public string? ToolName { get; set; } + + /// + /// Gets or sets a snippet or excerpt from the source that was cited. + /// + public string? Snippet { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextSpanAnnotatedRegion.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextSpanAnnotatedRegion.cs new file mode 100644 index 00000000000..8ce3dbfa3c5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextSpanAnnotatedRegion.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// Describes a location in the associated based on starting and ending character indices. +/// This typically applies to . +[DebuggerDisplay("[{StartIndex}, {EndIndex})")] +public sealed class TextSpanAnnotatedRegion : AnnotatedRegion +{ + /// + /// Initializes a new instance of the class. + /// + public TextSpanAnnotatedRegion() + { + } + + /// + /// Gets or sets the start character index (inclusive) of the annotated span in the . + /// + [JsonPropertyName("start")] + public int? StartIndex { get; set; } + + /// + /// Gets or sets the end character index (exclusive) of the annotated span in the . + /// + [JsonPropertyName("end")] + public int? EndIndex { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 9562de0d93f..81dcda0bc8d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -123,6 +123,30 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.AIAnnotation", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.AIAnnotation.AIAnnotation();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.AIAnnotation.AnnotatedRegions { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.AIAnnotation.AdditionalProperties { get; set; }", + "Stage": "Stable" + }, + { + "Member": "object? Microsoft.Extensions.AI.AIAnnotation.RawRepresentation { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.AIContent", "Stage": "Stable", @@ -137,6 +161,10 @@ "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.AIContent.AdditionalProperties { get; set; }", "Stage": "Stable" }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.AIContent.Annotations { get; set; }", + "Stage": "Stable" + }, { "Member": "object? Microsoft.Extensions.AI.AIContent.RawRepresentation { get; set; }", "Stage": "Stable" @@ -653,6 +681,16 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.AnnotatedRegion", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.AnnotatedRegion.AnnotatedRegion();", + "Stage": "Stable" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.AutoChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", @@ -1323,6 +1361,38 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.CitationAnnotation : Microsoft.Extensions.AI.AIAnnotation", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.CitationAnnotation.CitationAnnotation();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Title { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.ToolName { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Uri? Microsoft.Extensions.AI.CitationAnnotation.Url { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.FileId { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Snippet { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.DataContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", @@ -2273,6 +2343,26 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.TextSpanAnnotatedRegion : Microsoft.Extensions.AI.AnnotatedRegion", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.TextSpanAnnotatedRegion.TextSpanAnnotatedRegion();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.StartIndex { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.EndIndex { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.UriContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 46bc39ee278..c437a553d28 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -210,7 +210,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( break; case MessageContentUpdate mcu: - yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) + ChatResponseUpdate textUpdate = new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) { AuthorName = _assistantId, ConversationId = threadId, @@ -218,10 +218,42 @@ public async IAsyncEnumerable GetStreamingResponseAsync( RawRepresentation = mcu, ResponseId = responseId, }; + + // Add any annotations from the text update. The OpenAI Assistants API does not support passing these back + // into the model (MessageContent.FromXx does not support providing annotations), so they end up being one way and are dropped + // on subsequent requests. + if (mcu.TextAnnotation is { } tau) + { + string? fileId = null; + string? toolName = null; + if (!string.IsNullOrWhiteSpace(tau.InputFileId)) + { + fileId = tau.InputFileId; + toolName = "file_search"; + } + else if (!string.IsNullOrWhiteSpace(tau.OutputFileId)) + { + fileId = tau.OutputFileId; + toolName = "code_interpreter"; + } + + if (fileId is not null) + { + (((TextContent)textUpdate.Contents[0]).Annotations ??= []).Add(new CitationAnnotation + { + RawRepresentation = tau, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = tau.StartIndex, EndIndex = tau.EndIndex }], + FileId = fileId, + ToolName = toolName, + }); + } + } + + yield return textUpdate; break; default: - yield return new ChatResponseUpdate + yield return new() { AuthorName = _assistantId, ConversationId = threadId, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index a70fcec2a8f..8a4e08cac9f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -480,6 +481,30 @@ internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompl returnMessage.Contents.Add(new ErrorContent(refusal) { ErrorCode = nameof(openAICompletion.Refusal) }); } + // And add annotations. OpenAI chat completion specifies annotations at the message level (and as such they can't be + // roundtripped back); we store them either on the first text content, assuming there is one, or on a dedicated content + // instance if not. + if (openAICompletion.Annotations is { Count: > 0 }) + { + TextContent? annotationContent = returnMessage.Contents.OfType().FirstOrDefault(); + if (annotationContent is null) + { + annotationContent = new(null); + returnMessage.Contents.Add(annotationContent); + } + + foreach (var annotation in openAICompletion.Annotations) + { + (annotationContent.Annotations ??= []).Add(new CitationAnnotation + { + RawRepresentation = annotation, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = annotation.StartIndex, EndIndex = annotation.EndIndex }], + Title = annotation.WebResourceTitle, + Url = annotation.WebResourceUri, + }); + } + } + // Wrap the content in a ChatResponse to return. var response = new ChatResponse(returnMessage) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index dfe0c50d374..5bd3529e292 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -608,10 +608,27 @@ private static List ToAIContents(IEnumerable con switch (part.Kind) { case ResponseContentPartKind.OutputText: - results.Add(new TextContent(part.Text) + TextContent text = new(part.Text) { RawRepresentation = part, - }); + }; + + if (part.OutputTextAnnotations is { Count: > 0 }) + { + foreach (var ota in part.OutputTextAnnotations) + { + (text.Annotations ??= []).Add(new CitationAnnotation + { + RawRepresentation = ota, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = ota.UriCitationStartIndex, EndIndex = ota.UriCitationEndIndex }], + Title = ota.UriCitationTitle, + Url = ota.UriCitationUri, + FileId = ota.FileCitationFileId ?? ota.FilePathFileId, + }); + } + } + + results.Add(text); break; case ResponseContentPartKind.Refusal: diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 2eb8db9b477..45a82542da8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -242,6 +242,48 @@ public async Task ToChatResponse_CoalescesTextContentAndTextReasoningContentSepa Assert.Equal("OP", Assert.IsType(message.Contents[7]).Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToChatResponse_DoesNotCoalesceAnnotatedContent(bool useAsync) + { + ChatResponseUpdate[] updates = + { + new(null, "A"), + new(null, "B"), + new(null, "C"), + new() { Contents = [new TextContent("D") { Annotations = [new()] }] }, + new() { Contents = [new TextContent("E") { Annotations = [new()] }] }, + new() { Contents = [new TextContent("F") { Annotations = [new()] }] }, + new() { Contents = [new TextContent("G") { Annotations = [] }] }, + new() { Contents = [new TextContent("H") { Annotations = [] }] }, + new() { Contents = [new TextContent("I") { Annotations = [new()] }] }, + new() { Contents = [new TextContent("J") { Annotations = [new()] }] }, + new(null, "K"), + new() { Contents = [new TextContent("L") { Annotations = [new()] }] }, + new(null, "M"), + new(null, "N"), + new() { Contents = [new TextContent("O") { Annotations = [new()] }] }, + new() { Contents = [new TextContent("P") { Annotations = [new()] }] }, + }; + + ChatResponse response = useAsync ? await YieldAsync(updates).ToChatResponseAsync() : updates.ToChatResponse(); + ChatMessage message = Assert.Single(response.Messages); + Assert.Equal(12, message.Contents.Count); + Assert.Equal("ABC", Assert.IsType(message.Contents[0]).Text); + Assert.Equal("D", Assert.IsType(message.Contents[1]).Text); + Assert.Equal("E", Assert.IsType(message.Contents[2]).Text); + Assert.Equal("F", Assert.IsType(message.Contents[3]).Text); + Assert.Equal("GH", Assert.IsType(message.Contents[4]).Text); + Assert.Equal("I", Assert.IsType(message.Contents[5]).Text); + Assert.Equal("J", Assert.IsType(message.Contents[6]).Text); + Assert.Equal("K", Assert.IsType(message.Contents[7]).Text); + Assert.Equal("L", Assert.IsType(message.Contents[8]).Text); + Assert.Equal("MN", Assert.IsType(message.Contents[9]).Text); + Assert.Equal("O", Assert.IsType(message.Contents[10]).Text); + Assert.Equal("P", Assert.IsType(message.Contents[11]).Text); + } + [Fact] public async Task ToChatResponse_UsageContentExtractedFromContents() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs index e69de29bb2d..2b4b23f0a72 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs @@ -0,0 +1,71 @@ +// 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 AIAnnotationTests +{ + [Fact] + public void Constructor_PropsDefault() + { + AIAnnotation a = new(); + Assert.Null(a.AdditionalProperties); + Assert.Null(a.RawRepresentation); + Assert.Null(a.AnnotatedRegions); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + AIAnnotation a = new(); + + Assert.Null(a.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + a.AdditionalProperties = props; + Assert.Same(props, a.AdditionalProperties); + + Assert.Null(a.AnnotatedRegions); + List regions = [new TextSpanAnnotatedRegion { StartIndex = 10, EndIndex = 42 }]; + a.AnnotatedRegions = regions; + Assert.Same(regions, a.AnnotatedRegions); + + Assert.Null(a.RawRepresentation); + object raw = new(); + a.RawRepresentation = raw; + Assert.Same(raw, a.RawRepresentation); + } + + [Fact] + public void Serialization_Roundtrips() + { + AIAnnotation original = new() + { + AdditionalProperties = new AdditionalPropertiesDictionary { { "key", "value" } }, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = 10, EndIndex = 42 }], + RawRepresentation = new object(), + }; + + string json = JsonSerializer.Serialize(original, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AIAnnotation))); + Assert.NotNull(json); + + var deserialized = (AIAnnotation?)JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AIAnnotation))); + Assert.NotNull(deserialized); + + Assert.NotNull(deserialized.AdditionalProperties); + Assert.Single(deserialized.AdditionalProperties); + Assert.Equal(JsonSerializer.Deserialize("\"value\"", AIJsonUtilities.DefaultOptions).ToString(), deserialized.AdditionalProperties["key"]!.ToString()); + + Assert.Null(deserialized.RawRepresentation); + + Assert.NotNull(deserialized.AnnotatedRegions); + TextSpanAnnotatedRegion? region = Assert.IsType(Assert.Single(deserialized.AnnotatedRegions)); + Assert.NotNull(region); + Assert.Equal(10, region.StartIndex); + Assert.Equal(42, region.EndIndex); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs new file mode 100644 index 00000000000..08097f3e05e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs @@ -0,0 +1,100 @@ +// 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 CitationAnnotationTests +{ + [Fact] + public void Constructor_PropsDefault() + { + CitationAnnotation a = new(); + Assert.Null(a.AdditionalProperties); + Assert.Null(a.AnnotatedRegions); + Assert.Null(a.RawRepresentation); + Assert.Null(a.Snippet); + Assert.Null(a.Title); + Assert.Null(a.ToolName); + Assert.Null(a.Url); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + CitationAnnotation a = new(); + + Assert.Null(a.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + a.AdditionalProperties = props; + Assert.Same(props, a.AdditionalProperties); + + Assert.Null(a.RawRepresentation); + object raw = new(); + a.RawRepresentation = raw; + Assert.Same(raw, a.RawRepresentation); + + Assert.Null(a.AnnotatedRegions); + List regions = [new TextSpanAnnotatedRegion { StartIndex = 10, EndIndex = 42 }]; + a.AnnotatedRegions = regions; + Assert.Same(regions, a.AnnotatedRegions); + + Assert.Null(a.Snippet); + a.Snippet = "snippet"; + Assert.Equal("snippet", a.Snippet); + + Assert.Null(a.Title); + a.Title = "title"; + Assert.Equal("title", a.Title); + + Assert.Null(a.ToolName); + a.ToolName = "toolName"; + Assert.Equal("toolName", a.ToolName); + + Assert.Null(a.Url); + Uri url = new("https://example.com"); + a.Url = url; + Assert.Same(url, a.Url); + } + + [Fact] + public void Serialization_Roundtrips() + { + CitationAnnotation original = new() + { + AdditionalProperties = new AdditionalPropertiesDictionary { { "key", "value" } }, + RawRepresentation = new object(), + Snippet = "snippet", + Title = "title", + ToolName = "toolName", + Url = new("https://example.com"), + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = 10, EndIndex = 42 }], + }; + + string json = JsonSerializer.Serialize(original, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(CitationAnnotation))); + Assert.NotNull(json); + + var deserialized = (CitationAnnotation?)JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(CitationAnnotation))); + Assert.NotNull(deserialized); + + Assert.NotNull(deserialized.AdditionalProperties); + Assert.Single(deserialized.AdditionalProperties); + Assert.Equal(JsonSerializer.Deserialize("\"value\"", AIJsonUtilities.DefaultOptions).ToString(), deserialized.AdditionalProperties["key"]!.ToString()); + + Assert.Null(deserialized.RawRepresentation); + Assert.Equal("snippet", deserialized.Snippet); + Assert.Equal("title", deserialized.Title); + Assert.Equal("toolName", deserialized.ToolName); + Assert.NotNull(deserialized.AnnotatedRegions); + TextSpanAnnotatedRegion region = Assert.IsType(Assert.Single(deserialized.AnnotatedRegions)); + Assert.Equal(10, region.StartIndex); + Assert.Equal(42, region.EndIndex); + + Assert.NotNull(deserialized.Url); + Assert.Equal(original.Url, deserialized.Url); + } +} 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 c2177486fea..acbb5515085 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -943,13 +943,14 @@ public static void AddAIContentType_DerivedAIContent() JsonSerializerOptions options = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine(AIJsonUtilities.DefaultOptions.TypeInfoResolver, JsonContext.Default), + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; options.AddAIContentType("derivativeContent"); AIContent c = new DerivedAIContent { DerivedValue = 42 }; string json = JsonSerializer.Serialize(c, options); - Assert.Equal("""{"$type":"derivativeContent","DerivedValue":42,"AdditionalProperties":null}""", json); + Assert.Equal("""{"$type":"derivativeContent","DerivedValue":42}""", json); AIContent? deserialized = JsonSerializer.Deserialize(json, options); Assert.IsType(deserialized); diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index c9f0e09abf3..5f7f3769d21 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -34,16 +34,16 @@ namespace Microsoft.Extensions.AI; public abstract class ChatClientIntegrationTests : IDisposable { - private readonly IChatClient? _chatClient; - protected ChatClientIntegrationTests() { - _chatClient = CreateChatClient(); + ChatClient = CreateChatClient(); } + protected IChatClient? ChatClient { get; } + public void Dispose() { - _chatClient?.Dispose(); + ChatClient?.Dispose(); GC.SuppressFinalize(this); } @@ -54,7 +54,7 @@ public virtual async Task GetResponseAsync_SingleRequestMessage() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync("What's the biggest animal?"); + var response = await ChatClient.GetResponseAsync("What's the biggest animal?"); Assert.Contains("whale", response.Text, StringComparison.OrdinalIgnoreCase); } @@ -64,7 +64,7 @@ public virtual async Task GetResponseAsync_MultipleRequestMessages() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync( + var response = await ChatClient.GetResponseAsync( [ new(ChatRole.User, "Pick a city, any city"), new(ChatRole.Assistant, "Seattle"), @@ -82,7 +82,7 @@ public virtual async Task GetResponseAsync_WithEmptyMessage() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync( + var response = await ChatClient.GetResponseAsync( [ new(ChatRole.System, []), new(ChatRole.User, []), @@ -104,7 +104,7 @@ public virtual async Task GetStreamingResponseAsync() ]; StringBuilder sb = new(); - await foreach (var chunk in _chatClient.GetStreamingResponseAsync(chatHistory)) + await foreach (var chunk in ChatClient.GetStreamingResponseAsync(chatHistory)) { sb.Append(chunk.Text); } @@ -119,7 +119,7 @@ public virtual async Task GetResponseAsync_UsageDataAvailable() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync("Explain in 10 words how AI works"); + var response = await ChatClient.GetResponseAsync("Explain in 10 words how AI works"); Assert.True(response.Usage?.InputTokenCount > 1); Assert.True(response.Usage?.OutputTokenCount > 1); @@ -131,7 +131,7 @@ public virtual async Task GetStreamingResponseAsync_UsageDataAvailable() { SkipIfNotEnabled(); - var response = _chatClient.GetStreamingResponseAsync("Explain in 10 words how AI works", new() + var response = ChatClient.GetStreamingResponseAsync("Explain in 10 words how AI works", new() { AdditionalProperties = new() { @@ -160,7 +160,7 @@ public virtual async Task GetStreamingResponseAsync_AppendToHistory() List history = [new(ChatRole.User, "Explain in 100 words how AI works")]; - var streamingResponse = _chatClient.GetStreamingResponseAsync(history); + var streamingResponse = ChatClient.GetStreamingResponseAsync(history); Assert.Single(history); await history.AddMessagesAsync(streamingResponse); @@ -179,7 +179,7 @@ public virtual async Task MultiModal_DescribeImage() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync( + var response = await ChatClient.GetResponseAsync( [ new(ChatRole.User, [ @@ -197,7 +197,7 @@ public virtual async Task MultiModal_DescribePdf() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync( + var response = await ChatClient.GetResponseAsync( [ new(ChatRole.User, [ @@ -223,7 +223,7 @@ public virtual async Task FunctionInvocation_AutomaticallyInvokeFunction_Paramet .Build(); using var chatClient = new FunctionInvokingChatClient( - new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + new OpenTelemetryChatClient(ChatClient, sourceName: sourceName)); int secretNumber = 42; @@ -246,7 +246,7 @@ public virtual async Task FunctionInvocation_AutomaticallyInvokeFunction_WithPar { SkipIfNotEnabled(); - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); var response = await chatClient.GetResponseAsync("What is the result of SecretComputation on 42 and 84?", new() { @@ -261,7 +261,7 @@ public virtual async Task FunctionInvocation_AutomaticallyInvokeFunction_WithPar { SkipIfNotEnabled(); - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); var response = chatClient.GetStreamingResponseAsync("What is the result of SecretComputation on 42 and 84?", new() { @@ -290,7 +290,7 @@ public virtual async Task FunctionInvocation_OptionalParameter() .Build(); using var chatClient = new FunctionInvokingChatClient( - new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + new OpenTelemetryChatClient(ChatClient, sourceName: sourceName)); int secretNumber = 42; @@ -322,7 +322,7 @@ public virtual async Task FunctionInvocation_NestedParameters() .Build(); using var chatClient = new FunctionInvokingChatClient( - new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + new OpenTelemetryChatClient(ChatClient, sourceName: sourceName)); int secretNumber = 42; @@ -354,7 +354,7 @@ public virtual async Task FunctionInvocation_ArrayParameter() .Build(); using var chatClient = new FunctionInvokingChatClient( - new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + new OpenTelemetryChatClient(ChatClient, sourceName: sourceName)); List messages = [ @@ -411,7 +411,7 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict) .Build(); using var chatClient = new FunctionInvokingChatClient( - new OpenTelemetryChatClient(_chatClient, sourceName: sourceName)); + new OpenTelemetryChatClient(ChatClient, sourceName: sourceName)); int methodCount = 1; Func createOptions = () => @@ -567,7 +567,7 @@ public virtual async Task FunctionInvocation_SupportsMultipleParallelRequests() throw new SkipTestException("Parallel function calling is not supported by this chat client"); } - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); // The service/model isn't guaranteed to request two calls to GetPersonAge in the same turn, but it's common that it will. var response = await chatClient.GetResponseAsync("How much older is Elsa than Anna? Return the age difference as a single number.", new() @@ -600,7 +600,7 @@ public virtual async Task FunctionInvocation_RequireAny() return 123; }, "GetSecretNumber"); - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); var response = await chatClient.GetResponseAsync("Are birds real?", new() { @@ -620,7 +620,7 @@ public virtual async Task FunctionInvocation_RequireSpecific() var getSecretNumberTool = AIFunctionFactory.Create(() => 123, "GetSecretNumber"); var shieldsUpTool = AIFunctionFactory.Create(() => shieldsUp = true, "ShieldsUp"); - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); // Even though the user doesn't ask for the shields to be activated, verify that the tool is invoked var response = await chatClient.GetResponseAsync("What's the current secret number?", new() @@ -638,9 +638,9 @@ public virtual async Task Caching_OutputVariesWithoutCaching() SkipIfNotEnabled(); var message = new ChatMessage(ChatRole.User, "Pick a random number, uniformly distributed between 1 and 1000000"); - var firstResponse = await _chatClient.GetResponseAsync([message]); + var firstResponse = await ChatClient.GetResponseAsync([message]); - var secondResponse = await _chatClient.GetResponseAsync([message]); + var secondResponse = await ChatClient.GetResponseAsync([message]); Assert.NotEqual(firstResponse.Text, secondResponse.Text); } @@ -650,7 +650,7 @@ public virtual async Task Caching_SamePromptResultsInCacheHit_NonStreaming() SkipIfNotEnabled(); using var chatClient = new DistributedCachingChatClient( - _chatClient, + ChatClient, new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))); var message = new ChatMessage(ChatRole.User, "Pick a random number, uniformly distributed between 1 and 1000000"); @@ -675,7 +675,7 @@ public virtual async Task Caching_SamePromptResultsInCacheHit_Streaming() SkipIfNotEnabled(); using var chatClient = new DistributedCachingChatClient( - _chatClient, + ChatClient, new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))); var message = new ChatMessage(ChatRole.User, "Pick a random number, uniformly distributed between 1 and 1000000"); @@ -957,7 +957,7 @@ public virtual async Task GetResponseAsync_StructuredOutput() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" Who is described in the following sentence? Jimbo Smith is a 35-year-old programmer from Cardiff, Wales. """); @@ -973,7 +973,7 @@ public virtual async Task GetResponseAsync_StructuredOutputArray() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" Who are described in the following sentence? Jimbo Smith is a 35-year-old software developer from Cardiff, Wales. Josh Simpson is a 25-year-old software developer from Newport, Wales. @@ -989,7 +989,7 @@ public virtual async Task GetResponseAsync_StructuredOutputInteger() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" There were 14 abstractions for AI programming, which was too many. To fix this we added another one. How many are there now? """); @@ -1002,7 +1002,7 @@ public virtual async Task GetResponseAsync_StructuredOutputString() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" The software developer, Jimbo Smith, is a 35-year-old from Cardiff, Wales. What's his full name? """); @@ -1015,7 +1015,7 @@ public virtual async Task GetResponseAsync_StructuredOutputBool_True() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" Jimbo Smith is a 35-year-old software developer from Cardiff, Wales. Is there at least one software developer from Cardiff? """); @@ -1028,7 +1028,7 @@ public virtual async Task GetResponseAsync_StructuredOutputBool_False() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" Jimbo Smith is a 35-year-old software developer from Cardiff, Wales. Reply true if the previous statement indicates that he is a medical doctor, otherwise false. """); @@ -1041,7 +1041,7 @@ public virtual async Task GetResponseAsync_StructuredOutputEnum() { SkipIfNotEnabled(); - var response = await _chatClient.GetResponseAsync(""" + var response = await ChatClient.GetResponseAsync(""" Taylor Swift is a famous singer and songwriter. What is her job? """); @@ -1061,7 +1061,7 @@ public virtual async Task GetResponseAsync_StructuredOutput_WithFunctions() Job = JobType.Programmer, }; - using var chatClient = new FunctionInvokingChatClient(_chatClient); + using var chatClient = new FunctionInvokingChatClient(ChatClient); var response = await chatClient.GetResponseAsync( "Who is person with ID 123?", new ChatOptions { @@ -1085,7 +1085,7 @@ public virtual async Task GetResponseAsync_StructuredOutput_NonNative() SkipIfNotEnabled(); var capturedOptions = new List(); - var captureOutputChatClient = _chatClient.AsBuilder() + var captureOutputChatClient = ChatClient.AsBuilder() .Use((messages, options, nextAsync, cancellationToken) => { capturedOptions.Add(options); @@ -1125,12 +1125,12 @@ private enum JobType Unknown, } - [MemberNotNull(nameof(_chatClient))] + [MemberNotNull(nameof(ChatClient))] protected void SkipIfNotEnabled() { string? skipIntegration = TestRunnerConfiguration.Instance["SkipIntegrationTests"]; - if (skipIntegration is not null || _chatClient is null) + if (skipIntegration is not null || ChatClient is null) { throw new SkipTestException("Client is not enabled."); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index 9d8f806ca8a..a794460a9bd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -18,7 +18,7 @@ internal static class IntegrationTestHelpers { var configuration = TestRunnerConfiguration.Instance; - string? apiKey = configuration["OpenAI:Key"]; + string? apiKey = configuration["AI:OpenAI:ApiKey"]; string? mode = configuration["OpenAI:Mode"]; if (string.Equals(mode, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index f8e835bdb81..630af9e34b0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Microsoft.TestUtilities; +using Xunit; namespace Microsoft.Extensions.AI; @@ -16,4 +20,33 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests // Test structure doesn't make sense with Respones. public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; + + [ConditionalFact] + public async Task UseWebSearch_AnnotationsReflectResults() + { + SkipIfNotEnabled(); + + var response = await ChatClient.GetResponseAsync( + "Write a paragraph about the three most recent blog posts on the .NET blog. Cite your sources.", + new() { Tools = [new HostedWebSearchTool()] }); + + ChatMessage m = Assert.Single(response.Messages); + TextContent tc = m.Contents.OfType().First(); + Assert.NotNull(tc.Annotations); + Assert.NotEmpty(tc.Annotations); + Assert.All(tc.Annotations, a => + { + CitationAnnotation ca = Assert.IsType(a); + var regions = Assert.IsType>(ca.AnnotatedRegions); + Assert.NotNull(regions); + Assert.Single(regions); + var region = Assert.IsType(regions[0]); + Assert.NotNull(region); + Assert.NotNull(region.StartIndex); + Assert.NotNull(region.EndIndex); + Assert.NotNull(ca.Url); + Assert.NotNull(ca.Title); + Assert.NotEmpty(ca.Title); + }); + } } From cb570950446b792c330c0dbb27edc23ab828d34f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 29 Jul 2025 11:04:19 -0400 Subject: [PATCH 228/472] Add FunctionInvokingChatClient.AdditionalTools (#6661) * Add FunctionInvokingChatClient.AdditionalTools> --- .../FunctionInvokingChatClient.cs | 52 ++++++++++--- .../Microsoft.Extensions.AI.json | 4 + .../FunctionInvokingChatClientTests.cs | 77 ++++++++++++++++++- .../Microsoft.Extensions.AI.Tests.csproj | 2 +- 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 0a8673dc91d..9c0506a2307 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -205,6 +205,16 @@ public int MaximumConsecutiveErrorsPerRequest set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0); } + /// Gets or sets a collection of additional tools the client is able to invoke. + /// + /// These will not impact the requests sent by the , which will pass through the + /// unmodified. However, if the inner client requests the invocation of a tool + /// that was not in , this collection will also be consulted + /// to look for a corresponding tool to invoke. This is useful when the service may have been pre-configured to be aware + /// of certain tools that aren't also sent on each individual request. + /// + public IList? AdditionalTools { get; set; } + /// Gets or sets a delegate used to invoke instances. /// /// By default, the protected method is called for each to be invoked, @@ -250,7 +260,7 @@ public override async Task GetResponseAsync( // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents. bool requiresFunctionInvocation = - options?.Tools is { Count: > 0 } && + (options?.Tools is { Count: > 0 } || AdditionalTools is { Count: > 0 }) && iteration < MaximumIterationsPerRequest && CopyFunctionCalls(response.Messages, ref functionCallContents); @@ -288,7 +298,7 @@ public override async Task GetResponseAsync( // Add the responses from the function calls into the augmented history and also into the tracked // list of response messages. - var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken); + var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken); responseMessages.AddRange(modeAndMessages.MessagesAdded); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; @@ -297,7 +307,7 @@ public override async Task GetResponseAsync( break; } - UpdateOptionsForNextIteration(ref options!, response.ConversationId); + UpdateOptionsForNextIteration(ref options, response.ConversationId); } Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages."); @@ -367,7 +377,7 @@ public override async IAsyncEnumerable GetStreamingResponseA // If there are no tools to call, or for any other reason we should stop, return the response. if (functionCallContents is not { Count: > 0 } || - options?.Tools is not { Count: > 0 } || + (options?.Tools is not { Count: > 0 } && AdditionalTools is not { Count: > 0 }) || iteration >= _maximumIterationsPerRequest) { break; @@ -535,9 +545,16 @@ private static bool CopyFunctionCalls( return any; } - private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? conversationId) + private static void UpdateOptionsForNextIteration(ref ChatOptions? options, string? conversationId) { - if (options.ToolMode is RequiredChatToolMode) + if (options is null) + { + if (conversationId is not null) + { + options = new() { ConversationId = conversationId }; + } + } + else if (options.ToolMode is RequiredChatToolMode) { // We have to reset the tool mode to be non-required after the first iteration, // as otherwise we'll be in an infinite loop. @@ -566,7 +583,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin /// The to monitor for cancellation requests. /// A value indicating how the caller should proceed. private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync( - List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, + List messages, ChatOptions? options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken) { // We must add a response for every tool call, regardless of whether we successfully executed it or not. @@ -695,13 +712,13 @@ private void ThrowIfNoFunctionResultsAdded(IList? messages) /// The to monitor for cancellation requests. /// A value indicating how the caller should proceed. private async Task ProcessFunctionCallAsync( - List messages, ChatOptions options, List callContents, + List messages, ChatOptions? options, List callContents, int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken) { var callContent = callContents[functionCallIndex]; // Look up the AIFunction for the function call. If the requested function isn't available, send back an error. - AIFunction? aiFunction = options.Tools!.OfType().FirstOrDefault(t => t.Name == callContent.Name); + AIFunction? aiFunction = FindAIFunction(options?.Tools, callContent.Name) ?? FindAIFunction(AdditionalTools, callContent.Name); if (aiFunction is null) { return new(terminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null); @@ -746,6 +763,23 @@ private async Task ProcessFunctionCallAsync( callContent, result, exception: null); + + static AIFunction? FindAIFunction(IList? tools, string functionName) + { + if (tools is not null) + { + int count = tools.Count; + for (int i = 0; i < count; i++) + { + if (tools[i] is AIFunction function && function.Name == functionName) + { + return function; + } + } + } + + return null; + } } /// Creates one or more response messages for function invocation results. diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index 59ed3d32fab..3e3f0426dd1 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -515,6 +515,10 @@ } ], "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.FunctionInvokingChatClient.AdditionalTools { get; set; }", + "Stage": "Stable" + }, { "Member": "bool Microsoft.Extensions.AI.FunctionInvokingChatClient.AllowConcurrentInvocation { get; set; }", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index b4ce2f1546c..08cb5ee5760 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -39,6 +39,7 @@ public void Ctor_HasExpectedDefaults() Assert.Equal(40, client.MaximumIterationsPerRequest); Assert.Equal(3, client.MaximumConsecutiveErrorsPerRequest); Assert.Null(client.FunctionInvoker); + Assert.Null(client.AdditionalTools); } [Fact] @@ -67,6 +68,11 @@ public void Properties_Roundtrip() Func> invoker = (ctx, ct) => new ValueTask("test"); client.FunctionInvoker = invoker; Assert.Same(invoker, client.FunctionInvoker); + + Assert.Null(client.AdditionalTools); + IList additionalTools = [AIFunctionFactory.Create(() => "Additional Tool")]; + client.AdditionalTools = additionalTools; + Assert.Same(additionalTools, client.AdditionalTools); } [Fact] @@ -99,6 +105,73 @@ public async Task SupportsSingleFunctionCallPerRequestAsync() await InvokeAndAssertStreamingAsync(options, plan); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SupportsToolsProvidedByAdditionalTools(bool provideOptions) + { + ChatOptions? options = provideOptions ? + new() { Tools = [AIFunctionFactory.Create(() => "Shouldn't be invoked", "ChatOptionsFunc")] } : + null; + + Func configure = builder => + builder.UseFunctionInvocation(configure: c => c.AdditionalTools = + [ + AIFunctionFactory.Create(() => "Result 1", "Func1"), + AIFunctionFactory.Create((int i) => $"Result 2: {i}", "Func2"), + AIFunctionFactory.Create((int i) => { }, "VoidReturn"), + ]); + + List plan = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId3", "VoidReturn", arguments: new Dictionary { { "i", 43 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Success: Function completed.")]), + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); + + await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure); + } + + [Fact] + public async Task PrefersToolsProvidedByChatOptions() + { + ChatOptions options = new() + { + Tools = [AIFunctionFactory.Create(() => "Result 1", "Func1")] + }; + + Func configure = builder => + builder.UseFunctionInvocation(configure: c => c.AdditionalTools = + [ + AIFunctionFactory.Create(() => "Should never be invoked", "Func1"), + AIFunctionFactory.Create((int i) => $"Result 2: {i}", "Func2"), + AIFunctionFactory.Create((int i) => { }, "VoidReturn"), + ]); + + List plan = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId3", "VoidReturn", arguments: new Dictionary { { "i", 43 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Success: Function completed.")]), + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); + + await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -1002,7 +1075,7 @@ public override void Post(SendOrPostCallback d, object? state) } private static async Task> InvokeAndAssertAsync( - ChatOptions options, + ChatOptions? options, List plan, List? expected = null, Func? configurePipeline = null, @@ -1102,7 +1175,7 @@ private static UsageDetails CreateRandomUsage() } private static async Task> InvokeAndAssertStreamingAsync( - ChatOptions options, + ChatOptions? options, List plan, List? expected = null, Func? configurePipeline = null, diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index e4f17abb179..c07f3056054 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -5,7 +5,7 @@ - $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 + $(NoWarn);CA1063;CA1861;S104;SA1130;VSTHRD003 $(NoWarn);MEAI001 true From edf7a946f3d77f50b30cc4a8496793adef460f65 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 29 Jul 2025 11:04:35 -0400 Subject: [PATCH 229/472] Fix unintentional test env var change (#6660) --- .../IntegrationTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index a794460a9bd..9d8f806ca8a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -18,7 +18,7 @@ internal static class IntegrationTestHelpers { var configuration = TestRunnerConfiguration.Instance; - string? apiKey = configuration["AI:OpenAI:ApiKey"]; + string? apiKey = configuration["OpenAI:Key"]; string? mode = configuration["OpenAI:Mode"]; if (string.Equals(mode, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)) From 65c679b342662c23f707a92a775eab38dfa7fff1 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 30 Jul 2025 23:01:52 -0400 Subject: [PATCH 230/472] Add more OpenAI conversion helpers (#6662) --- .../MicrosoftExtensionsAIChatExtensions.cs | 175 +++++++++++++ ...icrosoftExtensionsAIResponsesExtensions.cs | 24 ++ .../OpenAIAssistantsChatClient.cs | 24 +- .../OpenAIChatClient.cs | 59 +++-- .../OpenAIClientExtensions.cs | 22 ++ .../OpenAIResponsesChatClient.cs | 126 ++++----- .../OpenAIConversionTests.cs | 241 +++++++++++++++++- 7 files changed, 573 insertions(+), 98 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs index c7eb3b2e03e..13242a9b32f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -2,10 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; +#pragma warning disable S103 // Lines should not be too long + namespace OpenAI.Chat; /// Provides extension methods for working with content associated with OpenAI.Chat. @@ -21,13 +28,181 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) => /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. /// A sequence of OpenAI chat messages. + /// is . public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages) => OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null); + /// Creates an OpenAI from a . + /// The to convert to a . + /// A converted . + /// is . + public static ChatCompletion AsOpenAIChatCompletion(this ChatResponse response) + { + _ = Throw.IfNull(response); + + if (response.RawRepresentation is ChatCompletion chatCompletion) + { + return chatCompletion; + } + + var lastMessage = response.Messages.LastOrDefault(); + + ChatMessageRole role = lastMessage?.Role.Value switch + { + "user" => ChatMessageRole.User, + "function" => ChatMessageRole.Function, + "tool" => ChatMessageRole.Tool, + "developer" => ChatMessageRole.Developer, + "system" => ChatMessageRole.System, + _ => ChatMessageRole.Assistant, + }; + + ChatFinishReason finishReason = response.FinishReason?.Value switch + { + "length" => ChatFinishReason.Length, + "content_filter" => ChatFinishReason.ContentFilter, + "tool_calls" => ChatFinishReason.ToolCalls, + "function_call" => ChatFinishReason.FunctionCall, + _ => ChatFinishReason.Stop, + }; + + ChatTokenUsage usage = OpenAIChatModelFactory.ChatTokenUsage( + (int?)response.Usage?.OutputTokenCount ?? 0, + (int?)response.Usage?.InputTokenCount ?? 0, + (int?)response.Usage?.TotalTokenCount ?? 0); + + IEnumerable? toolCalls = lastMessage?.Contents + .OfType().Select(c => ChatToolCall.CreateFunctionToolCall(c.CallId, c.Name, + new BinaryData(JsonSerializer.SerializeToUtf8Bytes(c.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))))); + + return OpenAIChatModelFactory.ChatCompletion( + response.ResponseId, + finishReason, + new(OpenAIChatClient.ToOpenAIChatContent(lastMessage?.Contents ?? [])), + toolCalls: toolCalls, + role: role, + createdAt: response.CreatedAt ?? default, + model: response.ModelId, + usage: usage, + outputAudio: lastMessage?.Contents.OfType().Where(dc => dc.HasTopLevelMediaType("audio")).Select(a => OpenAIChatModelFactory.ChatOutputAudio(new(a.Data))).FirstOrDefault(), + messageAnnotations: ConvertAnnotations(lastMessage?.Contents)); + + static IEnumerable ConvertAnnotations(IEnumerable? contents) + { + if (contents is null) + { + yield break; + } + + foreach (var content in contents) + { + if (content.Annotations is null) + { + continue; + } + + foreach (var annotation in content.Annotations) + { + if (annotation is not CitationAnnotation citation) + { + continue; + } + + if (citation.AnnotatedRegions?.OfType().ToArray() is { Length: > 0 } regions) + { + foreach (var region in regions) + { + yield return OpenAIChatModelFactory.ChatMessageAnnotation(region.StartIndex ?? 0, region.EndIndex ?? 0, citation.Url, citation.Title); + } + } + else + { + yield return OpenAIChatModelFactory.ChatMessageAnnotation(0, 0, citation.Url, citation.Title); + } + } + } + } + } + + /// Creates a sequence of instances from the specified input messages. + /// The input messages to convert. + /// A sequence of Microsoft.Extensions.AI chat messages. + /// is . + public static IEnumerable AsChatMessages(this IEnumerable messages) + { + _ = Throw.IfNull(messages); + + foreach (var message in messages) + { + Microsoft.Extensions.AI.ChatMessage resultMessage = new() + { + RawRepresentation = message, + }; + + switch (message) + { + case AssistantChatMessage acm: + resultMessage.AuthorName = acm.ParticipantName; + OpenAIChatClient.ConvertContentParts(acm.Content, resultMessage.Contents); + foreach (var toolCall in acm.ToolCalls) + { + var fcc = OpenAIClientExtensions.ParseCallContent(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName); + fcc.RawRepresentation = toolCall; + resultMessage.Contents.Add(fcc); + } + + break; + + case UserChatMessage ucm: + resultMessage.AuthorName = ucm.ParticipantName; + OpenAIChatClient.ConvertContentParts(ucm.Content, resultMessage.Contents); + break; + + case DeveloperChatMessage dcm: + resultMessage.AuthorName = dcm.ParticipantName; + OpenAIChatClient.ConvertContentParts(dcm.Content, resultMessage.Contents); + break; + + case SystemChatMessage scm: + resultMessage.AuthorName = scm.ParticipantName; + OpenAIChatClient.ConvertContentParts(scm.Content, resultMessage.Contents); + break; + + case ToolChatMessage tcm: + resultMessage.Contents.Add(new FunctionResultContent(tcm.ToolCallId, ToToolResult(tcm.Content)) + { + RawRepresentation = tcm, + }); + + static object ToToolResult(ChatMessageContent content) + { + if (content.Count == 1 && content[0] is { Text: { } text }) + { + return text; + } + + MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms, new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + foreach (IJsonModel part in content) + { + part.Write(writer, ModelReaderWriterOptions.Json); + } + + return JsonSerializer.Deserialize(ms.GetBuffer().AsSpan(0, (int)ms.Position), AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!; + } + + break; + } + + yield return resultMessage; + } + } + /// Creates a Microsoft.Extensions.AI from a . /// The to convert to a . /// The options employed in the creation of the response. /// A converted . + /// is . public static ChatResponse AsChatResponse(this ChatCompletion chatCompletion, ChatCompletionOptions? options = null) => OpenAIChatClient.FromOpenAIChatCompletion(Throw.IfNull(chatCompletion), options); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index e54b3092c5a..8f39ad7852e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -21,13 +21,37 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. /// A sequence of OpenAI response items. + /// is . public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages) => OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages)); + /// Creates a sequence of instances from the specified input items. + /// The input messages to convert. + /// A sequence of instances. + /// is . + public static IEnumerable AsChatMessages(this IEnumerable items) => + OpenAIResponsesChatClient.ToChatMessages(Throw.IfNull(items)); + /// Creates a Microsoft.Extensions.AI from an . /// The to convert to a . /// The options employed in the creation of the response. /// A converted . + /// is . public static ChatResponse AsChatResponse(this OpenAIResponse response, ResponseCreationOptions? options = null) => OpenAIResponsesChatClient.FromOpenAIResponse(Throw.IfNull(response), options); + + /// Creates an OpenAI from a . + /// The response to convert. + /// The created . + internal static OpenAIResponse AsOpenAIResponse(this ChatResponse response) // Implement and make public once OpenAIResponse can be constructed external to the OpenAI library. + { + _ = Throw.IfNull(response); + + if (response.RawRepresentation is OpenAIResponse openAIResponse) + { + return openAIResponse; + } + + throw new NotSupportedException(); + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index c437a553d28..bca2802460c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -199,11 +199,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName) { - ruUpdate.Contents.Add( - new FunctionCallContent( - JsonSerializer.Serialize([ru.Value.Id, toolCallId], OpenAIJsonContext.Default.StringArray), - functionName, - JsonSerializer.Deserialize(rau.FunctionArguments, OpenAIJsonContext.Default.IDictionaryStringObject)!)); + var fcc = OpenAIClientExtensions.ParseCallContent( + rau.FunctionArguments, + JsonSerializer.Serialize([ru.Value.Id, toolCallId], OpenAIJsonContext.Default.StringArray), + functionName); + fcc.RawRepresentation = ru; + ruUpdate.Contents.Add(fcc); } yield return ruUpdate; @@ -440,6 +441,10 @@ void AppendSystemInstructions(string? toAppend) { switch (content) { + case AIContent when content.RawRepresentation is MessageContent rawRep: + messageContents.Add(rawRep); + break; + case TextContent text: messageContents.Add(MessageContent.FromText(text.Text)); break; @@ -448,18 +453,9 @@ void AppendSystemInstructions(string? toAppend) messageContents.Add(MessageContent.FromImageUri(image.Uri)); break; - // Assistants doesn't support data URIs. - //case DataContent image when image.HasTopLevelMediaType("image"): - // messageContents.Add(MessageContent.FromImageUri(new Uri(image.Uri))); - // break; - case FunctionResultContent result: (functionResults ??= []).Add(result); break; - - case AIContent when content.RawRepresentation is MessageContent rawRep: - messageContents.Add(rawRep); - break; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 8a4e08cac9f..7173046ccac 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -129,6 +129,12 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op foreach (ChatMessage input in inputs) { + if (input.RawRepresentation is OpenAI.Chat.ChatMessage raw) + { + yield return raw; + continue; + } + if (input.Role == ChatRole.System || input.Role == ChatRole.User || input.Role == OpenAIClientExtensions.ChatRoleDeveloper) @@ -219,15 +225,22 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op } /// Converts a list of to a list of . - private static List ToOpenAIChatContent(IList contents) + internal static List ToOpenAIChatContent(IEnumerable contents) { List parts = []; foreach (var content in contents) { - if (ToChatMessageContentPart(content) is { } part) + if (content.RawRepresentation is ChatMessageContentPart raw) { - parts.Add(part); + parts.Add(raw); + } + else + { + if (ToChatMessageContentPart(content) is { } part) + { + parts.Add(part); + } } } @@ -243,6 +256,9 @@ private static List ToOpenAIChatContent(IList { switch (content) { + case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: + return rawContentPart; + case TextContent textContent: return ChatMessageContentPart.CreateTextPart(textContent.Text); @@ -267,9 +283,6 @@ private static List ToOpenAIChatContent(IList case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf"); - - case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: - return rawContentPart; } return null; @@ -328,13 +341,7 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha // Transfer over content update items. if (update.ContentUpdate is { Count: > 0 }) { - foreach (ChatMessageContentPart contentPart in update.ContentUpdate) - { - if (ToAIContent(contentPart) is AIContent aiContent) - { - responseUpdate.Contents.Add(aiContent); - } - } + ConvertContentParts(update.ContentUpdate, responseUpdate.Contents); } if (update.OutputAudioUpdate is { } audioUpdate) @@ -402,7 +409,7 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha FunctionCallInfo fci = entry.Value; if (!string.IsNullOrWhiteSpace(fci.Name)) { - var callContent = ParseCallContentFromJsonString( + var callContent = OpenAIClientExtensions.ParseCallContent( fci.Arguments?.ToString() ?? string.Empty, fci.CallId!, fci.Name!); @@ -468,7 +475,7 @@ internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompl { if (!string.IsNullOrWhiteSpace(toolCall.FunctionName)) { - var callContent = ParseCallContentFromBinaryData(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName); + var callContent = OpenAIClientExtensions.ParseCallContent(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName); callContent.RawRepresentation = toolCall; returnMessage.Contents.Add(callContent); @@ -648,6 +655,20 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => _ => new ChatRole(role.ToString()), }; + /// Creates s from . + /// The content parts to convert into a content. + /// The result collection into which to write the resulting content. + internal static void ConvertContentParts(ChatMessageContent content, IList results) + { + foreach (ChatMessageContentPart contentPart in content) + { + if (ToAIContent(contentPart) is { } aiContent) + { + results.Add(aiContent); + } + } + } + /// Creates an from a . /// The content part to convert into a content. /// The constructed , or if the content part could not be converted. @@ -697,14 +718,6 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) => _ => new ChatFinishReason(s), }; - private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) => - FunctionCallContent.CreateFromParsedArguments(json, callId, name, - argumentParser: static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); - - private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8Json, string callId, string name) => - FunctionCallContent.CreateFromParsedArguments(ut8Json, callId, name, - argumentParser: static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); - /// POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates. private sealed class FunctionCallInfo { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 889998cc933..3881f246d98 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -184,6 +184,28 @@ internal static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, boo return functionParameters; } + /// Creates a new instance of parsing arguments using a specified encoding and parser. + /// The input arguments to be parsed. + /// The function call ID. + /// The function name. + /// A new instance of containing the parse result. + /// is . + /// is . + internal static FunctionCallContent ParseCallContent(string json, string callId, string name) => + FunctionCallContent.CreateFromParsedArguments(json, callId, name, + static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); + + /// Creates a new instance of parsing arguments using a specified encoding and parser. + /// The input arguments to be parsed. + /// The function call ID. + /// The function name. + /// A new instance of containing the parse result. + /// is . + /// is . + internal static FunctionCallContent ParseCallContent(BinaryData utf8json, string callId, string name) => + FunctionCallContent.CreateFromParsedArguments(utf8json, callId, name, + static utf8json => JsonSerializer.Deserialize(utf8json, OpenAIJsonContext.Default.IDictionaryStringObject)!); + /// Used to create the JSON payload for an OpenAI tool description. internal sealed class ToolJson { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 5bd3529e292..40e2ee048fd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -90,7 +90,6 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R ConversationId = openAIOptions?.StoredOutputEnabled is false ? null : openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), - Messages = [new(ChatRole.Assistant, [])], ModelId = openAIResponse.Model, RawRepresentation = openAIResponse, ResponseId = openAIResponse.Id, @@ -109,65 +108,69 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R if (openAIResponse.OutputItems is not null) { - ChatMessage message = response.Messages[0]; - Debug.Assert(message.Contents is List, "Expected a List for message contents."); + response.Messages = [.. ToChatMessages(openAIResponse.OutputItems)]; - foreach (ResponseItem outputItem in openAIResponse.OutputItems) + if (response.Messages.LastOrDefault() is { } lastMessage && openAIResponse.Error is { } error) { - switch (outputItem) - { - case MessageResponseItem messageItem: - if (message.MessageId is not null && message.MessageId != messageItem.Id) - { - message = new ChatMessage(); - response.Messages.Add(message); - } + lastMessage.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() }); + } - message.MessageId = messageItem.Id; - message.RawRepresentation = messageItem; - message.Role = ToChatRole(messageItem.Role); - ((List)message.Contents).AddRange(ToAIContents(messageItem.Content)); - break; + foreach (var message in response.Messages) + { + message.CreatedAt ??= openAIResponse.CreatedAt; + } + } - case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary && !string.IsNullOrWhiteSpace(summary): - message.Contents.Add(new TextReasoningContent(summary) - { - RawRepresentation = reasoningItem - }); - break; + return response; + } - case FunctionCallResponseItem functionCall: - response.FinishReason ??= ChatFinishReason.ToolCalls; - var fcc = FunctionCallContent.CreateFromParsedArguments( - functionCall.FunctionArguments.ToMemory(), - functionCall.CallId, - functionCall.FunctionName, - static json => JsonSerializer.Deserialize(json.Span, OpenAIJsonContext.Default.IDictionaryStringObject)!); - fcc.RawRepresentation = outputItem; - message.Contents.Add(fcc); - break; + internal static IEnumerable ToChatMessages(IEnumerable items) + { + ChatMessage? message = null; - default: - message.Contents.Add(new() - { - RawRepresentation = outputItem, - }); - break; - } - } + foreach (ResponseItem outputItem in items) + { + message ??= new(ChatRole.Assistant, (string?)null); - if (openAIResponse.Error is { } error) + switch (outputItem) { - message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() }); + case MessageResponseItem messageItem: + if (message.MessageId is not null && message.MessageId != messageItem.Id) + { + yield return message; + message = new ChatMessage(); + } + + message.MessageId = messageItem.Id; + message.RawRepresentation = messageItem; + message.Role = ToChatRole(messageItem.Role); + ((List)message.Contents).AddRange(ToAIContents(messageItem.Content)); + break; + + case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary: + message.Contents.Add(new TextReasoningContent(summary) { RawRepresentation = reasoningItem }); + break; + + case FunctionCallResponseItem functionCall: + var fcc = OpenAIClientExtensions.ParseCallContent(functionCall.FunctionArguments, functionCall.CallId, functionCall.FunctionName); + fcc.RawRepresentation = outputItem; + message.Contents.Add(fcc); + break; + + case FunctionCallOutputResponseItem functionCallOutputItem: + message.Contents.Add(new FunctionResultContent(functionCallOutputItem.CallId, functionCallOutputItem.FunctionOutput) { RawRepresentation = functionCallOutputItem }); + break; + + default: + message.Contents.Add(new() { RawRepresentation = outputItem }); + break; } } - foreach (var message in response.Messages) + if (message is not null) { - message.CreatedAt = openAIResponse.CreatedAt; + yield return message; } - - return response; } /// @@ -266,15 +269,14 @@ public async IAsyncEnumerable GetStreamingResponseAsync( { _ = functionCallInfos.Remove(functionCallOutputDoneUpdate.OutputIndex); - var fci = FunctionCallContent.CreateFromParsedArguments( + var fcc = OpenAIClientExtensions.ParseCallContent( callInfo.Arguments?.ToString() ?? string.Empty, callInfo.ResponseItem.CallId, - callInfo.ResponseItem.FunctionName, - static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!); + callInfo.ResponseItem.FunctionName); lastMessageId = callInfo.ResponseItem.Id; lastRole = ChatRole.Assistant; - yield return new ChatResponseUpdate(lastRole, [fci]) + yield return new ChatResponseUpdate(lastRole, [fcc]) { ConversationId = conversationId, CreatedAt = createdAt, @@ -513,6 +515,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable ToOpenAIResponseItems(IEnumerable ToOpenAIResponseItems(IEnumerable))))); break; - - case AIContent when item.RawRepresentation is ResponseItem rawRep: - yield return rawRep; - break; } } @@ -659,6 +665,10 @@ private static List ToOpenAIResponsesContent(IList ToOpenAIResponsesContent(IList(message.Contents[1]).Uri.ToString()); Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); } + + [Fact] + public void AsChatMessages_FromOpenAIChatMessages_ProducesExpectedOutput() + { + Assert.Throws("messages", () => ((IEnumerable)null!).AsChatMessages().ToArray()); + + List openAIMessages = + [ + new SystemChatMessage("You are a helpful assistant."), + new UserChatMessage("Hello"), + new AssistantChatMessage(ChatMessageContentPart.CreateTextPart("Hi there!")), + new ToolChatMessage("call456", "Function output") + ]; + + var convertedMessages = openAIMessages.AsChatMessages().ToArray(); + + Assert.Equal(4, convertedMessages.Length); + + Assert.Equal("You are a helpful assistant.", convertedMessages[0].Text); + Assert.Equal("Hello", convertedMessages[1].Text); + Assert.Equal("Hi there!", convertedMessages[2].Text); + Assert.Equal("Function output", convertedMessages[3].Contents.OfType().First().Result); + } + + [Fact] + public void AsChatMessages_FromResponseItems_WithNullArgument_ThrowsArgumentNullException() + { + Assert.Throws("items", () => ((IEnumerable)null!).AsChatMessages()); + } + + [Fact] + public void AsChatMessages_FromResponseItems_ProducesExpectedOutput() + { + List inputMessages = + [ + new(ChatRole.Assistant, "Hi there!") + ]; + + var responseItems = inputMessages.AsOpenAIResponseItems().ToArray(); + + var convertedMessages = responseItems.AsChatMessages().ToArray(); + + Assert.Single(convertedMessages); + + var message = convertedMessages[0]; + Assert.Equal(ChatRole.Assistant, message.Role); + Assert.Equal("Hi there!", message.Text); + } + + [Fact] + public void AsChatMessages_FromResponseItems_WithEmptyCollection_ReturnsEmptyCollection() + { + var convertedMessages = Array.Empty().AsChatMessages().ToArray(); + Assert.Empty(convertedMessages); + } + + [Fact] + public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly() + { + List inputMessages = + [ + new(ChatRole.Assistant, + [ + new TextContent("I'll call a function."), + new FunctionCallContent("call123", "TestFunction", new Dictionary { ["param"] = "value" }) + ]) + ]; + + var responseItems = inputMessages.AsOpenAIResponseItems().ToArray(); + var convertedMessages = responseItems.AsChatMessages().ToArray(); + + Assert.Single(convertedMessages); + + var message = convertedMessages[0]; + Assert.Equal(ChatRole.Assistant, message.Role); + + var textContent = message.Contents.OfType().FirstOrDefault(); + var functionCall = message.Contents.OfType().FirstOrDefault(); + + Assert.NotNull(textContent); + Assert.Equal("I'll call a function.", textContent.Text); + + Assert.NotNull(functionCall); + Assert.Equal("call123", functionCall.CallId); + Assert.Equal("TestFunction", functionCall.Name); + Assert.Equal("value", functionCall.Arguments!["param"]?.ToString()); + } + + [Fact] + public void AsOpenAIChatCompletion_WithNullArgument_ThrowsArgumentNullException() + { + Assert.Throws("response", () => ((ChatResponse)null!).AsOpenAIChatCompletion()); + } + + [Fact] + public void AsOpenAIChatCompletion_WithMultipleContents_ProducesValidInstance() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, + [ + new TextContent("Here's an image and some text."), + new UriContent("https://example.com/image.jpg", "image/jpeg"), + new DataContent(new byte[] { 1, 2, 3, 4 }, "application/octet-stream") + ])) + { + ResponseId = "multi-content-response", + ModelId = "gpt-4-vision", + FinishReason = ChatFinishReason.Stop, + CreatedAt = new DateTimeOffset(2025, 1, 3, 14, 30, 0, TimeSpan.Zero), + Usage = new UsageDetails + { + InputTokenCount = 25, + OutputTokenCount = 12, + TotalTokenCount = 37 + } + }; + + ChatCompletion completion = chatResponse.AsOpenAIChatCompletion(); + + Assert.Equal("multi-content-response", completion.Id); + Assert.Equal("gpt-4-vision", completion.Model); + Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, completion.FinishReason); + Assert.Equal(ChatMessageRole.Assistant, completion.Role); + Assert.Equal(new DateTimeOffset(2025, 1, 3, 14, 30, 0, TimeSpan.Zero), completion.CreatedAt); + + Assert.NotNull(completion.Usage); + Assert.Equal(25, completion.Usage.InputTokenCount); + Assert.Equal(12, completion.Usage.OutputTokenCount); + Assert.Equal(37, completion.Usage.TotalTokenCount); + + Assert.NotEmpty(completion.Content); + Assert.Contains(completion.Content, c => c.Text == "Here's an image and some text."); + } + + [Fact] + public void AsOpenAIChatCompletion_WithEmptyData_HandlesGracefully() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello")); + var completion = chatResponse.AsOpenAIChatCompletion(); + + Assert.NotNull(completion); + Assert.Equal(ChatMessageRole.Assistant, completion.Role); + Assert.Equal("Hello", Assert.Single(completion.Content).Text); + Assert.Empty(completion.ToolCalls); + + var emptyResponse = new ChatResponse([]); + var emptyCompletion = emptyResponse.AsOpenAIChatCompletion(); + Assert.NotNull(emptyCompletion); + Assert.Equal(ChatMessageRole.Assistant, emptyCompletion.Role); + } + + [Fact] + public void AsOpenAIChatCompletion_WithComplexFunctionCallArguments_SerializesCorrectly() + { + var complexArgs = new Dictionary + { + ["simpleString"] = "hello", + ["number"] = 42, + ["boolean"] = true, + ["nullValue"] = null, + ["nestedObject"] = new Dictionary + { + ["innerString"] = "world", + ["innerArray"] = new[] { 1, 2, 3 } + } + }; + + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, + [ + new TextContent("I'll process this complex data."), + new FunctionCallContent("process_data", "ProcessComplexData", complexArgs) + ])) + { + ResponseId = "complex-function-call", + ModelId = "gpt-4", + FinishReason = ChatFinishReason.ToolCalls + }; + + ChatCompletion completion = chatResponse.AsOpenAIChatCompletion(); + + Assert.Equal("complex-function-call", completion.Id); + Assert.Equal(OpenAI.Chat.ChatFinishReason.ToolCalls, completion.FinishReason); + + var toolCall = Assert.Single(completion.ToolCalls); + Assert.Equal("process_data", toolCall.Id); + Assert.Equal("ProcessComplexData", toolCall.FunctionName); + + var deserializedArgs = JsonSerializer.Deserialize>(toolCall.FunctionArguments.ToMemory().Span); + Assert.NotNull(deserializedArgs); + Assert.Equal("hello", deserializedArgs["simpleString"]?.ToString()); + Assert.Equal(42, ((JsonElement)deserializedArgs["number"]!).GetInt32()); + Assert.True(((JsonElement)deserializedArgs["boolean"]!).GetBoolean()); + Assert.Null(deserializedArgs["nullValue"]); + + var nestedObj = (JsonElement)deserializedArgs["nestedObject"]!; + Assert.Equal("world", nestedObj.GetProperty("innerString").GetString()); + Assert.Equal(3, nestedObj.GetProperty("innerArray").GetArrayLength()); + } + + [Fact] + public void AsOpenAIChatCompletion_WithDifferentFinishReasons_MapsCorrectly() + { + var testCases = new[] + { + (ChatFinishReason.Stop, OpenAI.Chat.ChatFinishReason.Stop), + (ChatFinishReason.Length, OpenAI.Chat.ChatFinishReason.Length), + (ChatFinishReason.ContentFilter, OpenAI.Chat.ChatFinishReason.ContentFilter), + (ChatFinishReason.ToolCalls, OpenAI.Chat.ChatFinishReason.ToolCalls) + }; + + foreach (var (inputFinishReason, expectedOpenAIFinishReason) in testCases) + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test")) + { + FinishReason = inputFinishReason + }; + + var completion = chatResponse.AsOpenAIChatCompletion(); + Assert.Equal(expectedOpenAIFinishReason, completion.FinishReason); + } + } + + [Fact] + public void AsOpenAIChatCompletion_WithDifferentRoles_MapsCorrectly() + { + var testCases = new[] + { + (ChatRole.Assistant, ChatMessageRole.Assistant), + (ChatRole.User, ChatMessageRole.User), + (ChatRole.System, ChatMessageRole.System), + (ChatRole.Tool, ChatMessageRole.Tool) + }; + + foreach (var (inputRole, expectedOpenAIRole) in testCases) + { + var chatResponse = new ChatResponse(new ChatMessage(inputRole, "Test")); + var completion = chatResponse.AsOpenAIChatCompletion(); + Assert.Equal(expectedOpenAIRole, completion.Role); + } + } } From 55f44e897fa54f3864bfdf5c64c82a0d3687c6bb Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 30 Jul 2025 21:07:14 -0700 Subject: [PATCH 231/472] Add OriginalRepoCommitHash to assemblies (#6667) --- Directory.Build.targets | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Directory.Build.targets b/Directory.Build.targets index 5fcf797523c..31b130899b1 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -59,6 +59,13 @@ + + + <_Parameter1>OriginalRepoCommitHash + <_Parameter2>$(RepoOriginalSourceRevisionId) + + + From c9584a40813de2f70b2e8ab9a03048c3b5089422 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Jul 2025 17:26:51 +0300 Subject: [PATCH 232/472] Include a trivial items keyword if missing. (#6669) --- .../Utilities/AIJsonUtilities.Schema.Create.cs | 7 +++++++ .../Utilities/AIJsonUtilitiesTests.cs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index c77e7dffb5b..17e5e4d5353 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -14,6 +14,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Schema; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using Microsoft.Shared.Diagnostics; @@ -289,6 +290,12 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js objSchema.InsertAtStart(TypePropertyName, "string"); } + // Include a trivial items keyword if missing + if (ctx.TypeInfo.Kind is JsonTypeInfoKind.Enumerable && !objSchema.ContainsKey(ItemsPropertyName)) + { + objSchema.Add(ItemsPropertyName, new JsonObject()); + } + // 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. // In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error. 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 acbb5515085..bcec2981c5b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -170,6 +170,21 @@ public static void CreateJsonSchema_DefaultParameters_GeneratesExpectedJsonSchem AssertDeepEquals(expected, actual); } + [Fact] + public static void CreateJsonSchema_TrivialArray_GeneratesExpectedJsonSchema() + { + JsonElement expected = JsonDocument.Parse(""" + { + "type": "array", + "items": {} + } + """).RootElement; + + JsonElement actual = AIJsonUtilities.CreateJsonSchema(typeof(object[]), serializerOptions: JsonContext.Default.Options); + + AssertDeepEquals(expected, actual); + } + [Fact] public static void CreateJsonSchema_OverriddenParameters_GeneratesExpectedJsonSchema() { @@ -1326,6 +1341,7 @@ private class DerivedAIContent : AIContent [JsonSerializable(typeof(DerivedAIContent))] [JsonSerializable(typeof(MyPoco))] [JsonSerializable(typeof(MyEnumValue?))] + [JsonSerializable(typeof(object[]))] private partial class JsonContext : JsonSerializerContext; private static bool DeepEquals(JsonElement element1, JsonElement element2) From 9267e19f9cd8de418721947a3cdc08419a5d1a6f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Jul 2025 20:54:45 +0300 Subject: [PATCH 233/472] Add resolution of function parameter level data annotation attributes. (#6671) * Add resolution of function parameter level data annotation attributes. * Update test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs Co-authored-by: Stephen Toub --------- Co-authored-by: Stephen Toub --- .../AIJsonUtilities.Schema.Create.cs | 53 +++++++++++-------- .../Utilities/AIJsonUtilitiesTests.cs | 20 +++++++ 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index 17e5e4d5353..da0124639de 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -113,7 +113,7 @@ public static JsonElement CreateFunctionJsonSchema( JsonNode parameterSchema = CreateJsonSchemaCore( type: parameter.ParameterType, - parameterName: parameter.Name, + parameter: parameter, description: parameter.GetCustomAttribute(inherit: true)?.Description, hasDefaultValue: parameter.HasDefaultValue, defaultValue: GetDefaultValueNormalized(parameter), @@ -178,7 +178,7 @@ public static JsonElement CreateJsonSchema( { serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; - JsonNode schema = CreateJsonSchemaCore(type, parameterName: null, description, hasDefaultValue, defaultValue, serializerOptions, inferenceOptions); + JsonNode schema = CreateJsonSchemaCore(type, parameter: null, description, hasDefaultValue, defaultValue, serializerOptions, inferenceOptions); // Finally, apply any schema transformations if specified. if (inferenceOptions.TransformOptions is { } options) @@ -208,7 +208,7 @@ internal static void ValidateSchemaDocument(JsonElement document, [CallerArgumen #endif private static JsonNode CreateJsonSchemaCore( Type? type, - string? parameterName, + ParameterInfo? parameter, string? description, bool hasDefaultValue, object? defaultValue, @@ -272,14 +272,14 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js // The resulting schema might be a $ref using a pointer to a different location in the document. // As JSON pointer doesn't support relative paths, parameter schemas need to fix up such paths // to accommodate the fact that they're being nested inside of a higher-level schema. - if (parameterName is not null && objSchema.TryGetPropertyValue(RefPropertyName, out JsonNode? paramName)) + if (parameter?.Name is not null && objSchema.TryGetPropertyValue(RefPropertyName, out JsonNode? paramName)) { // Fix up any $ref URIs to match the path from the root document. string refUri = paramName!.GetValue(); Debug.Assert(refUri is "#" || refUri.StartsWith("#/", StringComparison.Ordinal), $"Expected {nameof(refUri)} to be either # or start with #/, got {refUri}"); refUri = refUri == "#" - ? $"#/{PropertiesPropertyName}/{parameterName}" - : $"#/{PropertiesPropertyName}/{parameterName}/{refUri.AsMemory("#/".Length)}"; + ? $"#/{PropertiesPropertyName}/{parameter.Name}" + : $"#/{PropertiesPropertyName}/{parameter.Name}/{refUri.AsMemory("#/".Length)}"; objSchema[RefPropertyName] = (JsonNode)refUri; } @@ -359,7 +359,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js ConvertSchemaToObject(ref schema).InsertAtStart(SchemaPropertyName, (JsonNode)SchemaKeywordUri); } - ApplyDataAnnotations(parameterName, ref schema, ctx); + ApplyDataAnnotations(ref schema, ctx); // Finally, apply any user-defined transformations if specified. if (inferenceOptions.TransformSchemaNode is { } transformer) @@ -389,30 +389,30 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema) } } - void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSchemaCreateContext ctx) + void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx) { - if (ctx.GetCustomAttribute() is { } displayNameAttribute) + if (ResolveAttribute() is { } displayNameAttribute) { ConvertSchemaToObject(ref schema)[TitlePropertyName] ??= displayNameAttribute.DisplayName; } #if NET || NETFRAMEWORK - if (ctx.GetCustomAttribute() is { } emailAttribute) + if (ResolveAttribute() is { } emailAttribute) { ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "email"; } - if (ctx.GetCustomAttribute() is { } urlAttribute) + if (ResolveAttribute() is { } urlAttribute) { ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "uri"; } - if (ctx.GetCustomAttribute() is { } regexAttribute) + if (ResolveAttribute() is { } regexAttribute) { ConvertSchemaToObject(ref schema)[PatternPropertyName] ??= regexAttribute.Pattern; } - if (ctx.GetCustomAttribute() is { } stringLengthAttribute) + if (ResolveAttribute() is { } stringLengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); @@ -424,7 +424,7 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche obj[MaxLengthStringPropertyName] ??= stringLengthAttribute.MaximumLength; } - if (ctx.GetCustomAttribute() is { } minLengthAttribute) + if (ResolveAttribute() is { } minLengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") @@ -437,7 +437,7 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche } } - if (ctx.GetCustomAttribute() is { } maxLengthAttribute) + if (ResolveAttribute() is { } maxLengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") @@ -450,7 +450,7 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche } } - if (ctx.GetCustomAttribute() is { } rangeAttribute) + if (ResolveAttribute() is { } rangeAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); @@ -521,12 +521,12 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche #endif #if NET - if (ctx.GetCustomAttribute() is { } base64Attribute) + if (ResolveAttribute() is { } base64Attribute) { ConvertSchemaToObject(ref schema)[ContentEncodingPropertyName] ??= "base64"; } - if (ctx.GetCustomAttribute() is { } lengthAttribute) + if (ResolveAttribute() is { } lengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); @@ -550,7 +550,7 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche } } - if (ctx.GetCustomAttribute() is { } allowedValuesAttribute) + if (ResolveAttribute() is { } allowedValuesAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); if (!obj.ContainsKey(EnumPropertyName)) @@ -562,7 +562,7 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche } } - if (ctx.GetCustomAttribute() is { } deniedValuesAttribute) + if (ResolveAttribute() is { } deniedValuesAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); @@ -597,7 +597,7 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali return enumArray; } - if (ctx.GetCustomAttribute() is { } dataTypeAttribute) + if (ResolveAttribute() is { } dataTypeAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); switch (dataTypeAttribute.DataType) @@ -629,6 +629,17 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali } } #endif + TAttribute? ResolveAttribute() + where TAttribute : Attribute + { + // If this is the root schema, check for any parameter attributes first. + if (ctx.Path.IsEmpty && parameter?.GetCustomAttribute(inherit: true) is TAttribute attr) + { + return attr; + } + + return ctx.GetCustomAttribute(inherit: true); + } } } } 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 bcec2981c5b..11926e5132e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -403,6 +403,26 @@ public enum MyEnumValue B = 2 } + [Fact] + public static void CreateFunctionJsonSchema_ReadsParameterDataAnnotationAttributes() + { + JsonSerializerOptions options = new(AIJsonUtilities.DefaultOptions) { NumberHandling = JsonNumberHandling.AllowReadingFromString }; + AIFunction func = AIFunctionFactory.Create(([Range(1, 10)] int num, [StringLength(100, MinimumLength = 1)] string str) => num + str.Length, serializerOptions: options); + + using JsonDocument expectedSchema = JsonDocument.Parse(""" + { + "type":"object", + "properties": { + "num": { "type":"integer", "minimum": 1, "maximum": 10 }, + "str": { "type":"string", "minLength": 1, "maxLength": 100 } + }, + "required":["num","str"] + } + """); + + AssertDeepEquals(expectedSchema.RootElement, func.JsonSchema); + } + [Fact] public static void CreateJsonSchema_CanBeBoolean() { From 6de2ba433c997fae0bcdc4b28370f4d40ad7016b Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 31 Jul 2025 14:02:04 -0700 Subject: [PATCH 234/472] Fix issue with NetSourceIndexStage1 for dependency conflict versions (#6672) --- .../Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj b/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj index aa1d5b37fbc..f9212aeacf8 100644 --- a/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj +++ b/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj @@ -31,6 +31,7 @@ + From 11e16a92e3b5ba3980bb36f0b0fa8896e3ad76f3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 31 Jul 2025 17:03:53 -0400 Subject: [PATCH 235/472] Expose streaming conversion utility methods (#6636) We're already exposing the non-streaming response conversions. Expose the streaming ones as well. --- .../MicrosoftExtensionsAIChatExtensions.cs | 112 ++++- ...icrosoftExtensionsAIResponsesExtensions.cs | 19 +- .../OpenAIChatClient.cs | 2 +- .../OpenAIResponsesChatClient.cs | 27 +- .../OpenAIConversionTests.cs | 418 +++++++++++++++++- 5 files changed, 535 insertions(+), 43 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs index 13242a9b32f..0385d318842 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -6,13 +6,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; -#pragma warning disable S103 // Lines should not be too long - namespace OpenAI.Chat; /// Provides extension methods for working with content associated with OpenAI.Chat. @@ -27,10 +28,10 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) => /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. + /// The options employed while processing . /// A sequence of OpenAI chat messages. - /// is . - public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages) => - OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null); + public static IEnumerable AsOpenAIChatMessages(this IEnumerable messages, ChatOptions? options = null) => + OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), options); /// Creates an OpenAI from a . /// The to convert to a . @@ -47,24 +48,9 @@ public static ChatCompletion AsOpenAIChatCompletion(this ChatResponse response) var lastMessage = response.Messages.LastOrDefault(); - ChatMessageRole role = lastMessage?.Role.Value switch - { - "user" => ChatMessageRole.User, - "function" => ChatMessageRole.Function, - "tool" => ChatMessageRole.Tool, - "developer" => ChatMessageRole.Developer, - "system" => ChatMessageRole.System, - _ => ChatMessageRole.Assistant, - }; + ChatMessageRole role = ToChatMessageRole(lastMessage?.Role); - ChatFinishReason finishReason = response.FinishReason?.Value switch - { - "length" => ChatFinishReason.Length, - "content_filter" => ChatFinishReason.ContentFilter, - "tool_calls" => ChatFinishReason.ToolCalls, - "function_call" => ChatFinishReason.FunctionCall, - _ => ChatFinishReason.Stop, - }; + ChatFinishReason finishReason = ToChatFinishReason(response.FinishReason); ChatTokenUsage usage = OpenAIChatModelFactory.ChatTokenUsage( (int?)response.Usage?.OutputTokenCount ?? 0, @@ -124,6 +110,52 @@ static IEnumerable ConvertAnnotations(IEnumerable + /// Creates a sequence of OpenAI instances from the specified + /// sequence of instances. + /// + /// The update instances. + /// The to monitor for cancellation requests. The default is . + /// A sequence of converted instances. + /// is . + public static async IAsyncEnumerable AsOpenAIStreamingChatCompletionUpdatesAsync( + this IAsyncEnumerable responseUpdates, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(responseUpdates); + + await foreach (var update in responseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (update.RawRepresentation is StreamingChatCompletionUpdate streamingUpdate) + { + yield return streamingUpdate; + continue; + } + + var usage = update.Contents.FirstOrDefault(c => c is UsageContent) is UsageContent usageContent ? + OpenAIChatModelFactory.ChatTokenUsage( + (int?)usageContent.Details.OutputTokenCount ?? 0, + (int?)usageContent.Details.InputTokenCount ?? 0, + (int?)usageContent.Details.TotalTokenCount ?? 0) : + null; + + var toolCallUpdates = update.Contents.OfType().Select((fcc, index) => + OpenAIChatModelFactory.StreamingChatToolCallUpdate( + index, fcc.CallId, ChatToolCallKind.Function, fcc.Name, + new(JsonSerializer.SerializeToUtf8Bytes(fcc.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))))) + .ToList(); + + yield return OpenAIChatModelFactory.StreamingChatCompletionUpdate( + update.ResponseId, + new(OpenAIChatClient.ToOpenAIChatContent(update.Contents)), + toolCallUpdates: toolCallUpdates, + role: ToChatMessageRole(update.Role), + finishReason: ToChatFinishReason(update.FinishReason), + createdAt: update.CreatedAt ?? default, + model: update.ModelId, + usage: usage); + } + } + /// Creates a sequence of instances from the specified input messages. /// The input messages to convert. /// A sequence of Microsoft.Extensions.AI chat messages. @@ -205,4 +237,40 @@ static object ToToolResult(ChatMessageContent content) /// is . public static ChatResponse AsChatResponse(this ChatCompletion chatCompletion, ChatCompletionOptions? options = null) => OpenAIChatClient.FromOpenAIChatCompletion(Throw.IfNull(chatCompletion), options); + + /// + /// Creates a sequence of Microsoft.Extensions.AI instances from the specified + /// sequence of OpenAI instances. + /// + /// The update instances. + /// The options employed in the creation of the response. + /// The to monitor for cancellation requests. The default is . + /// A sequence of converted instances. + /// is . + public static IAsyncEnumerable AsChatResponseUpdatesAsync( + this IAsyncEnumerable chatCompletionUpdates, ChatCompletionOptions? options = null, CancellationToken cancellationToken = default) => + OpenAIChatClient.FromOpenAIStreamingChatCompletionAsync(Throw.IfNull(chatCompletionUpdates), options, cancellationToken); + + /// Converts the to a . + private static ChatMessageRole ToChatMessageRole(ChatRole? role) => + role?.Value switch + { + "user" => ChatMessageRole.User, + "function" => ChatMessageRole.Function, + "tool" => ChatMessageRole.Tool, + "developer" => ChatMessageRole.Developer, + "system" => ChatMessageRole.System, + _ => ChatMessageRole.Assistant, + }; + + /// Converts the to a . + private static ChatFinishReason ToChatFinishReason(Microsoft.Extensions.AI.ChatFinishReason? finishReason) => + finishReason?.Value switch + { + "length" => ChatFinishReason.Length, + "content_filter" => ChatFinishReason.ContentFilter, + "tool_calls" => ChatFinishReason.ToolCalls, + "function_call" => ChatFinishReason.FunctionCall, + _ => ChatFinishReason.Stop, + }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 8f39ad7852e..083b4057d7d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -20,10 +21,11 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. + /// The options employed while processing . /// A sequence of OpenAI response items. /// is . - public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages) => - OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages)); + public static IEnumerable AsOpenAIResponseItems(this IEnumerable messages, ChatOptions? options = null) => + OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages), options); /// Creates a sequence of instances from the specified input items. /// The input messages to convert. @@ -40,6 +42,19 @@ public static IEnumerable AsChatMessages(this IEnumerable OpenAIResponsesChatClient.FromOpenAIResponse(Throw.IfNull(response), options); + /// + /// Creates a sequence of Microsoft.Extensions.AI instances from the specified + /// sequence of OpenAI instances. + /// + /// The update instances. + /// The options employed in the creation of the response. + /// The to monitor for cancellation requests. The default is . + /// A sequence of converted instances. + /// is . + public static IAsyncEnumerable AsChatResponseUpdatesAsync( + this IAsyncEnumerable responseUpdates, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default) => + OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken); + /// Creates an OpenAI from a . /// The response to convert. /// The created . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 7173046ccac..1a56452e332 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -303,7 +303,7 @@ internal static List ToOpenAIChatContent(IEnumerable FromOpenAIStreamingChatCompletionAsync( + internal static async IAsyncEnumerable FromOpenAIStreamingChatCompletionAsync( IAsyncEnumerable updates, ChatCompletionOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 40e2ee048fd..b1d4b010b99 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -72,7 +72,7 @@ public async Task GetResponseAsync( _ = Throw.IfNull(messages); // Convert the inputs into what OpenAIResponseClient expects. - var openAIResponseItems = ToOpenAIResponseItems(messages); + var openAIResponseItems = ToOpenAIResponseItems(messages, options); var openAIOptions = ToOpenAIResponseCreationOptions(options); // Make the call to the OpenAIResponseClient. @@ -174,16 +174,22 @@ internal static IEnumerable ToChatMessages(IEnumerable - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); - // Convert the inputs into what OpenAIResponseClient expects. - var openAIResponseItems = ToOpenAIResponseItems(messages); + var openAIResponseItems = ToOpenAIResponseItems(messages, options); var openAIOptions = ToOpenAIResponseCreationOptions(options); - // Make the call to the OpenAIResponseClient and process the streaming results. + var streamingUpdates = _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken); + + return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken); + } + + internal static async IAsyncEnumerable FromOpenAIStreamingResponseUpdatesAsync( + IAsyncEnumerable streamingResponseUpdates, ResponseCreationOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { DateTimeOffset? createdAt = null; string? responseId = null; string? conversationId = null; @@ -192,14 +198,15 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ChatRole? lastRole = null; Dictionary outputIndexToMessages = []; Dictionary? functionCallInfos = null; - await foreach (var streamingUpdate in _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken).ConfigureAwait(false)) + + await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) { switch (streamingUpdate) { case StreamingResponseCreatedUpdate createdUpdate: createdAt = createdUpdate.Response.CreatedAt; responseId = createdUpdate.Response.Id; - conversationId = openAIOptions.StoredOutputEnabled is false ? null : responseId; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; modelId = createdUpdate.Response.Model; goto default; @@ -485,8 +492,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt } /// Convert a sequence of s to s. - internal static IEnumerable ToOpenAIResponseItems(IEnumerable inputs) + internal static IEnumerable ToOpenAIResponseItems(IEnumerable inputs, ChatOptions? options) { + _ = options; // currently unused + foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 46a3c8ee8a0..79b8148a040 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using OpenAI.Assistants; using OpenAI.Chat; using OpenAI.Realtime; @@ -77,8 +78,10 @@ private static void ValidateSchemaParameters(BinaryData parameters) Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString()); } - [Fact] - public void AsOpenAIChatMessages_ProducesExpectedOutput() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) { Assert.Throws("messages", () => ((IEnumerable)null!).AsOpenAIChatMessages()); @@ -99,17 +102,31 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput() new(ChatRole.Assistant, "The answer is 42."), ]; - var convertedMessages = messages.AsOpenAIChatMessages().ToArray(); + ChatOptions? options = withOptions ? new ChatOptions { Instructions = "You talk like a parrot." } : null; + + var convertedMessages = messages.AsOpenAIChatMessages(options).ToArray(); + + int index = 0; + if (withOptions) + { + Assert.Equal(6, convertedMessages.Length); - Assert.Equal(5, convertedMessages.Length); + index = 1; + SystemChatMessage instructionsMessage = Assert.IsType(convertedMessages[0]); + Assert.Equal("You talk like a parrot.", Assert.Single(instructionsMessage.Content).Text); + } + else + { + Assert.Equal(5, convertedMessages.Length); + } - SystemChatMessage m0 = Assert.IsType(convertedMessages[0]); + SystemChatMessage m0 = Assert.IsType(convertedMessages[index]); Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text); - UserChatMessage m1 = Assert.IsType(convertedMessages[1]); + UserChatMessage m1 = Assert.IsType(convertedMessages[index + 1]); Assert.Equal("Hello", Assert.Single(m1.Content).Text); - AssistantChatMessage m2 = Assert.IsType(convertedMessages[2]); + AssistantChatMessage m2 = Assert.IsType(convertedMessages[index + 2]); Assert.Single(m2.Content); Assert.Equal("Hi there!", m2.Content[0].Text); var tc = Assert.Single(m2.ToolCalls); @@ -121,11 +138,11 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput() ["param2"] = 42 }), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span))); - ToolChatMessage m3 = Assert.IsType(convertedMessages[3]); + ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3]); Assert.Equal("callid123", m3.ToolCallId); Assert.Equal("theresult", Assert.Single(m3.Content).Text); - AssistantChatMessage m4 = Assert.IsType(convertedMessages[4]); + AssistantChatMessage m4 = Assert.IsType(convertedMessages[index + 4]); Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text); } @@ -217,6 +234,70 @@ public void AsChatResponse_ConvertsOpenAIChatCompletion() Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); } + [Fact] + public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates() + { + Assert.Throws("chatCompletionUpdates", () => ((IAsyncEnumerable)null!).AsChatResponseUpdatesAsync()); + + List updates = []; + await foreach (var update in CreateUpdates().AsChatResponseUpdatesAsync()) + { + updates.Add(update); + } + + ChatResponse response = updates.ToChatResponse(); + + Assert.Equal("id", response.ResponseId); + Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason); + Assert.Equal("model123", response.ModelId); + Assert.Equal(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), response.CreatedAt); + Assert.NotNull(response.Usage); + Assert.Equal(1, response.Usage.InputTokenCount); + Assert.Equal(2, response.Usage.OutputTokenCount); + Assert.Equal(3, response.Usage.TotalTokenCount); + + ChatMessage message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.Assistant, message.Role); + + Assert.Equal(3, message.Contents.Count); + Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0]).Text); + Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1]).Uri.ToString()); + Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); + + static async IAsyncEnumerable CreateUpdates() + { + await Task.Yield(); + yield return OpenAIChatModelFactory.StreamingChatCompletionUpdate( + "id", + new ChatMessageContent( + ChatMessageContentPart.CreateTextPart("Hello, world!"), + ChatMessageContentPart.CreateImagePart(new Uri("http://example.com/image.png"))), + null, + [OpenAIChatModelFactory.StreamingChatToolCallUpdate(0, "id", ChatToolCallKind.Function, "functionName", BinaryData.FromString("test"))], + ChatMessageRole.Assistant, + null, null, null, OpenAI.Chat.ChatFinishReason.ToolCalls, new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + "model123", null, OpenAIChatModelFactory.ChatTokenUsage(2, 1, 3)); + } + } + + [Fact] + public void AsChatResponse_ConvertsOpenAIResponse() + { + Assert.Throws("response", () => ((OpenAIResponse)null!).AsChatResponse()); + + // The OpenAI library currently doesn't provide any way to create an OpenAIResponse instance, + // as all constructors/factory methods currently are internal. Update this test when such functionality is available. + } + + [Fact] + public void AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates() + { + Assert.Throws("responseUpdates", () => ((IAsyncEnumerable)null!).AsChatResponseUpdatesAsync()); + + // The OpenAI library currently doesn't provide any way to create a StreamingResponseUpdate instance, + // as all constructors/factory methods currently are internal. Update this test when such functionality is available. + } + [Fact] public void AsChatMessages_FromOpenAIChatMessages_ProducesExpectedOutput() { @@ -455,4 +536,323 @@ public void AsOpenAIChatCompletion_WithDifferentRoles_MapsCorrectly() Assert.Equal(expectedOpenAIRole, completion.Role); } } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithNullArgument_ThrowsArgumentNullException() + { + var asyncEnumerable = ((IAsyncEnumerable)null!).AsOpenAIStreamingChatCompletionUpdatesAsync(); + await Assert.ThrowsAsync(async () => await asyncEnumerable.GetAsyncEnumerator().MoveNextAsync()); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithEmptyCollection_ReturnsEmptySequence() + { + var updates = new List(); + var result = new List(); + + await foreach (var update in CreateAsyncEnumerable(updates).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Empty(result); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithRawRepresentation_ReturnsOriginal() + { + var originalUpdate = OpenAIChatModelFactory.StreamingChatCompletionUpdate( + "test-id", + new ChatMessageContent(ChatMessageContentPart.CreateTextPart("Hello")), + role: ChatMessageRole.Assistant, + finishReason: OpenAI.Chat.ChatFinishReason.Stop, + createdAt: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + model: "gpt-3.5-turbo"); + + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Hello") + { + RawRepresentation = originalUpdate + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + Assert.Same(originalUpdate, result[0]); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithTextContent_CreatesValidUpdate() + { + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Hello, world!") + { + ResponseId = "response-123", + MessageId = "message-456", + ModelId = "gpt-4", + FinishReason = ChatFinishReason.Stop, + CreatedAt = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero) + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + var streamingUpdate = result[0]; + + Assert.Equal("response-123", streamingUpdate.CompletionId); + Assert.Equal("gpt-4", streamingUpdate.Model); + Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, streamingUpdate.FinishReason); + Assert.Equal(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero), streamingUpdate.CreatedAt); + Assert.Equal(ChatMessageRole.Assistant, streamingUpdate.Role); + Assert.Equal("Hello, world!", Assert.Single(streamingUpdate.ContentUpdate).Text); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithUsageContent_CreatesUpdateWithUsage() + { + var responseUpdate = new ChatResponseUpdate + { + ResponseId = "response-123", + Contents = + [ + new UsageContent(new UsageDetails + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30 + }) + ] + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + var streamingUpdate = result[0]; + + Assert.Equal("response-123", streamingUpdate.CompletionId); + Assert.NotNull(streamingUpdate.Usage); + Assert.Equal(20, streamingUpdate.Usage.OutputTokenCount); + Assert.Equal(10, streamingUpdate.Usage.InputTokenCount); + Assert.Equal(30, streamingUpdate.Usage.TotalTokenCount); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithFunctionCallContent_CreatesUpdateWithToolCalls() + { + var functionCallContent = new FunctionCallContent("call-123", "GetWeather", new Dictionary + { + ["location"] = "Seattle", + ["units"] = "celsius" + }); + + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, [functionCallContent]) + { + ResponseId = "response-123" + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + var streamingUpdate = result[0]; + + Assert.Equal("response-123", streamingUpdate.CompletionId); + Assert.Single(streamingUpdate.ToolCallUpdates); + + var toolCallUpdate = streamingUpdate.ToolCallUpdates[0]; + Assert.Equal(0, toolCallUpdate.Index); + Assert.Equal("call-123", toolCallUpdate.ToolCallId); + Assert.Equal(ChatToolCallKind.Function, toolCallUpdate.Kind); + Assert.Equal("GetWeather", toolCallUpdate.FunctionName); + + var deserializedArgs = JsonSerializer.Deserialize>( + toolCallUpdate.FunctionArgumentsUpdate.ToMemory().Span); + Assert.Equal("Seattle", deserializedArgs?["location"]?.ToString()); + Assert.Equal("celsius", deserializedArgs?["units"]?.ToString()); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleFunctionCalls_CreatesCorrectIndexes() + { + var functionCall1 = new FunctionCallContent("call-1", "Function1", new Dictionary { ["param1"] = "value1" }); + var functionCall2 = new FunctionCallContent("call-2", "Function2", new Dictionary { ["param2"] = "value2" }); + + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, [functionCall1, functionCall2]) + { + ResponseId = "response-123" + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + var streamingUpdate = result[0]; + + Assert.Equal(2, streamingUpdate.ToolCallUpdates.Count); + + Assert.Equal(0, streamingUpdate.ToolCallUpdates[0].Index); + Assert.Equal("call-1", streamingUpdate.ToolCallUpdates[0].ToolCallId); + Assert.Equal("Function1", streamingUpdate.ToolCallUpdates[0].FunctionName); + + Assert.Equal(1, streamingUpdate.ToolCallUpdates[1].Index); + Assert.Equal("call-2", streamingUpdate.ToolCallUpdates[1].ToolCallId); + Assert.Equal("Function2", streamingUpdate.ToolCallUpdates[1].FunctionName); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMixedContent_IncludesAllContent() + { + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, + [ + new TextContent("Processing your request..."), + new FunctionCallContent("call-123", "GetWeather", new Dictionary { ["location"] = "Seattle" }), + new UsageContent(new UsageDetails { TotalTokenCount = 50 }) + ]) + { + ResponseId = "response-123", + ModelId = "gpt-4" + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + var streamingUpdate = result[0]; + + Assert.Equal("response-123", streamingUpdate.CompletionId); + Assert.Equal("gpt-4", streamingUpdate.Model); + + // Should have text content + Assert.Contains(streamingUpdate.ContentUpdate, c => c.Text == "Processing your request..."); + + // Should have tool call + Assert.Single(streamingUpdate.ToolCallUpdates); + Assert.Equal("call-123", streamingUpdate.ToolCallUpdates[0].ToolCallId); + + // Should have usage + Assert.NotNull(streamingUpdate.Usage); + Assert.Equal(50, streamingUpdate.Usage.TotalTokenCount); + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithDifferentRoles_MapsCorrectly() + { + var testCases = new[] + { + (ChatRole.Assistant, ChatMessageRole.Assistant), + (ChatRole.User, ChatMessageRole.User), + (ChatRole.System, ChatMessageRole.System), + (ChatRole.Tool, ChatMessageRole.Tool) + }; + + foreach (var (inputRole, expectedOpenAIRole) in testCases) + { + var responseUpdate = new ChatResponseUpdate(inputRole, "Test message"); + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + Assert.Equal(expectedOpenAIRole, result[0].Role); + } + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithDifferentFinishReasons_MapsCorrectly() + { + var testCases = new[] + { + (ChatFinishReason.Stop, OpenAI.Chat.ChatFinishReason.Stop), + (ChatFinishReason.Length, OpenAI.Chat.ChatFinishReason.Length), + (ChatFinishReason.ContentFilter, OpenAI.Chat.ChatFinishReason.ContentFilter), + (ChatFinishReason.ToolCalls, OpenAI.Chat.ChatFinishReason.ToolCalls) + }; + + foreach (var (inputFinishReason, expectedOpenAIFinishReason) in testCases) + { + var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Test") + { + FinishReason = inputFinishReason + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Single(result); + Assert.Equal(expectedOpenAIFinishReason, result[0].FinishReason); + } + } + + [Fact] + public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleUpdates_ProcessesAllCorrectly() + { + var updates = new[] + { + new ChatResponseUpdate(ChatRole.Assistant, "Hello, ") + { + ResponseId = "response-123", + MessageId = "message-1" + + // No FinishReason set - null + }, + new ChatResponseUpdate(ChatRole.Assistant, "world!") + { + ResponseId = "response-123", + MessageId = "message-1", + FinishReason = ChatFinishReason.Stop + } + }; + + var result = new List(); + await foreach (var update in CreateAsyncEnumerable(updates).AsOpenAIStreamingChatCompletionUpdatesAsync()) + { + result.Add(update); + } + + Assert.Equal(2, result.Count); + + Assert.Equal("response-123", result[0].CompletionId); + Assert.Equal("Hello, ", Assert.Single(result[0].ContentUpdate).Text); + + // The ToChatFinishReason method defaults null to Stop + Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, result[0].FinishReason); + + Assert.Equal("response-123", result[1].CompletionId); + Assert.Equal("world!", Assert.Single(result[1].ContentUpdate).Text); + Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, result[1].FinishReason); + } + + private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source) + { + foreach (var item in source) + { + await Task.Yield(); + yield return item; + } + } } From 3f277e460be81274b09d82eb089de0b2ae78f40c Mon Sep 17 00:00:00 2001 From: Shyam N Date: Thu, 31 Jul 2025 14:30:47 -0700 Subject: [PATCH 236/472] Couple of fixes for MEAI.Evaluation (#6673) * Truncate long metric values in trends table * Simplify metadata for ContentSafetyChatClient --- .../TypeScript/components/MetricCard.tsx | 13 ++++--- .../ContentSafetyChatClient.cs | 35 ++++--------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx index 5f770298bf4..18476474697 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/MetricCard.tsx @@ -242,9 +242,14 @@ export const MetricDisplay = ({ metric }: { metric: MetricWithNoValue | NumericM const classes = useCardStyles(); const { fg, bg } = useCardColors(metric.interpretation); - const pillClass = mergeClasses( - bg, - classes.metricPill, + const pillClass = mergeClasses(bg, classes.metricPill); + const valueClass = mergeClasses(fg, classes.metricValueText); + + return ( + +
    + {metricValue} +
    +
    ); - return (
    {metricValue}
    ); }; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs index a8c1ba889d2..60d87cf700d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs @@ -20,7 +20,8 @@ namespace Microsoft.Extensions.AI.Evaluation.Safety; internal sealed class ContentSafetyChatClient : IChatClient { - private const string Moniker = "Azure AI Foundry Evaluation"; + private const string ProviderName = "azure.ai.foundry"; + private const string ModelId = $"{ProviderName}.evaluation"; private readonly ContentSafetyService _service; private readonly IChatClient? _originalChatClient; @@ -35,38 +36,14 @@ public ContentSafetyChatClient( ChatClientMetadata? originalMetadata = _originalChatClient?.GetService(); - string providerName; - Uri? providerUri = originalMetadata?.ProviderUri; - - if (contentSafetyServiceConfiguration.IsHubBasedProject) - { - providerName = - $"{Moniker} (" + - $"Subscription: {contentSafetyServiceConfiguration.SubscriptionId}, " + - $"Resource Group: {contentSafetyServiceConfiguration.ResourceGroupName}, " + - $"Project: {contentSafetyServiceConfiguration.ProjectName})"; - } - else - { - providerName = $"{Moniker} (Endpoint: {contentSafetyServiceConfiguration.Endpoint})"; - providerUri = contentSafetyServiceConfiguration.Endpoint; - } - + string providerName = ProviderName; if (originalMetadata?.ProviderName is string originalProviderName && !string.IsNullOrWhiteSpace(originalProviderName)) { providerName = $"{providerName}; {originalProviderName}"; } - string modelId = Moniker; - - if (originalMetadata?.DefaultModelId is string originalModelId && - !string.IsNullOrWhiteSpace(originalModelId)) - { - modelId = $"{modelId}; {originalModelId}"; - } - - _metadata = new ChatClientMetadata(providerName, providerUri, modelId); + _metadata = new ChatClientMetadata(providerName, defaultModelId: ModelId); } public async Task GetResponseAsync( @@ -88,7 +65,7 @@ await _service.AnnotateAsync( return new ChatResponse(new ChatMessage(ChatRole.Assistant, annotationResult)) { - ModelId = Moniker + ModelId = ModelId }; } else @@ -121,7 +98,7 @@ await _service.AnnotateAsync( yield return new ChatResponseUpdate(ChatRole.Assistant, annotationResult) { - ModelId = Moniker + ModelId = ModelId }; } else From c6529a0a68989cc881e4add4872c344917bc1ca9 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 1 Aug 2025 13:52:08 -0700 Subject: [PATCH 237/472] Fix one more version conflict on the docs transport package (#6675) --- .../Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj b/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj index f9212aeacf8..41e9d3b63bd 100644 --- a/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj +++ b/src/Packages/Microsoft.Internal.Extensions.DotNetApiDocs.Transport/Microsoft.Internal.Extensions.DotNetApiDocs.Transport.proj @@ -32,6 +32,7 @@ and some others depending on the 9.0 version. --> +
    From d494f06e36affa83f5d216ddfe0bef2eb3f37c0d Mon Sep 17 00:00:00 2001 From: Shyam N Date: Sat, 2 Aug 2025 01:09:18 -0700 Subject: [PATCH 238/472] Introduce metadata to identify built-in evaluation metrics (#6674) --- ...ft.Extensions.AI.Evaluation.Console.csproj | 5 ++ .../BLEUEvaluator.cs | 1 + .../F1Evaluator.cs | 1 + .../GLEUEvaluator.cs | 1 + ...rosoft.Extensions.AI.Evaluation.NLP.csproj | 9 +-- .../CoherenceEvaluator.cs | 1 + .../CompletenessEvaluator.cs | 1 + .../EquivalenceEvaluator.cs | 1 + .../FluencyEvaluator.cs | 1 + .../GroundednessEvaluator.cs | 1 + .../IntentResolutionEvaluator.cs | 1 + ...ft.Extensions.AI.Evaluation.Quality.csproj | 1 + .../RelevanceEvaluator.cs | 1 + .../RelevanceTruthAndCompletenessEvaluator.cs | 4 ++ .../RetrievalEvaluator.cs | 1 + .../TaskAdherenceEvaluator.cs | 1 + .../ToolCallAccuracyEvaluator.cs | 1 + .../ContentSafetyEvaluator.cs | 1 + ...oft.Extensions.AI.Evaluation.Safety.csproj | 1 + .../Utilities/BuiltInEvaluatorUtilities.cs | 17 +++++ .../BuiltInEvaluatorUtilitiesTests.cs | 65 +++++++++++++++++++ ...soft.Extensions.AI.Evaluation.Tests.csproj | 2 +- 22 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/BuiltInEvaluatorUtilities.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/BuiltInEvaluatorUtilitiesTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj index 67995f0ac60..835d7f7c1e6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj @@ -28,6 +28,11 @@ false + + + + + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs index f3030ec7cfb..5d9df9c801f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs @@ -50,6 +50,7 @@ public ValueTask EvaluateAsync( var metric = new NumericMetric(BLEUMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs index e070577c448..6924524ffb8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs @@ -50,6 +50,7 @@ public ValueTask EvaluateAsync( var metric = new NumericMetric(F1MetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs index 60df30879a4..fb32b6c81bd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs @@ -50,6 +50,7 @@ public ValueTask EvaluateAsync( var metric = new NumericMetric(GLEUMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj index 12e7cebb957..53564605660 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj @@ -20,6 +20,11 @@ true + + + + + @@ -33,8 +38,4 @@ - - - -
    diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs index 1f93712a35c..7bf68d887c9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs @@ -76,6 +76,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(CoherenceMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs index 3bd57cf322b..20a0b7b58b3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs @@ -73,6 +73,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(CompletenessMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs index ced79652bc7..9d820aeffc0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs @@ -72,6 +72,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(EquivalenceMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs index 44a36a6dcaa..97a7a651427 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs @@ -70,6 +70,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(FluencyMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs index a52fbcf2ad9..080fb9262f2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs @@ -71,6 +71,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(GroundednessMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (string.IsNullOrWhiteSpace(modelResponse.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs index 4f19d308f10..5960eb14aa0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs @@ -84,6 +84,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(IntentResolutionMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (!messages.Any()) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.csproj index b539eedef95..50fa41b7549 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/Microsoft.Extensions.AI.Evaluation.Quality.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs index 3946853a2a4..46f48d13ab8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs @@ -74,6 +74,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(RelevanceMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || string.IsNullOrWhiteSpace(userRequest.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs index 4eb41b15361..d175bfa7852 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs @@ -90,6 +90,10 @@ public async ValueTask EvaluateAsync( var completeness = new NumericMetric(CompletenessMetricName); var result = new EvaluationResult(relevance, truth, completeness); + relevance.MarkAsBuiltIn(); + truth.MarkAsBuiltIn(); + completeness.MarkAsBuiltIn(); + if (!messages.TryGetUserRequest( out ChatMessage? userRequest, out IReadOnlyList conversationHistory) || diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs index cd2f94456e6..557f66b0d21 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs @@ -79,6 +79,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(RetrievalMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (!messages.TryGetUserRequest(out ChatMessage? userRequest) || string.IsNullOrWhiteSpace(userRequest.Text)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs index cf4ba4073ee..fc97dcc0268 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs @@ -83,6 +83,7 @@ public async ValueTask EvaluateAsync( var metric = new NumericMetric(TaskAdherenceMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (!messages.Any()) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs index 5b3631bf598..bed95eeb3a2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs @@ -85,6 +85,7 @@ public async ValueTask EvaluateAsync( var metric = new BooleanMetric(ToolCallAccuracyMetricName); var result = new EvaluationResult(metric); + metric.MarkAsBuiltIn(); if (!messages.Any()) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs index afe90b0ac1d..9c562c6f80c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs @@ -166,6 +166,7 @@ EvaluationResult UpdateMetrics() metric.Name = metricName; } + metric.MarkAsBuiltIn(); metric.AddOrUpdateChatMetadata(annotationResponse, annotationDuration); metric.Interpretation = diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj index 12512e6884c..cc458103480 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/BuiltInEvaluatorUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/BuiltInEvaluatorUtilities.cs new file mode 100644 index 00000000000..a86da7aaf6d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/BuiltInEvaluatorUtilities.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI.Evaluation.Utilities; + +internal static class BuiltInEvaluatorUtilities +{ + internal const string BuiltInEvalMetadataName = "built-in-eval"; + + internal static void MarkAsBuiltIn(this EvaluationMetric metric) => + metric.AddOrUpdateMetadata(BuiltInEvalMetadataName, "True"); + + internal static bool IsBuiltIn(this EvaluationMetric metric) => + metric.Metadata?.TryGetValue(BuiltInEvalMetadataName, out string? stringValue) is true && + bool.TryParse(stringValue, out bool value) && + value; +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/BuiltInEvaluatorUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/BuiltInEvaluatorUtilitiesTests.cs new file mode 100644 index 00000000000..12267943a2e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/BuiltInEvaluatorUtilitiesTests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +extern alias Evaluation; +using Evaluation::Microsoft.Extensions.AI.Evaluation; +using Evaluation::Microsoft.Extensions.AI.Evaluation.Utilities; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Tests; + +public class BuiltInEvaluatorUtilitiesTests +{ + [Fact] + public void MarkAsBuiltInAddsMetadata() + { + var metric = new NumericMetric("name"); + metric.MarkAsBuiltIn(); + Assert.True(metric.IsBuiltIn()); + } + + [Fact] + public void IsBuiltInReturnsFalseIfMetadataIsMissing() + { + var metric = new NumericMetric("name"); + Assert.False(metric.IsBuiltIn()); + } + + [Theory] + [InlineData("true")] + [InlineData("TRUE")] + [InlineData("True")] + public void MetadataValueOfTrueIsCaseInsensitive(string value) + { + var metric = new BooleanMetric("name"); + metric.AddOrUpdateMetadata(BuiltInEvaluatorUtilities.BuiltInEvalMetadataName, value); + Assert.True(metric.IsBuiltIn()); + } + + [Theory] + [InlineData("false")] + [InlineData("FALSE")] + [InlineData("False")] + public void MetadataValueOfFalseIsCaseInsensitive(string value) + { + var metric = new StringMetric("name"); + metric.AddOrUpdateMetadata(BuiltInEvaluatorUtilities.BuiltInEvalMetadataName, value); + Assert.False(metric.IsBuiltIn()); + } + + [Fact] + public void UnrecognizedMetadataValueIsTreatedAsFalse() + { + var metric = new NumericMetric("name"); + metric.AddOrUpdateMetadata(BuiltInEvaluatorUtilities.BuiltInEvalMetadataName, "unrecognized"); + Assert.False(metric.IsBuiltIn()); + } + + [Fact] + public void EmptyMetadataValueIsTreatedAsFalse() + { + var metric = new NumericMetric("name"); + metric.AddOrUpdateMetadata(BuiltInEvaluatorUtilities.BuiltInEvalMetadataName, string.Empty); + Assert.False(metric.IsBuiltIn()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/Microsoft.Extensions.AI.Evaluation.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/Microsoft.Extensions.AI.Evaluation.Tests.csproj index d668fb94d14..b4c415ac358 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/Microsoft.Extensions.AI.Evaluation.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/Microsoft.Extensions.AI.Evaluation.Tests.csproj @@ -6,7 +6,7 @@ - + From 6978715d2e9a7e38821509d0553fc7e27c751039 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:11:55 +0200 Subject: [PATCH 239/472] Update dependencies from https://github.com/dotnet/arcade build 20250730.1 (#6679) Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Templating , Microsoft.DotNet.Helix.Sdk From Version 9.0.0-beta.25366.1 -> To Version 9.0.0-beta.25380.1 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- global.json | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 324779fef3a..71ed9339a71 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f + 7e67a7b4b62513a475afe46c4cd74d54b68f65c9 - + https://github.com/dotnet/arcade - 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f + 7e67a7b4b62513a475afe46c4cd74d54b68f65c9 - + https://github.com/dotnet/arcade - 1a2e280a031aaed0dca606ec8c59c6fe0f9bfc7f + 7e67a7b4b62513a475afe46c4cd74d54b68f65c9 diff --git a/eng/Versions.props b/eng/Versions.props index 67d58f467f3..22b244c73b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -83,7 +83,7 @@ 9.0.7 - 9.0.0-beta.25366.1 + 9.0.0-beta.25380.1 diff --git a/global.json b/global.json index 945f04c1f57..8bd45c3e768 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25366.1", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25366.1" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25380.1", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25380.1" } } From 2fbb77643383db36bfd0e55e6923187491fcbfc1 Mon Sep 17 00:00:00 2001 From: Iliar Turdushev Date: Tue, 5 Aug 2025 15:07:36 +0200 Subject: [PATCH 240/472] Fixes #6364 (#6682) Adds the {OriginalFormat} property to the list of tags of HttpClient logs --- .../Logging/Internal/Log.cs | 12 ++++-- .../Logging/AcceptanceTests.cs | 4 +- .../Logging/HttpClientLoggerTest.cs | 39 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs index c156eb72419..3ae22bf50bf 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs @@ -17,9 +17,12 @@ namespace Microsoft.Extensions.Http.Logging.Internal; [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Event ID's.")] internal static partial class Log { - internal const string OriginalFormat = "{OriginalFormat}"; + private const int MinimalPropertyCount = 5; - private const int MinimalPropertyCount = 4; + private const string OriginalFormat = "{OriginalFormat}"; + + private const string OriginalFormatValue = + $"{{{HttpClientLoggingTagNames.Method}}} {{{HttpClientLoggingTagNames.Host}}}/{{{HttpClientLoggingTagNames.Path}}}"; private const string RequestReadErrorMessage = "An error occurred while reading the request data to fill the logger context for request: " + @@ -133,9 +136,12 @@ private static void OutgoingRequest( if (record.ResponseBody is not null) { - loggerMessageState.TagArray[index] = new(HttpClientLoggingTagNames.ResponseBody, record.ResponseBody); + loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.ResponseBody, record.ResponseBody); } + // "{OriginalFormat}" property needs to be the last tag in the list. + loggerMessageState.TagArray[index] = new(OriginalFormat, OriginalFormatValue); + logger.Log( level, new(eventId, eventName), diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs index 3143aab9185..ec8f21c06bc 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/AcceptanceTests.cs @@ -385,14 +385,14 @@ public async Task AddHttpClientLogging_StructuredPathLogging_RedactsSensitivePar if (parameterRedactionMode == HttpRouteParameterRedactionMode.None) { loggedPath.Should().Be(httpRequestMessage.RequestUri.AbsolutePath); - state.Should().HaveCount(5); + state.Should().HaveCount(6); } else { loggedPath.Should().Be(RequestRoute); state.Should().ContainSingle(kvp => kvp.Key == "userId").Which.Value.Should().Be(expectedUserId); state.Should().ContainSingle(kvp => kvp.Key == "unitId").Which.Value.Should().Be(expectedUnitId); - state.Should().HaveCount(7); + state.Should().HaveCount(8); } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs index 84ed98263c1..f57fb23b8d7 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpClientLoggerTest.cs @@ -252,6 +252,7 @@ public async Task HttpLoggingHandler_AllOptions_LogsOutgoingRequest() logRecordState.Contains(testSharedResponseHeaderKey, expectedLogRecord.ResponseHeaders[1].Value); logRecordState.Contains(testSharedRequestHeaderKey, expectedLogRecord.RequestHeaders[1].Value); logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + EnsureLogRecordContainsOriginalFormat(logRecord); } [Fact] @@ -344,6 +345,7 @@ public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingR logRecordRequest.NotContains(HttpClientLoggingTagNames.StatusCode); logRecordRequest.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); logRecordRequest.NotContains(testEnricher.KvpRequest.Key); + EnsureLogRecordContainsOriginalFormat(logRecords[0]); var logRecordFull = logRecords[1].GetStructuredState(); logRecordFull.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host); @@ -357,6 +359,7 @@ public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingR logRecordFull.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); logRecordFull.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); logRecordFull.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key)); + EnsureLogRecordContainsOriginalFormat(logRecords[1]); } [Fact] @@ -453,6 +456,7 @@ public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInform logRecordState.NotContains(testEnricher.KvpResponse.Key); logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration); Assert.DoesNotContain(logRecordState, kvp => kvp.Key.StartsWith(HttpClientLoggingTagNames.ResponseHeaderPrefix)); + EnsureLogRecordContainsOriginalFormat(logRecord); } [Fact(Skip = "Flaky test, see https://github.com/dotnet/extensions/issues/4530")] @@ -568,6 +572,7 @@ public async Task HttpLoggingHandler_ReadResponseThrows_LogsException() logRecordState.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key)); logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration); Assert.DoesNotContain(logRecordState, kvp => kvp.Key.StartsWith(HttpClientLoggingTagNames.ResponseHeaderPrefix)); + EnsureLogRecordContainsOriginalFormat(logRecord); } [Fact] @@ -657,6 +662,7 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_Logs logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); logRecordState.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + EnsureLogRecordContainsOriginalFormat(logRecord); } [Fact] @@ -918,18 +924,20 @@ public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgo await client.SendAsync(httpRequestMessage, It.IsAny()); var logRecords = fakeLogger.Collector.GetSnapshot(); - var logRecord = Assert.Single(logRecords).GetStructuredState(); - - logRecord.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host); - logRecord.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString()); - logRecord.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted); - logRecord.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration); - logRecord.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); - logRecord.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody); - logRecord.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody); - logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); - logRecord.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); - logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + var logRecord = Assert.Single(logRecords); + var logRecordState = logRecord.GetStructuredState(); + + logRecordState.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host); + logRecordState.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString()); + logRecordState.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted); + logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration); + logRecordState.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); + logRecordState.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody); + logRecordState.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody); + logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecordState.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); + logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + EnsureLogRecordContainsOriginalFormat(logRecord); } [Theory] @@ -1024,4 +1032,11 @@ private static void EnsureLogRecordDuration(string? actualValue) private static IOutgoingRequestContext RequestMetadataContext => new Mock().Object; + + private static void EnsureLogRecordContainsOriginalFormat(FakeLogRecord logRecord) + { + var pair = logRecord.StructuredState!.Last(); + Assert.Equal("{OriginalFormat}", pair.Key); + Assert.Equal("{http.request.method} {server.address}/{url.path}", pair.Value); + } } From f1f8260445f7897d045ce0751a8471efd01e8df6 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 5 Aug 2025 11:00:35 -0400 Subject: [PATCH 241/472] Update to OpenAI 2.3.0 (#6684) * Update to OpenAI 2.3.0 --- eng/packages/General.props | 2 +- ...icrosoftExtensionsAIResponsesExtensions.cs | 19 +- .../OpenAIChatClient.cs | 4 +- .../OpenAIEmbeddingGenerator.cs | 5 +- .../OpenAIResponsesChatClient.cs | 151 +++++----- .../OpenAISpeechToTextClient.cs | 4 +- .../OpenAIConversionTests.cs | 282 +++++++++++++++++- .../OpenAISpeechToTextClientTests.cs | 26 +- 8 files changed, 372 insertions(+), 121 deletions(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index a6c1a69f4d8..253fb51ce1b 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -16,7 +16,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 083b4057d7d..188f5df3e52 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; +#pragma warning disable S3254 // Default parameter values should not be passed as arguments + namespace OpenAI.Responses; /// Provides extension methods for working with content associated with OpenAI.Responses. @@ -57,8 +59,9 @@ public static IAsyncEnumerable AsChatResponseUpdatesAsync( /// Creates an OpenAI from a . /// The response to convert. + /// The options employed in the creation of the response. /// The created . - internal static OpenAIResponse AsOpenAIResponse(this ChatResponse response) // Implement and make public once OpenAIResponse can be constructed external to the OpenAI library. + public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOptions? options = null) { _ = Throw.IfNull(response); @@ -67,6 +70,18 @@ internal static OpenAIResponse AsOpenAIResponse(this ChatResponse response) // I return openAIResponse; } - throw new NotSupportedException(); + return OpenAIResponsesModelFactory.OpenAIResponse( + response.ResponseId, + response.CreatedAt ?? default, + ResponseStatus.Completed, + usage: null, // No way to construct a ResponseTokenUsage right now from external to the OpenAI library + maxOutputTokenCount: options?.MaxOutputTokens, + outputItems: OpenAIResponsesChatClient.ToOpenAIResponseItems(response.Messages, options), + parallelToolCallsEnabled: options?.AllowMultipleToolCalls ?? false, + model: response.ModelId ?? options?.ModelId, + temperature: options?.Temperature, + topP: options?.TopP, + previousResponseId: options?.ConversationId, + instructions: options?.Instructions); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 1a56452e332..80ca0fc1237 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -47,10 +47,8 @@ public OpenAIChatClient(ChatClient chatClient) // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(ChatClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(chatClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - string? model = typeof(ChatClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatClient) as string; - _metadata = new("openai", providerUrl, model); + _metadata = new("openai", providerUrl, _chatClient.Model); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index a6c4af831ba..84f3c5966b8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -54,10 +54,7 @@ public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? defaultMod ?.GetValue(embeddingClient) as Uri)?.ToString() ?? DefaultOpenAIEndpoint; - FieldInfo? modelField = typeof(EmbeddingClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - string? modelId = modelField?.GetValue(embeddingClient) as string; - - _metadata = CreateMetadata("openai", providerUrl, modelId, defaultModelDimensions); + _metadata = CreateMetadata("openai", providerUrl, _embeddingClient.Model, defaultModelDimensions); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index b1d4b010b99..a85d2294a7b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -201,6 +201,18 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) { + // Create an update populated with the current state of the response. + ChatResponseUpdate CreateUpdate(AIContent? content = null) => + new(lastRole, content is not null ? [content] : null) + { + ConversationId = conversationId, + CreatedAt = createdAt, + MessageId = lastMessageId, + ModelId = modelId, + RawRepresentation = streamingUpdate, + ResponseId = responseId, + }; + switch (streamingUpdate) { case StreamingResponseCreatedUpdate createdUpdate: @@ -211,21 +223,15 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe goto default; case StreamingResponseCompletedUpdate completedUpdate: - yield return new() - { - Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [], - ConversationId = conversationId, - CreatedAt = createdAt, - FinishReason = - ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? - (functionCallInfos is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop), - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - Role = lastRole, - }; + { + var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null); + update.FinishReason = + ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? + (functionCallInfos is not null ? ChatFinishReason.ToolCalls : + ChatFinishReason.Stop); + yield return update; break; + } case StreamingResponseOutputItemAddedUpdate outputItemAddedUpdate: switch (outputItemAddedUpdate.Item) @@ -243,22 +249,32 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate: _ = outputIndexToMessages.Remove(outputItemDoneUpdate.OutputIndex); + + if (outputItemDoneUpdate.Item is MessageResponseItem item && + item.Content is { Count: > 0 } content && + content.Any(c => c.OutputTextAnnotations is { Count: > 0 })) + { + AIContent annotatedContent = new(); + foreach (var c in content) + { + PopulateAnnotations(c, annotatedContent); + } + + yield return CreateUpdate(annotatedContent); + break; + } + goto default; case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate: + { _ = outputIndexToMessages.TryGetValue(outputTextDeltaUpdate.OutputIndex, out MessageResponseItem? messageItem); lastMessageId = messageItem?.Id; lastRole = ToChatRole(messageItem?.Role); - yield return new ChatResponseUpdate(lastRole, outputTextDeltaUpdate.Delta) - { - ConversationId = conversationId, - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - }; + + yield return CreateUpdate(new TextContent(outputTextDeltaUpdate.Delta)); break; + } case StreamingResponseFunctionCallArgumentsDeltaUpdate functionCallArgumentsDeltaUpdate: { @@ -283,16 +299,8 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe lastMessageId = callInfo.ResponseItem.Id; lastRole = ChatRole.Assistant; - yield return new ChatResponseUpdate(lastRole, [fcc]) - { - ConversationId = conversationId, - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - }; + yield return CreateUpdate(fcc); break; } @@ -300,51 +308,22 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe } case StreamingResponseErrorUpdate errorUpdate: - yield return new ChatResponseUpdate + yield return CreateUpdate(new ErrorContent(errorUpdate.Message) { - Contents = - [ - new ErrorContent(errorUpdate.Message) - { - ErrorCode = errorUpdate.Code, - Details = errorUpdate.Param, - } - ], - ConversationId = conversationId, - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - Role = lastRole, - }; + ErrorCode = errorUpdate.Code, + Details = errorUpdate.Param, + }); break; case StreamingResponseRefusalDoneUpdate refusalDone: - yield return new ChatResponseUpdate + yield return CreateUpdate(new ErrorContent(refusalDone.Refusal) { - Contents = [new ErrorContent(refusalDone.Refusal) { ErrorCode = nameof(ResponseContentPart.Refusal) }], - ConversationId = conversationId, - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - Role = lastRole, - }; + ErrorCode = nameof(ResponseContentPart.Refusal), + }); break; default: - yield return new ChatResponseUpdate - { - ConversationId = conversationId, - CreatedAt = createdAt, - MessageId = lastMessageId, - ModelId = modelId, - RawRepresentation = streamingUpdate, - ResponseId = responseId, - Role = lastRole, - }; + yield return CreateUpdate(); break; } } @@ -628,20 +607,7 @@ private static List ToAIContents(IEnumerable con RawRepresentation = part, }; - if (part.OutputTextAnnotations is { Count: > 0 }) - { - foreach (var ota in part.OutputTextAnnotations) - { - (text.Annotations ??= []).Add(new CitationAnnotation - { - RawRepresentation = ota, - AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = ota.UriCitationStartIndex, EndIndex = ota.UriCitationEndIndex }], - Title = ota.UriCitationTitle, - Url = ota.UriCitationUri, - FileId = ota.FileCitationFileId ?? ota.FilePathFileId, - }); - } - } + PopulateAnnotations(part, text); results.Add(text); break; @@ -666,6 +632,25 @@ private static List ToAIContents(IEnumerable con return results; } + /// Converts any annotations from and stores them in . + private static void PopulateAnnotations(ResponseContentPart source, AIContent destination) + { + if (source.OutputTextAnnotations is { Count: > 0 }) + { + foreach (var ota in source.OutputTextAnnotations) + { + (destination.Annotations ??= []).Add(new CitationAnnotation + { + RawRepresentation = ota, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = ota.UriCitationStartIndex, EndIndex = ota.UriCitationEndIndex }], + Title = ota.UriCitationTitle, + Url = ota.UriCitationUri, + FileId = ota.FileCitationFileId ?? ota.FilePathFileId, + }); + } + } + } + /// Convert a list of s to a list of . private static List ToOpenAIResponsesContent(IList contents) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index fa00fc45232..2e2503335f3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -47,10 +47,8 @@ public OpenAISpeechToTextClient(AudioClient audioClient) // the package can provide such implementations separate from what's exposed in the public API. Uri providerUrl = typeof(AudioClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(audioClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - string? model = typeof(AudioClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(audioClient) as string; - _metadata = new("openai", providerUrl, model); + _metadata = new("openai", providerUrl, _audioClient.Model); } /// diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 79b8148a040..378ab8e8101 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -112,7 +112,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) Assert.Equal(6, convertedMessages.Length); index = 1; - SystemChatMessage instructionsMessage = Assert.IsType(convertedMessages[0]); + SystemChatMessage instructionsMessage = Assert.IsType(convertedMessages[0], exactMatch: false); Assert.Equal("You talk like a parrot.", Assert.Single(instructionsMessage.Content).Text); } else @@ -120,13 +120,13 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) Assert.Equal(5, convertedMessages.Length); } - SystemChatMessage m0 = Assert.IsType(convertedMessages[index]); + SystemChatMessage m0 = Assert.IsType(convertedMessages[index], exactMatch: false); Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text); - UserChatMessage m1 = Assert.IsType(convertedMessages[index + 1]); + UserChatMessage m1 = Assert.IsType(convertedMessages[index + 1], exactMatch: false); Assert.Equal("Hello", Assert.Single(m1.Content).Text); - AssistantChatMessage m2 = Assert.IsType(convertedMessages[index + 2]); + AssistantChatMessage m2 = Assert.IsType(convertedMessages[index + 2], exactMatch: false); Assert.Single(m2.Content); Assert.Equal("Hi there!", m2.Content[0].Text); var tc = Assert.Single(m2.ToolCalls); @@ -138,11 +138,11 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) ["param2"] = 42 }), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span))); - ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3]); + ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3], exactMatch: false); Assert.Equal("callid123", m3.ToolCallId); Assert.Equal("theresult", Assert.Single(m3.Content).Text); - AssistantChatMessage m4 = Assert.IsType(convertedMessages[index + 4]); + AssistantChatMessage m4 = Assert.IsType(convertedMessages[index + 4], exactMatch: false); Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text); } @@ -229,9 +229,9 @@ public void AsChatResponse_ConvertsOpenAIChatCompletion() Assert.Equal(ChatRole.User, message.Role); Assert.Equal(3, message.Contents.Count); - Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0]).Text); - Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1]).Uri.ToString()); - Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); + Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0], exactMatch: false).Text); + Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1], exactMatch: false).Uri.ToString()); + Assert.Equal("functionName", Assert.IsType(message.Contents[2], exactMatch: false).Name); } [Fact] @@ -260,9 +260,9 @@ public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates() Assert.Equal(ChatRole.Assistant, message.Role); Assert.Equal(3, message.Contents.Count); - Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0]).Text); - Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1]).Uri.ToString()); - Assert.Equal("functionName", Assert.IsType(message.Contents[2]).Name); + Assert.Equal("Hello, world!", Assert.IsType(message.Contents[0], exactMatch: false).Text); + Assert.Equal("http://example.com/image.png", Assert.IsType(message.Contents[1], exactMatch: false).Uri.ToString()); + Assert.Equal("functionName", Assert.IsType(message.Contents[2], exactMatch: false).Name); static async IAsyncEnumerable CreateUpdates() { @@ -847,6 +847,264 @@ public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleUpdate Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, result[1].FinishReason); } + [Fact] + public void AsOpenAIResponse_WithNullArgument_ThrowsArgumentNullException() + { + Assert.Throws("response", () => ((ChatResponse)null!).AsOpenAIResponse()); + } + + [Fact] + public void AsOpenAIResponse_WithRawRepresentation_ReturnsOriginal() + { + var originalOpenAIResponse = OpenAIResponsesModelFactory.OpenAIResponse( + "original-response-id", + new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + ResponseStatus.Completed, + usage: null, + maxOutputTokenCount: 100, + outputItems: [], + parallelToolCallsEnabled: false, + model: "gpt-4", + temperature: 0.7f, + topP: 0.9f, + previousResponseId: "prev-id", + instructions: "Test instructions"); + + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test")) + { + RawRepresentation = originalOpenAIResponse + }; + + var result = chatResponse.AsOpenAIResponse(); + + Assert.Same(originalOpenAIResponse, result); + } + + [Fact] + public void AsOpenAIResponse_WithBasicChatResponse_CreatesValidOpenAIResponse() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello, world!")) + { + ResponseId = "test-response-id", + ModelId = "gpt-4-turbo", + CreatedAt = new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero), + FinishReason = ChatFinishReason.Stop + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + Assert.NotNull(openAIResponse); + Assert.Equal("test-response-id", openAIResponse.Id); + Assert.Equal("gpt-4-turbo", openAIResponse.Model); + Assert.Equal(new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero), openAIResponse.CreatedAt); + Assert.Equal(ResponseStatus.Completed, openAIResponse.Status); + Assert.NotNull(openAIResponse.OutputItems); + Assert.Single(openAIResponse.OutputItems); + + var outputItem = Assert.IsAssignableFrom(openAIResponse.OutputItems.First()); + Assert.Equal("Hello, world!", Assert.Single(outputItem.Content).Text); + } + + [Fact] + public void AsOpenAIResponse_WithChatOptions_IncludesOptionsInResponse() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test message")) + { + ResponseId = "options-test", + ModelId = "gpt-3.5-turbo" + }; + + var options = new ChatOptions + { + MaxOutputTokens = 500, + AllowMultipleToolCalls = true, + ConversationId = "conversation-123", + Instructions = "You are a helpful assistant.", + Temperature = 0.8f, + TopP = 0.95f, + ModelId = "override-model" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(options); + + Assert.Equal("options-test", openAIResponse.Id); + Assert.Equal("gpt-3.5-turbo", openAIResponse.Model); + Assert.Equal(500, openAIResponse.MaxOutputTokenCount); + Assert.True(openAIResponse.ParallelToolCallsEnabled); + Assert.Equal("conversation-123", openAIResponse.PreviousResponseId); + Assert.Equal("You are a helpful assistant.", openAIResponse.Instructions); + Assert.Equal(0.8f, openAIResponse.Temperature); + Assert.Equal(0.95f, openAIResponse.TopP); + } + + [Fact] + public void AsOpenAIResponse_WithEmptyMessages_CreatesResponseWithEmptyOutputItems() + { + var chatResponse = new ChatResponse([]) + { + ResponseId = "empty-response", + ModelId = "gpt-4" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + Assert.Equal("empty-response", openAIResponse.Id); + Assert.Equal("gpt-4", openAIResponse.Model); + Assert.Empty(openAIResponse.OutputItems); + } + + [Fact] + public void AsOpenAIResponse_WithMultipleMessages_ConvertsAllMessages() + { + var messages = new List + { + new(ChatRole.Assistant, "First message"), + new(ChatRole.Assistant, "Second message"), + new(ChatRole.Assistant, + [ + new TextContent("Third message with function call"), + new FunctionCallContent("call-123", "TestFunction", new Dictionary { ["param"] = "value" }) + ]) + }; + + var chatResponse = new ChatResponse(messages) + { + ResponseId = "multi-message-response" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + Assert.Equal(4, openAIResponse.OutputItems.Count); + + var messageItems = openAIResponse.OutputItems.OfType().ToArray(); + var functionCallItems = openAIResponse.OutputItems.OfType().ToArray(); + + Assert.Equal(3, messageItems.Length); + Assert.Single(functionCallItems); + + Assert.Equal("First message", Assert.Single(messageItems[0].Content).Text); + Assert.Equal("Second message", Assert.Single(messageItems[1].Content).Text); + Assert.Equal("Third message with function call", Assert.Single(messageItems[2].Content).Text); + + Assert.Equal("call-123", functionCallItems[0].CallId); + Assert.Equal("TestFunction", functionCallItems[0].FunctionName); + } + + [Fact] + public void AsOpenAIResponse_WithToolMessages_ConvertsCorrectly() + { + var messages = new List + { + new(ChatRole.Assistant, + [ + new TextContent("I'll call a function"), + new FunctionCallContent("call-456", "GetWeather", new Dictionary { ["location"] = "Seattle" }) + ]), + new(ChatRole.Tool, [new FunctionResultContent("call-456", "The weather is sunny")]), + new(ChatRole.Assistant, "The weather in Seattle is sunny!") + }; + + var chatResponse = new ChatResponse(messages) + { + ResponseId = "tool-message-test" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + var outputItems = openAIResponse.OutputItems.ToArray(); + Assert.Equal(4, outputItems.Length); + + // Should have message, function call, function output, and final message + Assert.IsType(outputItems[0], exactMatch: false); + Assert.IsType(outputItems[1], exactMatch: false); + Assert.IsType(outputItems[2], exactMatch: false); + Assert.IsType(outputItems[3], exactMatch: false); + + var functionCallOutput = (FunctionCallOutputResponseItem)outputItems[2]; + Assert.Equal("call-456", functionCallOutput.CallId); + Assert.Equal("The weather is sunny", functionCallOutput.FunctionOutput); + } + + [Fact] + public void AsOpenAIResponse_WithSystemAndUserMessages_ConvertsCorrectly() + { + var messages = new List + { + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, "Hello, how are you?"), + new(ChatRole.Assistant, "I'm doing well, thank you for asking!") + }; + + var chatResponse = new ChatResponse(messages) + { + ResponseId = "system-user-test" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + var outputItems = openAIResponse.OutputItems.ToArray(); + Assert.Equal(3, outputItems.Length); + + var systemMessage = Assert.IsType(outputItems[0], exactMatch: false); + var userMessage = Assert.IsType(outputItems[1], exactMatch: false); + var assistantMessage = Assert.IsType(outputItems[2], exactMatch: false); + + Assert.Equal("You are a helpful assistant.", Assert.Single(systemMessage.Content).Text); + Assert.Equal("Hello, how are you?", Assert.Single(userMessage.Content).Text); + Assert.Equal("I'm doing well, thank you for asking!", Assert.Single(assistantMessage.Content).Text); + } + + [Fact] + public void AsOpenAIResponse_WithDefaultValues_UsesExpectedDefaults() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Default test")); + + var openAIResponse = chatResponse.AsOpenAIResponse(); + + Assert.NotNull(openAIResponse); + Assert.Equal(ResponseStatus.Completed, openAIResponse.Status); + Assert.False(openAIResponse.ParallelToolCallsEnabled); + Assert.Null(openAIResponse.MaxOutputTokenCount); + Assert.Null(openAIResponse.Temperature); + Assert.Null(openAIResponse.TopP); + Assert.Null(openAIResponse.PreviousResponseId); + Assert.Null(openAIResponse.Instructions); + Assert.NotNull(openAIResponse.OutputItems); + } + + [Fact] + public void AsOpenAIResponse_WithOptionsButNoModelId_UsesOptionsModelId() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Model test")); + + var options = new ChatOptions + { + ModelId = "options-model-id" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(options); + + Assert.Equal("options-model-id", openAIResponse.Model); + } + + [Fact] + public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId() + { + var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Model priority test")) + { + ModelId = "response-model-id" + }; + + var options = new ChatOptions + { + ModelId = "options-model-id" + }; + + var openAIResponse = chatResponse.AsOpenAIResponse(options); + + Assert.Equal("response-model-id", openAIResponse.Model); + } + private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source) { foreach (var item in source) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs index 1252a20741b..2ba70995a86 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs @@ -68,7 +68,7 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri { string input = $$""" { - "model": "whisper-1", + "model": "gpt-4o-transcribe", "language": "{{speechLanguage}}" } """; @@ -81,7 +81,7 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri using VerbatimMultiPartHttpHandler handler = new(input, Output) { ExpectedRequestUriContains = "audio/transcriptions" }; using HttpClient httpClient = new(handler); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var audioSpeechStream = GetAudioStream(); var response = await client.GetTextAsync(audioSpeechStream, new SpeechToTextOptions @@ -102,7 +102,7 @@ public async Task GetTextAsync_BasicRequestResponse(string? speechLanguage, stri public async Task GetTextAsync_Cancelled_Throws() { using HttpClient httpClient = new(); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var fileStream = GetAudioStream(); using var cancellationTokenSource = new CancellationTokenSource(); @@ -116,7 +116,7 @@ await Assert.ThrowsAsync(() public async Task GetStreamingTextAsync_Cancelled_Throws() { using HttpClient httpClient = new(); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var fileStream = GetAudioStream(); using var cancellationTokenSource = new CancellationTokenSource(); @@ -142,7 +142,7 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu string input = $$""" { - "model": "whisper-1", + "model": "gpt-4o-transcribe", "language": "{{speechLanguage}}", "stream":true } @@ -156,7 +156,7 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu using VerbatimMultiPartHttpHandler handler = new(input, Output) { ExpectedRequestUriContains = "audio/transcriptions" }; using HttpClient httpClient = new(handler); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var audioSpeechStream = GetAudioStream(); await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions @@ -167,7 +167,7 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu { Assert.Contains("I finally got back to the gym the other day", update.Text); Assert.NotNull(update.RawRepresentation); - Assert.IsType(update.RawRepresentation); + Assert.IsType(update.RawRepresentation); } } @@ -179,7 +179,7 @@ public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() // There's no support for non english translations, so no language is passed to the API. const string Input = $$""" { - "model": "whisper-1" + "model": "gpt-4o-transcribe" } """; @@ -191,7 +191,7 @@ public async Task GetStreamingTextAsync_BasicTranslateRequestResponse() using VerbatimMultiPartHttpHandler handler = new(Input, Output) { ExpectedRequestUriContains = "audio/translations" }; using HttpClient httpClient = new(handler); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var audioSpeechStream = GetAudioStream(); await foreach (var update in client.GetStreamingTextAsync(audioSpeechStream, new SpeechToTextOptions @@ -211,7 +211,7 @@ public async Task GetTextAsync_Transcription_StronglyTypedOptions_AllSent() { const string Input = """ { - "model": "whisper-1", + "model": "gpt-4o-transcribe", "language": "pt", "prompt":"Hide any bad words with ", "temperature": 0.5, @@ -228,7 +228,7 @@ public async Task GetTextAsync_Transcription_StronglyTypedOptions_AllSent() using VerbatimMultiPartHttpHandler handler = new(Input, Output); using HttpClient httpClient = new(handler); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() @@ -251,7 +251,7 @@ public async Task GetTextAsync_Translation_StronglyTypedOptions_AllSent() { const string Input = """ { - "model": "whisper-1", + "model": "gpt-4o-transcribe", "prompt":"Hide any bad words with ", "response_format": "vtt" } @@ -265,7 +265,7 @@ public async Task GetTextAsync_Translation_StronglyTypedOptions_AllSent() using VerbatimMultiPartHttpHandler handler = new(Input, Output); using HttpClient httpClient = new(handler); - using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "whisper-1"); + using ISpeechToTextClient client = CreateSpeechToTextClient(httpClient, "gpt-4o-transcribe"); using var audioSpeechStream = GetAudioStream(); Assert.NotNull(await client.GetTextAsync(audioSpeechStream, new() From d5aa4572fae5e577204a8fc451413427ad493267 Mon Sep 17 00:00:00 2001 From: Amadeusz Lechniak Date: Tue, 5 Aug 2025 20:03:00 +0200 Subject: [PATCH 242/472] Remove AdjustTime from experimental (#6668) * Remove AdjustTime from experimental * Remove unnecessary usings * Update API * Remove unnecessary package * Update version * Run Api Chief --- .../FakeTimeProvider.cs | 5 +---- .../Microsoft.Extensions.TimeProvider.Testing.csproj | 1 - .../Microsoft.Extensions.TimeProvider.Testing.json | 6 +++++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs index 43b9e92b1c5..b9404afa30e 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Threading; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Time.Testing; @@ -137,7 +135,7 @@ public void Advance(TimeSpan delta) } /// - /// Advances the date and time in the UTC time zone. + /// Sets the date and time in the UTC time zone. /// /// The date and time in the UTC time zone. /// @@ -145,7 +143,6 @@ public void Advance(TimeSpan delta) /// timers. This is similar to what happens in a real system when the system's /// time is changed. /// - [Experimental(diagnosticId: DiagnosticIds.Experiments.TimeProvider, UrlFormat = DiagnosticIds.UrlFormat)] public void AdjustTime(DateTimeOffset value) { lock (Waiters) diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj index f1987e0ad68..679d9e74854 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj @@ -9,7 +9,6 @@ Fundamentals Testing $(PackageTags);Testing;TimeProvider;FakeTimeProvider - true true diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.json b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.json index b1c5d11adc2..c2417fd0d63 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.json +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.TimeProvider.Testing, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.TimeProvider.Testing, Version=9.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "class Microsoft.Extensions.Time.Testing.FakeTimeProvider : System.TimeProvider", @@ -13,6 +13,10 @@ "Member": "Microsoft.Extensions.Time.Testing.FakeTimeProvider.FakeTimeProvider(System.DateTimeOffset startDateTime);", "Stage": "Stable" }, + { + "Member": "void Microsoft.Extensions.Time.Testing.FakeTimeProvider.AdjustTime(System.DateTimeOffset value);", + "Stage": "Stable" + }, { "Member": "void Microsoft.Extensions.Time.Testing.FakeTimeProvider.Advance(System.TimeSpan delta);", "Stage": "Stable" From 9db47febee40559ada850bd457386285ae5bcfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Wiesner?= Date: Wed, 6 Aug 2025 11:18:29 +0200 Subject: [PATCH 243/472] Allow custom filtering logic for FakeLogger (#5848) * Adds new nullable predicate property CustomFilter to FakeLogCollectorOptions. * FakeLogCollector uses the new CustomFilter property when not null to filter out records if they have satisfied the previous filtering options but not the defined predicate. --- .../Logging/FakeLogCollector.cs | 7 +++ .../Logging/FakeLogCollectorOptions.cs | 14 ++++- .../Logging/FakeLoggerTests.cs | 59 ++++++++++++++++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.cs index 376cc87be8c..24b9f933b9c 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.cs @@ -129,6 +129,13 @@ internal void AddRecord(FakeLogRecord record) return; } + var customFilter = _options.CustomFilter; + if (customFilter is not null && !customFilter(record)) + { + // record was filtered out by a custom filter + return; + } + lock (_records) { _records.Add(record); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollectorOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollectorOptions.cs index be5476e685a..356b1f8e6d6 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollectorOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollectorOptions.cs @@ -3,7 +3,8 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; #pragma warning disable CA2227 // Collection properties should be read only @@ -34,6 +35,17 @@ public class FakeLogCollectorOptions /// public ISet FilteredLevels { get; set; } = new HashSet(); + /// + /// Gets or sets custom filter for which records are collected. + /// + /// The default is . + /// + /// Defaults to which doesn't apply any additional filter to the records. + /// If not empty, only records for which the filter function returns will be collected by the fake logger. + /// + [Experimental(DiagnosticIds.Experiments.Telemetry)] + public Func? CustomFilter { get; set; } + /// /// Gets or sets a value indicating whether to collect records that are logged when the associated log level is currently disabled. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Testing.Tests/Logging/FakeLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Testing.Tests/Logging/FakeLoggerTests.cs index 9ec81a36077..18083f43569 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.Testing.Tests/Logging/FakeLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Testing.Tests/Logging/FakeLoggerTests.cs @@ -6,8 +6,6 @@ using System.Globalization; using System.Linq; using System.Threading; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Time.Testing; using Xunit; @@ -283,4 +281,61 @@ public void Scopes() Assert.Equal(42, (int)logger.LatestRecord.Scopes[0]!); Assert.Equal("Hello World", (string)logger.LatestRecord.Scopes[1]!); } + + [Theory] + [InlineData(false, 2)] + [InlineData(true, 1)] + public void FilterByCustomFilter(bool useErrorLevelFilter, int expectedRecordCount) + { + const string NotIgnoredMessage1 = "Not ignored message 1"; + const string NotIgnoredMessage2 = "Not ignored message 2"; + const string IgnoredMessage = "Ignored message"; + + // Given + var options = new FakeLogCollectorOptions + { + CustomFilter = r => r.Message != IgnoredMessage, + FilteredLevels = useErrorLevelFilter ? [LogLevel.Error] : new HashSet(), + }; + + var collector = FakeLogCollector.Create(options); + var logger = new FakeLogger(collector); + + // When + logger.LogInformation(NotIgnoredMessage1); + logger.LogInformation(IgnoredMessage); + logger.LogError(IgnoredMessage); + logger.LogError(NotIgnoredMessage2); + logger.LogCritical(IgnoredMessage); + + var records = logger.Collector.GetSnapshot(); + + // Then + Assert.Equal(expectedRecordCount, records.Count); + Assert.Equal(expectedRecordCount, logger.Collector.Count); + + IList<(string message, LogLevel level, string prefix)> expectationsInOrder = useErrorLevelFilter + ? [(NotIgnoredMessage2, LogLevel.Error, "error] ")] + : [(NotIgnoredMessage1, LogLevel.Information, "info] "), (NotIgnoredMessage2, LogLevel.Error, "error] ")]; + + for (var i = 0; i < expectedRecordCount; i++) + { + var (expectedMessage, expectedLevel, expectedPrefix) = expectationsInOrder[i]; + var record = records[i]; + + Assert.Equal(expectedMessage, record.Message); + Assert.Equal(expectedLevel, record.Level); + Assert.Null(record.Exception); + Assert.Null(record.Category); + Assert.True(record.LevelEnabled); + Assert.Empty(record.Scopes); + Assert.Equal(0, record.Id.Id); + Assert.EndsWith($"{expectedPrefix}{expectedMessage}", record.ToString()); + + if (i == expectedRecordCount - 1) + { + Assert.Equivalent(record, logger.LatestRecord); + } + } + } } From 9a573e3202f69d25f22bcb71146f3b5166b79ac4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:00:17 -0700 Subject: [PATCH 244/472] Branding updates for 9.9 (#6693) * Initial plan * Update branding from 9.8 to 9.9 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Restore CompatibilitySuppressions.xml file to prevent build break Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- eng/Versions.props | 2 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 2 +- .../aichatweb/aichatweb.csproj | 4 ++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 22b244c73b6..c13583791d9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ 9 - 8 + 9 0 preview 1 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..188a457f56e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index e3e384f86da..31df685f9c4 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index fd2138900b0..3f16b9a019a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..188a457f56e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 9a1b1da5279..8f0ddc89860 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..188a457f56e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 74fed505bde..36a9f859cb5 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 66360fa60a0..3f2c1fd0a7d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,9 +8,9 @@ - + - + From 5169f4b2c5c42d742fd886614c1298f03b067761 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 7 Aug 2025 12:01:45 -0400 Subject: [PATCH 245/472] Update Azure.AI.OpenAI test dependency to 2.3.0-beta.1 (#6698) --- eng/packages/TestOnly.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index 9bf2edcca4a..dcfc7c03525 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -2,7 +2,7 @@ - + From b897c64e31ad4ed9ef58d11c71a58714aa7da0cd Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 7 Aug 2025 11:56:58 -0700 Subject: [PATCH 246/472] Mark packages as stable and update to .NET Servicing versions (#6700) --- eng/Version.Details.xml | 188 ++++++++++++++++++++-------------------- eng/Versions.props | 122 +++++++++++++------------- 2 files changed, 155 insertions(+), 155 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 71ed9339a71..afaabc8ac46 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,196 +1,196 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 3c298d9f00936d651cc47d221762474e25277672 + aae90fa09086a9be09dac83fa66542232c7269d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - f6b3a5da75eb405046889a5447ec9b14cc29d285 + 215a587e52efa710de84138b0a3374b860b924d8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 67d253c17619e6ba325e5390905ea2a13cc7f532 + 3f7d40ec7be104358780955b3f0fea62495264dc diff --git a/eng/Versions.props b/eng/Versions.props index 22b244c73b6..c935f50e1e4 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -10,14 +10,14 @@ - false + true - + release true @@ -33,55 +33,55 @@ --> - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 - 9.0.7 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 + 9.0.8 - 9.0.7 + 9.0.8 9.0.0-beta.25380.1 @@ -107,8 +107,8 @@ 8.0.1 8.0.0 8.0.2 - 8.0.18 - 8.0.18 + 8.0.19 + 8.0.19 8.0.0 8.0.1 8.0.1 @@ -125,17 +125,17 @@ 8.0.6 8.0.0 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 - 8.0.18 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 + 8.0.19 - 8.0.18 + 8.0.19 9.0.7 - 9.0.0-beta.25380.1 + 9.0.0-beta.25407.2 diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index abe80a2a0e0..8947ea3f059 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,6 +19,7 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false + microbuildUseESRP: true enablePublishBuildArtifacts: false enablePublishBuildAssets: false enablePublishTestResults: false @@ -134,10 +135,11 @@ jobs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea - ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' diff --git a/global.json b/global.json index 8bd45c3e768..8e8ecb1b1da 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "9.0.107" + "version": "9.0.109" }, "tools": { - "dotnet": "9.0.107", + "dotnet": "9.0.109", "runtimes": { "dotnet": [ "8.0.0", @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25380.1", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25380.1" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25407.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25407.2" } } From d5a62e3c19aeb7174f583cbc8db0ef82bd0d2508 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 11 Aug 2025 19:08:40 -0400 Subject: [PATCH 250/472] [release/9.8] Backport several AI-related PRs (#6703) * Update Azure.AI.OpenAI test dependency to 2.3.0-beta.1 (#6698) * Bring back per library CHANGELOGS for M.E.AI (#6697) * Revert "Delete M.E.AI changelog files (#6467)" This reverts commit 2ab21ec6d6fa7371f19d8485215d4c0c99f9c338. * Bring back per library CHANGELOGS for M.E.AI By popular demand. * Fix typos * Add HostedFile/VectorStoreContent, HostedFileSearchTool, and HostedCodeInterpreterTool.Inputs (#6620) * Add HostedFileContent, HostedVectorStoreContent, HostedFileSearchTool, and HostedCodeInterpreterTool.Inputs --- eng/packages/TestOnly.props | 2 +- .../CHANGELOG.md | 150 ++++++++++++++++++ .../ChatCompletion/ChatMessage.cs | 14 +- .../ChatCompletion/ChatResponseExtensions.cs | 105 ++++++++---- .../ChatCompletion/ChatResponseUpdate.cs | 14 +- .../Contents/AIContent.cs | 2 + .../Contents/HostedFileContent.cs | 43 +++++ .../Contents/HostedVectorStoreContent.cs | 44 +++++ .../HostedCodeInterpreterTool.cs | 10 ++ .../HostedFileSearchTool.cs | 31 ++++ .../Microsoft.Extensions.AI.Abstractions.json | 58 +++++++ .../CHANGELOG.md | 66 ++++++++ .../CHANGELOG.md | 88 ++++++++++ .../OpenAIAssistantsChatClient.cs | 55 ++++++- .../OpenAIChatClient.cs | 39 +++-- .../OpenAIJsonContext.cs | 1 + .../OpenAIResponsesChatClient.cs | 74 +++++++-- .../Microsoft.Extensions.AI/CHANGELOG.md | 133 ++++++++++++++++ .../Contents/HostedFileContentTests.cs | 52 ++++++ .../Contents/HostedVectorStoreContentTests.cs | 52 ++++++ .../HostedCodeInterpreterToolTests.cs | 19 +++ .../HostedFileSearchToolTests.cs | 41 +++++ ...enAIAssistantChatClientIntegrationTests.cs | 17 ++ 23 files changed, 1036 insertions(+), 74 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedFileContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedVectorStoreContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md create mode 100644 src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedFileContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedVectorStoreContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index 9bf2edcca4a..dcfc7c03525 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -2,7 +2,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md new file mode 100644 index 00000000000..c649dd906d2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -0,0 +1,150 @@ +# Release History + +## NOT YET RELEASED + +- Added `AIAnnotation` and related types to represent citations and other annotations in chat messages. +- Added `ChatMessage.CreatedAt` so that chat messages can carry their timestamp. +- Added a `[Description(...)]` attribute to `DataContent.Uri` to clarify its purpose when used in schemas. +- Added `DataContent.Name` property to associate a name with the binary data, like a filename. +- Added `HostedFileContent` for representing files hosted by the service. +- Added `HostedVectorStoreContent` for representing vector stores hosted by the service. +- Added `HostedFileSearchTool` to represent server-side file search tools. +- Added `HostedCodeInterpreterTool.Inputs` to supply context about what state is available to the code interpreter tool. +- Improved handling of function parameter data annotation attributes in `AIJsonUtilities.CreateJsonSchema`. +- Fixed schema generation to include an items keyword for arrays of objects in `AIJsonUtilities.CreateJsonSchema`. + +## 9.7.1 + +- Fixed schema generation for nullable function parameters in `AIJsonUtilities.CreateJsonSchema`. + +## 9.7.0 + +- Added `ChatOptions.Instructions` property for configuring system instructions separate from chat messages. +- Added `Usage` property to `SpeechToTextResponse` to provide details about the token usage. +- Augmented `AIJsonUtilities.CreateJsonSchema` with support for data annotations. + +## 9.6.0 + +- Added `AIFunction.ReturnJsonSchema` to represent the JSON schema of the return value of a function. +- Removed title and description keywords from root-level schemas in `AIFunctionFactory`. + +## 9.5.0 + +- Moved `AIFunctionFactory` down from `Microsoft.Extensions.AI` to `Microsoft.Extensions.AI.Abstractions`. +- Added `BinaryEmbedding` type for representing bit embeddings. +- Added `TextReasoningContent` to represent reasoning content in chat messages. +- Added `ChatOptions.AllowMultipleToolCalls` for configuring parallel tool calling. +- Added a public constructor to the base `AIContent`. +- Added a missing `[DebuggerDisplay]` attribute on `AIFunctionArguments`. +- Added `ChatOptions.RawRepresentationFactory` to facilitate passing raw options to the underlying service. +- Added an `AIJsonSchemaTransformOptions` property inside `AIJsonSchemaCreateOptions`. +- Added `DataContent.Base64Data` property for easier and more efficient handling of base64-encoded data. +- Added JSON schema transformation functionality to `AIJsonUtilities`. +- Fixed `AIJsonUtilities.CreateJsonSchema` to handle `JsonSerializerOptions` that do not have a `TypeInfoResolver` configured. +- Fixed `AIFunctionFactory` handling of default struct arguments. +- Fixed schema generation to ensure the type keyword is included when generating schemas for nullable enums. +- Renamed the `GenerateXx` extension methods on `IEmbeddingGenerator<>`. +- Renamed `ChatThreadId` to `ConversationId` across the libraries. +- Replaced `Type targetType` parameter in `AIFunctionFactory.Create` with a delegate. +- Remove `[Obsolete]` members from previews. + +## 9.4.4-preview.1.25259.16 + +- Added `AIJsonUtilities.TransformSchema` and supporting types. +- Added `BinaryEmbedding` for bit embeddings. +- Added `ChatOptions.RawRepresentationFactory` to make it easier to pass options to the underlying service. +- Added `Base64Data` property to `DataContent`. +- Moved `AIFunctionFactory` to `Microsoft.Extensions.AI.Abstractions`. +- Fixed `AIFunctionFactory` handling of default struct arguments. + +## 9.4.3-preview.1.25230.7 + +- Renamed `ChatThreadId` to `ConversationId` on `ChatResponse`, `ChatResponseUpdate`, and `ChatOptions`. +- Renamed `EmbeddingGeneratorExtensions` method `GenerateEmbeddingAsync` to `GenerateAsync` and `GenerateEmbeddingVectorAsync` to `GenerateVectorAsync`. +- Made `AIContent`'s constructor `public` instead of `protected`. +- Fixed `AIJsonUtilities.CreateJsonSchema` to tolerate `JsonSerializerOptions` instances that don't have a `TypeInfoResolver` already configured. + +## 9.4.0-preview.1.25207.5 + +- Added `ErrorContent` and `TextReasoningContent`. +- Added `MessageId` to `ChatMessage` and `ChatResponseUpdate`. +- Added `AIFunctionArguments`, changing `AIFunction.InvokeAsync` to accept one and to return a `ValueTask`. +- Updated `AIJsonUtilities`'s schema generation to not use `default` when `RequireAllProperties` is set to `true`. +- Added `ISpeechToTextClient` and supporting types. +- Fixed several issues related to Native AOT support. + +## 9.3.0-preview.1.25161.3 + +- Changed `IChatClient.GetResponseAsync` and `IChatClient.GetStreamingResponseAsync` to accept an `IEnumerable` rather than an `IList`. It is no longer mutated by implementations. +- Removed `ChatResponse.Choice` and `ChatResponseUpdate.ChoiceIndex`. +- Replaced `ChatResponse.Message` with `ChatResponse.Messages`. Responses now carry with them all messages generated as part of the operation, rather than all but the last being added to the history and the last returned. +- Added `GetRequiredService` extension method for `IChatClient`/`IEmbeddingGenerator`. +- Added non-generic `IEmbeddingGenerator` interface, which is inherited by `IEmbeddingGenerator`. The `GetService` method moves down to the non-generic interface, and the `GetService`/`GetRequiredService` extension methods are now in terms of the non-generic. +- `AIJsonUtilities.CreateFunctionJsonSchema` now special-cases `CancellationToken` to not include it in the schema. +- Improved the debugger displays for `ChatMessage` and the `AIContent` types. +- Added a static `AIJsonUtilities.HashDataToString` method. +- Split `DataContent`, which handled both in-memory data and URIs to remote data, into `DataContent` (for the former) and `UriContent` (for the latter). +- Renamed `DataContent.MediaTypeStartsWith` to `DataContent.HasTopLevelMediaType`, and changed semantics accordingly. + +## 9.3.0-preview.1.25114.11 + +- Renamed `IChatClient.Complete{Streaming}Async` to `IChatClient.Get{Streaming}ResponseAsync`. This is to avoid confusion with "Complete" being about stopping an operation, as well as to avoid tying the methods to a particular implementation detail of how responses are generated. Along with this, renamed `ChatCompletion` to `ChatResponse`, `StreamingChatCompletionUpdate` to `ChatResponseUpdate`, `CompletionId` to `ResponseId`, `ToStreamingChatCompletionUpdates` to `ToChatResponseUpdates`, and `ToChatCompletion{Async}` to `ToChatResponse{Async}`. +- Removed `IChatClient.Metadata` and `IEmbeddingGenerator.Metadata`. The `GetService` method may be used to retrieve `ChatClientMetadata` and `EmbeddingGeneratorMetadata`, respectively. +- Added overloads of `Get{Streaming}ResponseAsync` that accept a single `ChatMessage` (in addition to the other overloads that accept a `List` or a `string`). +- Added `ChatThreadId` properties to `ChatOptions`, `ChatResponse`, and `ChatResponseUpdate`. `IChatClient` can now be used in both stateful and stateless modes of operation, such as with agents that maintain server-side chat history. +- Made `ChatOptions.ToolMode` nullable and added a `None` option. +- Changed `UsageDetails`'s properties from `int?` to `long?`. +- Removed `DataContent.ContainsData`; `DataContent.Data.HasValue` may be used instead. +- Removed `ImageContent` and `AudioContent`; the base `DataContent` should now be used instead, with a new `DataContent.MediaTypeStartsWith` helper for routing based on media type. +- Removed setters on `FunctionCallContent` and `FunctionResultContent` properties where the value is supplied to the constructor. +- Removed `FunctionResultContent.Name`. +- Augmented the base `AITool` with `Name`, `Description`, and `AdditionalProperties` virtual properties. +- Added a `CodeInterpreterTool` for use with services that support server-side code execution. +- Changed `AIFunction`'s schema representation to be for the whole function rather than per parameter, and exposed corresponding methods on `AIJsonUtilities`, e.g. `CreateFunctionJsonSchema`. +- Removed `AIFunctionParameterMetadata` and `AIFunctionReturnParameterMetadata` classes and corresponding properties on `AIFunction` and `AIFunctionFactoryCreateOptions`, replacing them with a `MethodInfo?`. All relevant metadata, such as the JSON schema for the function, are moved to properties directly on `AIFunction`. +- Renamed `AIFunctionFactoryCreateOptions` to `AIFunctionFactoryOptions` and made all its properties nullable. +- Changed `AIJsonUtilities.DefaultOptions` to use relaxed JSON escaping. +- Made `IEmbeddingGenerator` contravariant on `TInput`. + +## 9.1.0-preview.1.25064.3 + +- Added `AdditionalPropertiesDictionary` and changed `UsageDetails.AdditionalProperties` to be named `AdditionalCounts` and to be of type `AdditionalPropertiesDictionary`. +- Updated `FunctionCallingChatClient` to sum all `UsageDetails` token counts from all intermediate messages. +- Fixed JSON schema generation for floating-point types. +- Added `AddAIContentType` for enabling custom `AIContent`-derived types to participate in polymorphic serialization. + +## 9.0.1-preview.1.24570.5 + +- Changed `IChatClient`/`IEmbeddingGenerator`.`GetService` to be non-generic. +- Added `ToChatCompletion` / `ToChatCompletionUpdate` extension methods for `IEnumerable` / `IAsyncEnumerable`, respectively. +- Added `ToStreamingChatCompletionUpdates` instance method to `ChatCompletion`. +- Added `IncludeTypeInEnumSchemas`, `DisallowAdditionalProperties`, `RequireAllProperties`, and `TransformSchemaNode` options to `AIJsonSchemaCreateOptions`. +- Fixed a Native AOT warning in `AIFunctionFactory.Create`. +- Fixed a bug in `AIJsonUtilities` in the handling of Boolean schemas. +- Improved the `ToString` override of `ChatMessage` and `StreamingChatCompletionUpdate` to include all `TextContent`, and of `ChatCompletion` to include all choices. +- Added `DebuggerDisplay` attributes to `DataContent` and `GeneratedEmbeddings`. +- Improved the documentation. + +## 9.0.0-preview.9.24556.5 + +- Added a strongly-typed `ChatOptions.Seed` property. +- Improved `AdditionalPropertiesDictionary` with a `TryAdd` method, a strongly-typed `Enumerator`, and debugger-related attributes for improved debuggability. +- Fixed `AIJsonUtilities` schema generation for Boolean schemas. + +## 9.0.0-preview.9.24525.1 + +- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. +- Annotated `FunctionCallContent.Exception` and `FunctionResultContent.Exception` as `[JsonIgnore]`, such that they're ignored when serializing instances with `JsonSerializer`. The corresponding constructors accepting an `Exception` were removed. +- Annotated `ChatCompletion.Message` as `[JsonIgnore]`, such that it's ignored when serializing instances with `JsonSerializer`. +- Added the `FunctionCallContent.CreateFromParsedArguments` method. +- Added the `AdditionalPropertiesDictionary.TryGetValue` method. +- Added the `StreamingChatCompletionUpdate.ModelId` property and removed the `AIContent.ModelId` property. +- Renamed the `GenerateAsync` extension method on `IEmbeddingGenerator<,>` to `GenerateEmbeddingsAsync` and updated it to return `Embedding` rather than `GeneratedEmbeddings`. +- Added `GenerateAndZipAsync` and `GenerateEmbeddingVectorAsync` extension methods for `IEmbeddingGenerator<,>`. +- Added the `EmbeddingGeneratorOptions.Dimensions` property. +- Added the `ChatOptions.TopK` property. +- Normalized `null` inputs in `TextContent` to be empty strings. + +## 9.0.0-preview.9.24507.7 + +- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs index 9e36f548c00..7cbbca4e822 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +#pragma warning disable S3358 // Ternary operators should not be nested + namespace Microsoft.Extensions.AI; /// Represents a chat message used by an . @@ -107,7 +109,17 @@ public IList Contents /// Gets a object to display in the debugger display. [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private AIContent? ContentForDebuggerDisplay => _contents is { Count: > 0 } ? _contents[0] : null; + private AIContent? ContentForDebuggerDisplay + { + get + { + string text = Text; + return + !string.IsNullOrWhiteSpace(text) ? new TextContent(text) : + _contents is { Count: > 0 } ? _contents[0] : + null; + } + } /// Gets an indication for the debugger display of whether there's more content. [DebuggerBrowsable(DebuggerBrowsableState.Never)] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 667a9d9aea1..e41368f115d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -183,72 +184,106 @@ static async Task ToChatResponseAsync( } /// Coalesces sequential content elements. - internal static void CoalesceTextContent(List contents) + internal static void CoalesceTextContent(IList contents) { - Coalesce(contents, static text => new(text)); - Coalesce(contents, static text => new(text)); + Coalesce(contents, mergeSingle: false, static (contents, start, end) => + new(MergeText(contents, start, end)) + { + AdditionalProperties = contents[start].AdditionalProperties?.Clone() + }); - // This implementation relies on TContent's ToString returning its exact text. - static void Coalesce(List contents, Func fromText) - where TContent : AIContent + Coalesce(contents, mergeSingle: false, static (contents, start, end) => + new(MergeText(contents, start, end)) + { + AdditionalProperties = contents[start].AdditionalProperties?.Clone() + }); + + static string MergeText(IList contents, int start, int end) { - StringBuilder? coalescedText = null; + StringBuilder sb = new(); + for (int i = start; i < end; i++) + { + _ = sb.Append(contents[i]); + } + return sb.ToString(); + } + + static void Coalesce(IList contents, bool mergeSingle, Func, int, int, TContent> merge) + where TContent : AIContent + { // Iterate through all of the items in the list looking for contiguous items that can be coalesced. int start = 0; - while (start < contents.Count - 1) + while (start < contents.Count) { - // We need at least two TextContents in a row to be able to coalesce. We also avoid touching contents - // that have annotations, as we want to ensure the annotations (and in particular any start/end indices - // into the text content) remain accurate. - if (!TryAsCoalescable(contents[start], out var firstText)) + if (!TryAsCoalescable(contents[start], out var firstContent)) { start++; continue; } - if (!TryAsCoalescable(contents[start + 1], out var secondText)) + // Iterate until we find a non-coalescable item. + int i = start + 1; + while (i < contents.Count && TryAsCoalescable(contents[i], out _)) { - start += 2; - continue; + i++; } - // Append the text from those nodes and continue appending subsequent TextContents until we run out. - // We null out nodes as their text is appended so that we can later remove them all in one O(N) operation. - coalescedText ??= new(); - _ = coalescedText.Clear().Append(firstText).Append(secondText); - contents[start + 1] = null!; - int i = start + 2; - for (; i < contents.Count && TryAsCoalescable(contents[i], out TContent? next); i++) + // If there's only one item in the run, and we don't want to merge single items, skip it. + if (start == i - 1 && !mergeSingle) { - _ = coalescedText.Append(next); - contents[i] = null!; + start++; + continue; } - // Store the replacement node. We inherit the properties of the first text node. We don't - // currently propagate additional properties from the subsequent nodes. If we ever need to, - // we can add that here. - var newContent = fromText(coalescedText.ToString()); - contents[start] = newContent; - newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone(); + // Store the replacement node and null out all of the nodes that we coalesced. + // We can then remove all coalesced nodes in one O(N) operation via RemoveAll. + // Leave start positioned at the start of the next run. + contents[start] = merge(contents, start, i); - start = i; + start++; + while (start < i) + { + contents[start++] = null!; + } static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable) { - if (content is TContent && (content is not TextContent tc || tc.Annotations is not { Count: > 0 })) + if (content is TContent tmp && tmp.Annotations is not { Count: > 0 }) { - coalescable = (TContent)content; + coalescable = tmp; return true; } - coalescable = null!; + coalescable = null; return false; } } // Remove all of the null slots left over from the coalescing process. - _ = contents.RemoveAll(u => u is null); + if (contents is List contentsList) + { + _ = contentsList.RemoveAll(u => u is null); + } + else + { + int nextSlot = 0; + int contentsCount = contents.Count; + for (int i = 0; i < contentsCount; i++) + { + if (contents[i] is { } content) + { + contents[nextSlot++] = content; + } + } + + for (int i = contentsCount - 1; i >= nextSlot; i--) + { + contents.RemoveAt(i); + } + + Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length."); + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index 63dbfbc0d7d..bea91f97ed9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +#pragma warning disable S3358 // Ternary operators should not be nested + namespace Microsoft.Extensions.AI; /// @@ -141,7 +143,17 @@ public IList Contents /// Gets a object to display in the debugger display. [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private AIContent? ContentForDebuggerDisplay => _contents is { Count: > 0 } ? _contents[0] : null; + private AIContent? ContentForDebuggerDisplay + { + get + { + string text = Text; + return + !string.IsNullOrWhiteSpace(text) ? new TextContent(text) : + _contents is { Count: > 0 } ? _contents[0] : + null; + } + } /// Gets an indication for the debugger display of whether there's more content. [DebuggerBrowsable(DebuggerBrowsableState.Never)] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 5d0baf93957..f9c7603d02a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -12,6 +12,8 @@ namespace Microsoft.Extensions.AI; [JsonDerivedType(typeof(ErrorContent), typeDiscriminator: "error")] [JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: "functionCall")] [JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")] +[JsonDerivedType(typeof(HostedFileContent), typeDiscriminator: "hostedFile")] +[JsonDerivedType(typeof(HostedVectorStoreContent), typeDiscriminator: "hostedVectorStore")] [JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")] [JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")] [JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedFileContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedFileContent.cs new file mode 100644 index 00000000000..1bc994b8eea --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedFileContent.cs @@ -0,0 +1,43 @@ +// 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.Diagnostics; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a file that is hosted by the AI service. +/// +/// +/// Unlike which contains the data for a file or blob, this class represents a file that is hosted +/// by the AI service and referenced by an identifier. Such identifiers are specific to the provider. +/// +[DebuggerDisplay("FileId = {FileId}")] +public sealed class HostedFileContent : AIContent +{ + private string _fileId; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the hosted file. + /// is . + /// is empty or composed entirely of whitespace. + public HostedFileContent(string fileId) + { + _fileId = Throw.IfNullOrWhitespace(fileId); + } + + /// + /// Gets or sets the ID of the hosted file. + /// + /// is . + /// is empty or composed entirely of whitespace. + public string FileId + { + get => _fileId; + set => _fileId = Throw.IfNullOrWhitespace(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedVectorStoreContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedVectorStoreContent.cs new file mode 100644 index 00000000000..cab314486cf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/HostedVectorStoreContent.cs @@ -0,0 +1,44 @@ +// 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.Diagnostics; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a vector store that is hosted by the AI service. +/// +/// +/// Unlike which represents a specific file that is hosted by the AI service, +/// represents a vector store that can contain multiple files, indexed +/// for searching. +/// +[DebuggerDisplay("VectorStoreId = {VectorStoreId}")] +public sealed class HostedVectorStoreContent : AIContent +{ + private string _vectorStoreId; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the hosted file store. + /// is . + /// is empty or composed entirely of whitespace. + public HostedVectorStoreContent(string vectorStoreId) + { + _vectorStoreId = Throw.IfNullOrWhitespace(vectorStoreId); + } + + /// + /// Gets or sets the ID of the hosted vector store. + /// + /// is . + /// is empty or composed entirely of whitespace. + public string VectorStoreId + { + get => _vectorStoreId; + set => _vectorStoreId = Throw.IfNullOrWhitespace(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs index 6662fc420e3..737226a5f7e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; + namespace Microsoft.Extensions.AI; /// Represents a hosted tool that can be specified to an AI service to enable it to execute code it generates. @@ -14,4 +16,12 @@ public class HostedCodeInterpreterTool : AITool public HostedCodeInterpreterTool() { } + + /// Gets or sets a collection of to be used as input to the code interpreter tool. + /// + /// Services support different varied kinds of inputs. Most support the IDs of files that are hosted by the service, + /// represented via . Some also support binary data, represented via . + /// Unsupported inputs will be ignored by the to which the tool is passed. + /// + public IList? Inputs { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs new file mode 100644 index 00000000000..9400efc000f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.AI; + +/// Represents a hosted tool that can be specified to an AI service to enable it to perform file search operations. +/// +/// This tool is designed to facilitate file search functionality within AI services. It allows the service to search +/// for relevant content based on the provided inputs and constraints, such as the maximum number of results. +/// +public class HostedFileSearchTool : AITool +{ + /// Initializes a new instance of the class. + public HostedFileSearchTool() + { + } + + /// Gets or sets a collection of to be used as input to the file search tool. + /// + /// If no explicit inputs are provided, the service will determine what inputs should be searched. Different services + /// support different kinds of inputs, e.g. some may respect using provider-specific file IDs, + /// others may support binary data uploaded as part of the request in , while others may support + /// content in a hosted vector store and represented by a . + /// + public IList? Inputs { get; set; } + + /// Gets or sets a requested bound on the number of matches the tool should produce. + public int? MaximumResultCount { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 81dcda0bc8d..92a5b51d6f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1895,6 +1895,32 @@ "Member": "Microsoft.Extensions.AI.HostedCodeInterpreterTool.HostedCodeInterpreterTool();", "Stage": "Stable" } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedCodeInterpreterTool.Inputs { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.HostedFileSearchTool : Microsoft.Extensions.AI.AITool", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedFileSearchTool.HostedFileSearchTool();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedFileSearchTool.Inputs { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.AI.HostedFileSearchTool.MaximumResultCount { get; set; }", + "Stage": "Stable" + } ] }, { @@ -1907,6 +1933,38 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedFileContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedFileContent.HostedFileContent(string fileId);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.HostedFileContent.FileId { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedVectorStoreContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedVectorStoreContent.HostedVectorStoreContent(string vectorStoreId);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.HostedVectorStoreContent.VectorStoreId { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "interface Microsoft.Extensions.AI.IChatClient : System.IDisposable", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md new file mode 100644 index 00000000000..1e2d8fa5343 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -0,0 +1,66 @@ +# Release History + +## NOT YET RELEASED + +- Updated to depend on Azure.AI.Inference 1.0.0-beta.5. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.7.0-preview.1.25356.2 + +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.6.0-preview.1.25310.2 + +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.5.0-preview.1.25265.7 + +- Added `AsIEmbeddingGenerator` for Azure.AI.Inference `ImageEmbeddingsClient`. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.4-preview.1.25259.16 + +- Added an `AsIEmbeddingGenerator` extension method for `ImageEmbeddingsClient`. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.3-preview.1.25230.7 + +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.0-preview.1.25207.5 + +- Updated to Azure.AI.Inference 1.0.0-beta.4. +- Renamed `AsChatClient`/`AsEmbeddingGenerator` extension methods to `AsIChatClient`/`AsIEmbeddingGenerator`. +- Removed the public `AzureAIInferenceChatClient`/`AzureAIInferenceEmbeddingGenerator` types. These are only created now via the extension methods. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.3.0-preview.1.25161.3 + +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.3.0-preview.1.25114.11 + +- Updated to use Azure.AI.Inference 1.0.0-beta.3, adding support for structured output and audio input. + +## 9.1.0-preview.1.25064.3 + +- Fixed handling of text-only user messages. + +## 9.0.1-preview.1.24570.5 + + - Made the `ToolCallJsonSerializerOptions` property non-nullable. + +## 9.0.0-preview.9.24556.5 + +- Fixed `AzureAIInferenceEmbeddingGenerator` to respect `EmbeddingGenerationOptions.Dimensions`. + +## 9.0.0-preview.9.24525.1 + +- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. +- Updated to use Azure.AI.Inference 1.0.0-beta.2. +- Added `AzureAIInferenceEmbeddingGenerator` and corresponding `AsEmbeddingGenerator` extension method. +- Improved handling of assistant messages that include both text and function call content. + +## 9.0.0-preview.9.24507.7 + +- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md new file mode 100644 index 00000000000..7290b2b6fe6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -0,0 +1,88 @@ +# Release History + +## NOT YET RELEASED + +- Updated to depend on OpenAI 2.3.0. +- Added more conversion helpers for converting bidirectionally between Microsoft.Extensions.AI messages and OpenAI messages. +- Fixed handling of multiple response messages in the Responses `IChatClient`. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.7.1-preview.1.25365.4 + +- Added some conversion helpers for converting Microsoft.Extensions.AI messages to OpenAI messages. + +## 9.7.0-preview.1.25356.2 + +- Updated to depend on OpenAI 2.2.0. +- Added conversion helpers from `AIFunction` to various OpenAI tool representations. +- Added `AsIChatClient` extension method for OpenAI's `AssistantClient`, enabling `IChatClient` to be used with OpenAI Assistants. +- Tweaked how JSON schemas for functions are transformed for better compatibility with OpenAI `strict` constraints. +- Improved handling of `RawRepresentation` in `IChatClients` for Responses and Chat Completion APIs. +- Improved `ISpeechToTextClient` implementation to support streaming transcriptions. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.6.0-preview.1.25310.2 + +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.5.0-preview.1.25265.7 + +- Added PDF support to `IChatClient` implementations. +- Disabled use of `strict` schema handling by default. +- Added support for creating `ErrorContent` in `IChatClient` implementations, such as for refusals. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.4-preview.1.25259.16 + +- Made `IChatClient` implementation more resilient with non-OpenAI services. +- Added `ErrorContent` to represent refusals. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.3-preview.1.25230.7 + +- Reverted previous change that enabled `strict` schemas by default. +- Updated `IChatClient` implementations to support `DataContent`s for PDFs. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.0-preview.1.25207.5 + +- Updated to OpenAI 2.2.0-beta-4. +- Added `AsISpeechToTextClient` extension method for `AudioClient`. +- Removed `AsChatClient(OpenAIClient)`/`AsEmbeddingGenerator(OpenAIClient)` extension methods, renamed the remaining `AsChatClient` methods to `AsIChatClient`, renamed the remaining `AsEmbeddingGenerator` methods to `AsIEmbeddingGenerator`, and added an `AsIChatClient` for `OpenAIResponseClient`. +- Removed the public `OpenAIChatClient`/`OpenAIEmbeddingGenerator` types. These are only created now via the extension methods. +- Removed serialization/deserialization helpers. +- Updated to support pulling propagating image detail from `AdditionalProperties`. +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.3.0-preview.1.25161.3 + +- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. + +## 9.3.0-preview.1.25114.11 + +- Updated to depend on OpenAI 2.2.0-beta.1, updating with support for the Developer role, audio input and output, and additional options like output prediction. It is now also compatible with NativeAOT. +- Added an `AsChatClient` extension method for OpenAI's `AssistantClient`, enabling `IChatClient` to be used with OpenAI Assistants. +- Improved the OpenAI serialization helpers, including a custom converter for the `OpenAIChatCompletionRequest` envelope type. + +## 9.1.0-preview.1.25064.3 + +- Updated to depend on OpenAI 2.1.0. +- Updated to propagate `Metadata` and `StoredOutputEnabled` from `ChatOptions.AdditionalProperties`. +- Added serialization helpers methods for deserializing OpenAI compatible JSON into the Microsoft.Extensions.AI object model, and vice versa serializing the Microsoft.Extensions.AI object model into OpenAI compatible JSON. + +## 9.0.1-preview.1.24570.5 + + - Upgraded to depend on the 2.1.0-beta.2 version of the OpenAI NuGet package. + - Added the `OpenAIRealtimeExtensions` class, with `ToConversationFunctionTool` and `HandleToolCallsAsync` extension methods for using `AIFunction` with the OpenAI Realtime API. + - Made the `ToolCallJsonSerializerOptions` property non-nullable. + +## 9.0.0-preview.9.24525.1 + +- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. +- Improved handling of system messages that include multiple content items. +- Improved handling of assistant messages that include both text and function call content. +- Fixed handling of streaming updates containing empty payloads. + +## 9.0.0-preview.9.24507.7 + +- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index bca2802460c..78bdc72d424 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -16,6 +16,7 @@ #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable SA1005 // Single line comments should begin with single space #pragma warning disable SA1204 // Static elements should appear before instance elements +#pragma warning disable S103 // Lines should not be too long #pragma warning disable S125 // Sections of code should not be commented out #pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex @@ -90,7 +91,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( _ = Throw.IfNull(messages); // Extract necessary state from messages and options. - (RunCreationOptions runOptions, List? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false); + (RunCreationOptions runOptions, ToolResources? toolResources, List? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false); // Get the thread ID. string? threadId = options?.ConversationId ?? _defaultThreadId; @@ -135,7 +136,11 @@ public async IAsyncEnumerable GetStreamingResponseAsync( if (threadId is null) { // No thread ID was provided, so create a new thread. - ThreadCreationOptions threadCreationOptions = new(); + ThreadCreationOptions threadCreationOptions = new() + { + ToolResources = toolResources, + }; + foreach (var message in runOptions.AdditionalMessages) { threadCreationOptions.InitialMessages.Add(message); @@ -293,7 +298,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( /// Creates the to use for the request and extracts any function result contents /// that need to be submitted as tool results. /// - private async ValueTask<(RunCreationOptions RunOptions, List? ToolResults)> CreateRunOptionsAsync( + private async ValueTask<(RunCreationOptions RunOptions, ToolResources? Resources, List? ToolResults)> CreateRunOptionsAsync( IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken) { // Create the options instance to populate, either a fresh or using one the caller provides. @@ -301,6 +306,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( options?.RawRepresentationFactory?.Invoke(this) as RunCreationOptions ?? new(); + ToolResources? resources = null; + // Populate the run options from the ChatOptions, if provided. if (options is not null) { @@ -339,8 +346,44 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; - case HostedCodeInterpreterTool: - runOptions.ToolsOverride.Add(new CodeInterpreterToolDefinition()); + case HostedCodeInterpreterTool codeInterpreterTool: + var interpreterToolDef = ToolDefinition.CreateCodeInterpreter(); + runOptions.ToolsOverride.Add(interpreterToolDef); + + if (codeInterpreterTool.Inputs?.Count is > 0) + { + ThreadInitializationMessage? threadInitializationMessage = null; + foreach (var input in codeInterpreterTool.Inputs) + { + if (input is HostedFileContent hostedFile) + { + threadInitializationMessage ??= new(MessageRole.User, [MessageContent.FromText("attachments")]); + threadInitializationMessage.Attachments.Add(new(hostedFile.FileId, [interpreterToolDef])); + } + } + + if (threadInitializationMessage is not null) + { + runOptions.AdditionalMessages.Add(threadInitializationMessage); + } + } + + break; + + case HostedFileSearchTool fileSearchTool: + runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); + if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs) + { + foreach (var input in fileSearchInputs) + { + if (input is HostedVectorStoreContent file) + { + (resources ??= new()).FileSearch ??= new(); + resources.FileSearch.VectorStoreIds.Add(file.VectorStoreId); + } + } + } + break; } } @@ -469,7 +512,7 @@ void AppendSystemInstructions(string? toAppend) runOptions.AdditionalInstructions = instructions?.ToString(); - return (runOptions, functionResults); + return (runOptions, resources, functionResults); } /// Convert instances to instances. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 80ca0fc1237..7bffbc79d10 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -281,6 +281,9 @@ internal static List ToOpenAIChatContent(IEnumerable))] [JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(JsonElement))] internal sealed partial class OpenAIJsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index a85d2294a7b..afb03b518d9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -148,7 +149,7 @@ internal static IEnumerable ToChatMessages(IEnumerable().Select(c => c.VectorStoreId) ?? [], + fileSearchTool.MaximumResultCount)); + break; + + case HostedCodeInterpreterTool codeTool: + string json; + if (codeTool.Inputs is { Count: > 0 } inputs) + { + string jsonArray = JsonSerializer.Serialize( + inputs.OfType().Select(c => c.FileId), + OpenAIJsonContext.Default.IEnumerableString); + json = $$"""{"type":"code_interpreter","container":{"type":"auto",files:{{jsonArray}}} }"""; + } + else + { + json = """{"type":"code_interpreter","container":"auto"}"""; + } + + result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json))); + break; } } @@ -493,7 +517,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable ToAIContents(IEnumerable con { switch (part.Kind) { - case ResponseContentPartKind.OutputText: - TextContent text = new(part.Text) - { - RawRepresentation = part, - }; - + case ResponseContentPartKind.InputText or ResponseContentPartKind.OutputText: + TextContent text = new(part.Text) { RawRepresentation = part }; PopulateAnnotations(part, text); - results.Add(text); break; + case ResponseContentPartKind.InputFile: + if (!string.IsNullOrWhiteSpace(part.InputImageFileId)) + { + results.Add(new HostedFileContent(part.InputImageFileId) { RawRepresentation = part }); + } + else if (!string.IsNullOrWhiteSpace(part.InputFileId)) + { + results.Add(new HostedFileContent(part.InputFileId) { RawRepresentation = part }); + } + else if (part.InputFileBytes is not null) + { + results.Add(new DataContent(part.InputFileBytes, part.InputFileBytesMediaType ?? "application/octet-stream") + { + Name = part.InputFilename, + RawRepresentation = part, + }); + } + + break; + case ResponseContentPartKind.Refusal: results.Add(new ErrorContent(part.Refusal) { @@ -621,10 +660,7 @@ private static List ToAIContents(IEnumerable con break; default: - results.Add(new() - { - RawRepresentation = part, - }); + results.Add(new() { RawRepresentation = part }); break; } } @@ -652,7 +688,7 @@ private static void PopulateAnnotations(ResponseContentPart source, AIContent de } /// Convert a list of s to a list of . - private static List ToOpenAIResponsesContent(IList contents) + private static List ToResponseContentParts(IList contents) { List parts = []; foreach (var content in contents) @@ -679,6 +715,10 @@ private static List ToOpenAIResponsesContent(IList`. +- Removed debug-level logging of updates in `LoggingChatClient`. +- Avoided caching in `CachingChatClient` when `ConversationId` is set. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + +## 9.4.4-preview.1.25259.16 + +- Fixed `CachingChatClient` to avoid caching when `ConversationId` is set. +- Renamed `useJsonSchema` parameter in `GetResponseAsync` to `useJsonSchemaResponseFormat`. +- Updated `OpenTelemetryChatClient` and `OpenTelemetryEmbeddingGenerator` to conform to the latest 1.33.0 draft specification of the Semantic Conventions for Generative AI systems. + +## 9.4.3-preview.1.25230.7 + +- Updated the diagnostic spans emitted by `FunctionInvokingChatClient` to include total input and output token counts. +- Updated `AIFunctionFactory` to recognize `[FromKeyedServices]` attribute on parameters in order to resolve those parameters from the `IServiceProvider`. +- Added `AIFunctionFactoryOptions.Services`, and used it with `IServiceProviderIsService` to automatically resolve `IServiceProvider`-based parameters in `AIFunction` methods. +- Added `ChatOptions.AllowMultipleToolCalls`. +- Changed `AIJsonSchemaCreateOptions.RequireAllProperties` to default to `false` instead of `true`. +- Unsealed `AIFunctionArguments`. +- Added `AIFunctionArguments` constructors accepting `IEqualityComparer` arguments. +- Unsealed `FunctionInvocationContext`. +- Added `FunctionInvocationContext.IsStreaming`. +- Added protected `FunctionInvokingChatClient.FunctionInvocationServices` property to surface the corresponding `IServiceProvider` provided at construction time. +- Changed protected virtual `FunctionInvokingChatClient.InvokeFunctionAsync` to return `ValueTask` instead of `Task`. Diagnostics are now emitted even if the method is overridden. +- Added `FunctionInvocationResult.Terminate`. + +## 9.4.0-preview.1.25207.5 + +- Updated `GetResponseAsync` to default to using JSON-schema based structured output by default. +- Updated `AIFunctionFactory` with support for customizable marshaling of function parameters and return values. +- Updated `AIFunctionFactory` with a new `Create` overload that supports creating a new receiver instance on each invocation, with either Activator.CreateInstance or ActivatorUtilities.CreateInstance. +- Updated `AIFunctionFactory` to support injecting an `IServiceProvider` as an argument, sourced from the `AIFunctionArguments` passed to `AIFunction.InvokeAsync`. +- Simplified `FunctionInvokingChatClient` error handling, removing `RetryOnError` and replacing it with `MaximumConsecutiveErrorsPerRequest`. +- `FunctionInvokingChatClient` will now ensure that it invokes `AIFunction`s in the same `SynchronizationContext` that `GetResponseAsync`/`GetStreamingResponseAsync` was called in. +- `OpenTelemetryChatClient` now considers `AdditionalProperties` to be sensitive and will only use that data as tags when `EnableSensitiveData` is set to `true`. +- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.32 draft specification of the Semantic Conventions for Generative AI systems. +- Updated the key used by `DistributedCachingChatClient` to employ SHA384 instead of SHA256. +- Lowered `System.Text.Json` 9.x dependency to 8.x when targeting `net8.0` or older. + +## 9.3.0-preview.1.25161.3 + +- Added caching to `AIFunctionFactory.Create` to improve performance of creating the same functions repeatedly. As part of this, `AIJsonSchemaCreateOptions` now implements `IEquatable`. +- Removed the public `AnonymousDelegatingChatClient`/`AnonymousDelegatingEmbeddingGenerator`. Their functionality is still available via the `Use` methods on the builders. +- Changed those `Use` methods to use `Func<...>` rather than a custom delegate type. +- `AIFunctionFactory.Create` now supports `CancellationToken` parameters, and the `AIFunctionContext` type that had served to enable that has been removed. +- Made `FunctionInvokingChatClient.CurrentContext`'s setter `protected`. +- Renamed `FunctionInvokingChatClient.DetailedErrors` to `IncludeDetailedErrors`. +- Renamed `FunctionInvokingChatClient.ConcurrentInvocation` to `AllowConcurrentInvocation`. +- Removed `FunctionInvokingChatClient.KeepFunctionCallingContent`, as it's no longer relevant now that the input messages are an `IEnumerable` rather than an `IList`. +- Renamed `FunctionStatus` to `FunctionInvocationStatus`. +- Renamed `FunctionInvocationStatus.Failed` to `FunctionInvocationStatus.Exception`. +- Moved the nested `FunctionInvocationContext` type to be a peer of `FunctionInvokingChatClient` rather than nested within it. +- Made the `serviceKey` parameters to `AddKeyedChatClient`/`AddKeyedEmbeddingGenerator` nullable. +- Improved `FunctionInvokingChatClient.GetStreamingResponseAsync` to send back to the inner client all content received until that point, and to stream back to the caller messages it generates (e.g. tool responses). +- Improved `AddEmbeddingGenerator` and `AddKeyedEmbeddingGenerator` to register for both the generic and non-generic interfaces. + +## 9.3.0-preview.1.25114.11 + +- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.30.0 draft specification of the Semantic Conventions for Generative AI systems. + +## 9.1.0-preview.1.25064.3 + +- Added `FunctionInvokingChatClient.CurrentContext` to give functions access to detailed function invocation information. +- Updated `OpenTelemetryChatClient`/`OpenTelemetryEmbeddingGenerator` to conform to the latest 1.29.0 draft specification of the Semantic Conventions for Generative AI systems. +- Updated `FunctionInvokingChatClient` to emit an `Activity`/span around all interactions related to a single chat operation. + +## 9.0.1-preview.1.24570.5 + +- Moved the `AddChatClient`, `AddKeyedChatClient`, `AddEmbeddingGenerator`, and `AddKeyedEmbeddingGenerator` extension methods to the `Microsoft.Extensions.DependencyInjection` namespace, changed them to register singleton instances instead of scoped instances, and changed them to support lambda-less chaining. +- Renamed `UseChatOptions`/`UseEmbeddingOptions` to `ConfigureOptions`, and changed the behavior to always invoke the delegate with a safely-mutable instance, either a new instance if the caller provided null, or a clone of the provided instance. +- Renamed the final `Use` method for building a builder to be named `Build`. The inner client instance is passed to the constructor and the `IServiceProvider` is optionally passed to the `Build` method. +- Added `AsBuilder` extension methods to `IChatClient`/`IEmbeddingGenerator` to create builders from the instances. +- Changed the `CachingChatClient`/`CachingEmbeddingGenerator`.`GetCacheKey` method to accept a `params ReadOnlySpan`, included the `ChatOptions`/`EmbeddingGeneratorOptions` as part of the caching key, and reduced memory allocation. +- Added support for anonymous delegating `IChatClient`/`IEmbeddingGenerator` implementations, with `Use` methods on `ChatClientBuilder`/`EmbeddingGeneratorBuilder` that enable the implementations of the core methods to be supplied as lambdas. +- Changed `UseLogging` to accept an `ILoggerFactory` rather than `ILogger`. +- Reversed the order of the `IChatClient`/`IEmbeddingGenerator` and `IServiceProvider` arguments to used by one of the `Use` overloads. +- Added logging capabilities to `FunctionInvokingChatClient`. `UseFunctionInvocation` now accepts an optional `ILoggerFactory`. +- Fixed the `FunctionInvokingChatClient` to include usage data for non-streaming completions in the augmented history. +- Fixed the `FunctionInvokingChatClient` streaming support to appropriately fail for multi-choice completions. +- Fixed the `FunctionInvokingChatClient` to stop yielding function calling content that was already being handled. +- Improved the documentation. + +## 9.0.0-preview.9.24556.5 + +- Added `UseEmbeddingGenerationOptions` and corresponding `ConfigureOptionsEmbeddingGenerator`. + +## 9.0.0-preview.9.24525.1 + +- Added new `AIJsonUtilities` and `AIJsonSchemaCreateOptions` classes. +- Made `AIFunctionFactory.Create` safe for use with Native AOT. +- Simplified the set of `AIFunctionFactory.Create` overloads. +- Changed the default for `FunctionInvokingChatClient.ConcurrentInvocation` from `true` to `false`. +- Improved the readability of JSON generated as part of logging. +- Fixed handling of generated JSON schema names when using arrays or generic types. +- Improved `CachingChatClient`'s coalescing of streaming updates, including reduced memory allocation and enhanced metadata propagation. +- Updated `OpenTelemetryChatClient` and `OpenTelemetryEmbeddingGenerator` to conform to the latest 1.28.0 draft specification of the Semantic Conventions for Generative AI systems. +- Improved `CompleteAsync`'s structured output support to handle primitive types, enums, and arrays. + +## 9.0.0-preview.9.24507.7 + +- Initial Preview diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedFileContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedFileContentTests.cs new file mode 100644 index 00000000000..d8c0f73cc32 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedFileContentTests.cs @@ -0,0 +1,52 @@ +// 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 HostedFileContentTests +{ + [Fact] + public void Constructor_InvalidInput_Throws() + { + Assert.Throws(() => new HostedFileContent(null!)); + Assert.Throws(() => new HostedFileContent(string.Empty)); + Assert.Throws(() => new HostedFileContent(" ")); + } + + [Fact] + public void Constructor_String_PropsDefault() + { + string fileId = "id123"; + HostedFileContent c = new(fileId); + Assert.Null(c.RawRepresentation); + Assert.Null(c.AdditionalProperties); + Assert.Equal(fileId, c.FileId); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + HostedFileContent c = new("id123"); + Assert.Equal("id123", c.FileId); + + c.FileId = "id456"; + Assert.Equal("id456", c.FileId); + + Assert.Throws(() => c.FileId = null!); + Assert.Throws(() => c.FileId = string.Empty); + Assert.Throws(() => c.FileId = " "); + + Assert.Null(c.RawRepresentation); + object raw = new(); + c.RawRepresentation = raw; + Assert.Same(raw, c.RawRepresentation); + + Assert.Null(c.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + c.AdditionalProperties = props; + Assert.Same(props, c.AdditionalProperties); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedVectorStoreContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedVectorStoreContentTests.cs new file mode 100644 index 00000000000..2dd12f53974 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/HostedVectorStoreContentTests.cs @@ -0,0 +1,52 @@ +// 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 HostedVectorStoreContentTests +{ + [Fact] + public void Constructor_InvalidInput_Throws() + { + Assert.Throws(() => new HostedVectorStoreContent(null!)); + Assert.Throws(() => new HostedVectorStoreContent(string.Empty)); + Assert.Throws(() => new HostedVectorStoreContent(" ")); + } + + [Fact] + public void Constructor_String_PropsDefault() + { + HostedVectorStoreContent c = new("id123"); + Assert.Null(c.RawRepresentation); + Assert.Null(c.AdditionalProperties); + Assert.Equal("id123", c.VectorStoreId); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + HostedVectorStoreContent c = new("id123"); + + Assert.Equal("id123", c.VectorStoreId); + c.VectorStoreId = "id456"; + Assert.Equal("id456", c.VectorStoreId); + + Assert.Throws(() => c.VectorStoreId = null!); + Assert.Throws(() => c.VectorStoreId = string.Empty); + Assert.Throws(() => c.VectorStoreId = " "); + Assert.Equal("id456", c.VectorStoreId); + + Assert.Null(c.RawRepresentation); + object raw = new(); + c.RawRepresentation = raw; + Assert.Same(raw, c.RawRepresentation); + + Assert.Null(c.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + c.AdditionalProperties = props; + Assert.Same(props, c.AdditionalProperties); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs index f69ffc5b399..e7e80c55048 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs @@ -14,6 +14,25 @@ public void Constructor_Roundtrips() Assert.Equal(nameof(HostedCodeInterpreterTool), tool.Name); Assert.Empty(tool.Description); Assert.Empty(tool.AdditionalProperties); + Assert.Null(tool.Inputs); Assert.Equal(nameof(HostedCodeInterpreterTool), tool.ToString()); } + + [Fact] + public void Properties_Roundtrip() + { + var tool = new HostedCodeInterpreterTool + { + Inputs = + [ + new HostedFileContent("id123"), + new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream") + ] + }; + + Assert.NotNull(tool.Inputs); + Assert.Equal(2, tool.Inputs.Count); + Assert.IsType(tool.Inputs[0]); + Assert.IsType(tool.Inputs[1]); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs new file mode 100644 index 00000000000..418689dda4b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class HostedFileSearchToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + var tool = new HostedFileSearchTool(); + Assert.Equal(nameof(HostedFileSearchTool), tool.Name); + Assert.Empty(tool.Description); + Assert.Empty(tool.AdditionalProperties); + Assert.Null(tool.Inputs); + Assert.Null(tool.MaximumResultCount); + Assert.Equal(nameof(HostedFileSearchTool), tool.ToString()); + } + + [Fact] + public void Properties_Roundtrip() + { + var tool = new HostedFileSearchTool + { + Inputs = + [ + new HostedVectorStoreContent("id123"), + new HostedFileContent("id456"), + ], + MaximumResultCount = 10, + }; + + Assert.NotNull(tool.Inputs); + Assert.Equal(2, tool.Inputs.Count); + Assert.Equal(10, tool.MaximumResultCount); + Assert.IsType(tool.Inputs[0]); + Assert.IsType(tool.Inputs[1]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs index 90bcf9f2632..4ef19182cbf 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -12,6 +12,7 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.TestUtilities; using OpenAI.Assistants; using Xunit; @@ -45,6 +46,22 @@ public class OpenAIAssistantChatClientIntegrationTests : ChatClientIntegrationTe public override Task MultiModal_DescribeImage() => Task.CompletedTask; public override Task MultiModal_DescribePdf() => Task.CompletedTask; + [ConditionalFact] + public async Task UseCodeInterpreter_ProducesCodeExecutionResults() + { + SkipIfNotEnabled(); + + var response = await ChatClient.GetResponseAsync("Use the code interpreter to calculate the square root of 42.", new() + { + Tools = [new HostedCodeInterpreterTool()], + }); + Assert.NotNull(response); + + ChatMessage message = Assert.Single(response.Messages); + + Assert.NotEmpty(message.Text); + } + // [Fact] // uncomment and run to clear out _all_ threads in your OpenAI account public async Task DeleteAllThreads() { From 7d2ea04a579a1712170b2fe34a7184dfdd80f8d1 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Mon, 11 Aug 2025 20:45:54 -0700 Subject: [PATCH 251/472] Add support for text-to-image (#6648) * Add ITextToImageClient * Remove URI based edit since it's not available * Add filename for edit * Add OpenAI implmentation of ITextToImageClient * Fix tests * Add tests for TextToImage * Add DeletgatingTextToImageClient and tests * Add integration test and fix some bugs * Add remaining support to MEAI for TextToImage * Make all TextToImageOptions optional These are all nullable now so that the client can use defaults where appropriate. Remove quality default since it's not consistent across models. Also remove setting ResponseFormat since this is not supported by gpt-image-1. * Address feedback * Document some exceptions * Address feedback * Make EditImageAsync plural OpenAI's image API supports multiple images and this does seem to be common functionality and a better generalization. The client library doesn't expose this yet, but we should account for it. Image models may be capable of things like "Combine the subjects of these images into a single image" or "Create a single image that uses the subject from the first image and background for the second" etc. * Address feedback and add/fix tests. * Fix bad merge * Address feedback * Fix test * Use DataContent.Name for filename. * Add extensions for EditImageAsync Extension that accepts a single DataContent and one that accepts a byte[]. I've left out streams and file paths, since these require more opinions about how to load them. I filed #6683 to address streams. * Fix test * Remove use of `_model` field. * Rename ImageToText to Image * Rename TextToImage directories to Image * Rename files TextToImage -> Image * Add new request and response type * Make GenerateImagesAsync accept ImageRequest * Remove EditImageAsync * Adding GenerateStreamingImagesAsync * Update docs * Rename ImageClient ImageGenerator * Fix up some text-to-image references * Rename Image(Options|Request|Response) * Remove `Images` from `GenerateImagesAsync` * Remove streaming method We don't yet have any good public support for streaming to vet this API We can guess at how it might behave for OpenAI, but that doesn't really give enough confidence to build the API around it. * Address feedback * Provide OpenAI an appropriate filename * Remove Style from ImageGenerationOptions --- .../Image/DelegatingImageGenerator.cs | 69 ++++++ .../Image/IImageGenerator.cs | 37 +++ .../Image/ImageGenerationOptions.cs | 103 ++++++++ .../Image/ImageGenerationRequest.cs | 45 ++++ .../Image/ImageGenerationResponse.cs | 52 ++++ .../Image/ImageGeneratorExtensions.cs | 215 ++++++++++++++++ .../Image/ImageGeneratorMetadata.cs | 43 ++++ .../Utilities/AIJsonUtilities.Defaults.cs | 2 + .../OpenAIClientExtensions.cs | 9 + .../OpenAIImageGenerator.cs | 234 ++++++++++++++++++ .../Image/ConfigureOptionsImageGenerator.cs | 53 ++++ ...eOptionsImageGeneratorBuilderExtensions.cs | 39 +++ .../Image/ImageGeneratorBuilder.cs | 85 +++++++ ...eneratorBuilderImageGeneratorExtensions.cs | 28 +++ ...ratorBuilderServiceCollectionExtensions.cs | 85 +++++++ .../Image/LoggingImageGenerator.cs | 124 ++++++++++ .../LoggingImageGeneratorBuilderExtensions.cs | 57 +++++ .../Image/DelegatingImageGeneratorTests.cs | 142 +++++++++++ .../Image/ImageGenerationOptionsTests.cs | 135 ++++++++++ .../Image/ImageGenerationResponseTests.cs | 154 ++++++++++++ .../Image/ImageGeneratorExtensionsTests.cs | 222 +++++++++++++++++ .../Image/ImageGeneratorMetadataTests.cs | 29 +++ .../Image/ImageGeneratorTests.cs | 155 ++++++++++++ .../TestImageGenerator.cs | 43 ++++ .../TestJsonSerializerContext.cs | 2 + .../ImageGeneratorIntegrationTests.cs | 125 ++++++++++ ...oft.Extensions.AI.Integration.Tests.csproj | 1 + .../README.md | 2 + .../OpenAIImageGeneratorIntegrationTests.cs | 12 + .../OpenAIImageGeneratorTests.cs | 45 ++++ .../ConfigureOptionsImageGeneratorTests.cs | 70 ++++++ ...ageGeneratorDependencyInjectionPatterns.cs | 162 ++++++++++++ .../Image/LoggingImageGeneratorTests.cs | 143 +++++++++++ .../SingletonImageGeneratorExtensions.cs | 11 + .../Microsoft.Extensions.AI.Tests.csproj | 1 + 35 files changed, 2734 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs new file mode 100644 index 00000000000..91ffb136af5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs @@ -0,0 +1,69 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +/// +/// This is recommended as a base type when building generators that can be chained in any order around an underlying . +/// The default implementation simply passes each call to the inner generator instance. +/// +[Experimental("MEAI001")] +public class DelegatingImageGenerator : IImageGenerator +{ + /// + /// Initializes a new instance of the class. + /// + /// The wrapped generator instance. + /// is . + protected DelegatingImageGenerator(IImageGenerator innerGenerator) + { + InnerGenerator = Throw.IfNull(innerGenerator); + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// Gets the inner . + protected IImageGenerator InnerGenerator { get; } + + /// + public virtual Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerGenerator.GenerateAsync(request, options, cancellationToken); + } + + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + // If the key is non-null, we don't know what it means so pass through to the inner service. + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + InnerGenerator.GetService(serviceType, serviceKey); + } + + /// Provides a mechanism for releasing unmanaged resources. + /// if being called from ; otherwise, . + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + InnerGenerator.Dispose(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs new file mode 100644 index 00000000000..e630ecff8e9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs @@ -0,0 +1,37 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a generator of images. +/// +[Experimental("MEAI001")] +public interface IImageGenerator : IDisposable +{ + /// + /// Sends an image generation request and returns the generated image as a . + /// + /// The image generation request containing the prompt and optional original images for editing. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// is . + /// The images generated by the . + Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default); + + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that might be provided by the , + /// including itself or any services it might be wrapping. + /// + object? GetService(Type serviceType, object? serviceKey = null); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs new file mode 100644 index 00000000000..f68aebd5b06 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs @@ -0,0 +1,103 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// Represents the options for an image generation request. +[Experimental("MEAI001")] +public class ImageGenerationOptions +{ + /// + /// Gets or sets the number of images to generate. + /// + public int? Count { get; set; } + + /// + /// Gets or sets the size of the generated image. + /// + /// + /// If a provider only supports fixed sizes the closest supported size will be used. + /// + public Size? ImageSize { get; set; } + + /// + /// Gets or sets the media type (also known as MIME type) of the generated image. + /// + public string? MediaType { get; set; } + + /// + /// Gets or sets the model ID to use for image generation. + /// + public string? ModelId { get; set; } + + /// + /// Gets or sets a callback responsible for creating the raw representation of the image generation options from an underlying implementation. + /// + /// + /// The underlying implementation may have its own representation of options. + /// When is invoked with an , + /// that implementation may convert the provided options into its own representation in order to use it while performing + /// the operation. For situations where a consumer knows which concrete is being used + /// and how it represents options, a new instance of that implementation-specific options type may be returned by this + /// callback, for the implementation to use instead of creating a new instance. + /// Such implementations may mutate the supplied options instance further based on other settings supplied on this + /// instance or from other inputs, therefore, it is strongly recommended to not + /// return shared instances and instead make the callback return a new instance on each call. + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// properties on . + /// + [JsonIgnore] + public Func? RawRepresentationFactory { get; set; } + + /// + /// Gets or sets the response format of the generated image. + /// + public ImageGenerationResponseFormat? ResponseFormat { get; set; } + + /// Produces a clone of the current instance. + /// A clone of the current instance. + public virtual ImageGenerationOptions Clone() + { + ImageGenerationOptions options = new() + { + Count = Count, + MediaType = MediaType, + ImageSize = ImageSize, + ModelId = ModelId, + RawRepresentationFactory = RawRepresentationFactory, + ResponseFormat = ResponseFormat + }; + + return options; + } +} + +/// +/// Represents the requested response format of the generated image. +/// +/// +/// Not all implementations support all response formats and this value may be ignored by the implementation if not supported. +/// +[Experimental("MEAI001")] +public enum ImageGenerationResponseFormat +{ + /// + /// The generated image is returned as a URI pointing to the image resource. + /// + Uri, + + /// + /// The generated image is returned as in-memory image data. + /// + Data, + + /// + /// The generated image is returned as a hosted resource identifier, which can be used to retrieve the image later. + /// + Hosted, +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs new file mode 100644 index 00000000000..d519d08c731 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// Represents a request for image generation. +[Experimental("MEAI001")] +public class ImageGenerationRequest +{ + /// Initializes a new instance of the class. + public ImageGenerationRequest() + { + } + + /// Initializes a new instance of the class. + /// The prompt to guide the image generation. + public ImageGenerationRequest(string prompt) + { + Prompt = prompt; + } + + /// Initializes a new instance of the class. + /// The prompt to guide the image generation. + /// The original images to base edits on. + public ImageGenerationRequest(string prompt, IEnumerable? originalImages) + { + Prompt = prompt; + OriginalImages = originalImages; + } + + /// Gets or sets the prompt to guide the image generation. + public string? Prompt { get; set; } + + /// + /// Gets or sets the original images to base edits on. + /// + /// + /// If this property is set, the request will behave as an image edit operation. + /// If this property is null or empty, the request will behave as a new image generation operation. + /// + public IEnumerable? OriginalImages { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs new file mode 100644 index 00000000000..22a6a7f0e12 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators + +namespace Microsoft.Extensions.AI; + +/// Represents the result of an image generation request. +[Experimental("MEAI001")] +public class ImageGenerationResponse +{ + /// The content items in the generated text response. + private IList? _contents; + + /// Initializes a new instance of the class. + [JsonConstructor] + public ImageGenerationResponse() + { + } + + /// Initializes a new instance of the class. + /// The contents for this response. + public ImageGenerationResponse(IList? contents) + { + _contents = contents; + } + + /// Gets or sets the raw representation of the image generation response from an underlying implementation. + /// + /// If a is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// + /// Gets or sets the generated content items. Content will typically be DataContent for + /// images streamed from the generator or UriContent for remotely hosted images, but may also + /// be provider specific content types that represent the generated images. + /// + [AllowNull] + public IList Contents + { + get => _contents ??= []; + set => _contents = value; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs new file mode 100644 index 00000000000..93de115c0ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs @@ -0,0 +1,215 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Extensions for . +[Experimental("MEAI001")] +public static class ImageGeneratorExtensions +{ + private static readonly Dictionary _extensionToMimeType = new(StringComparer.OrdinalIgnoreCase) + { + [".png"] = "image/png", + [".jpg"] = "image/jpeg", + [".jpeg"] = "image/jpeg", + [".webp"] = "image/webp", + [".gif"] = "image/gif", + [".bmp"] = "image/bmp", + [".tiff"] = "image/tiff", + [".tif"] = "image/tiff", + }; + + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// The generator. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. + /// + public static TService? GetService(this IImageGenerator generator, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + + return generator.GetService(typeof(TService), serviceKey) is TService service ? service : default; + } + + /// + /// Asks the for an object of the specified type + /// and throws an exception if one isn't available. + /// + /// The generator. + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object. + /// is . + /// is . + /// No service of the requested type for the specified key is available. + /// + /// The purpose of this method is to allow for the retrieval of services that are required to be provided by the , + /// including itself or any services it might be wrapping. + /// + public static object GetRequiredService(this IImageGenerator generator, Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(serviceType); + + return + generator.GetService(serviceType, serviceKey) ?? + throw Throw.CreateMissingServiceException(serviceType, serviceKey); + } + + /// + /// Asks the for an object of type + /// and throws an exception if one isn't available. + /// + /// The type of the object to be retrieved. + /// The generator. + /// An optional key that can be used to help identify the target service. + /// The found object. + /// is . + /// No service of the requested type for the specified key is available. + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that are required to be provided by the , + /// including itself or any services it might be wrapping. + /// + public static TService GetRequiredService(this IImageGenerator generator, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + + if (generator.GetService(typeof(TService), serviceKey) is not TService service) + { + throw Throw.CreateMissingServiceException(typeof(TService), serviceKey); + } + + return service; + } + + /// + /// Generates images based on a text prompt. + /// + /// The image generator. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// or are . + /// The images generated by the generator. + public static Task GenerateImagesAsync( + this IImageGenerator generator, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt), options, cancellationToken); + } + + /// + /// Edits images based on original images and a text prompt. + /// + /// The image generator. + /// The images to base edits on. + /// The prompt to guide the image editing. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// , , or are . + /// The images generated by the generator. + public static Task EditImagesAsync( + this IImageGenerator generator, + IEnumerable originalImages, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(originalImages); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt, originalImages), options, cancellationToken); + } + + /// + /// Edits a single image based on the original image and the specified prompt. + /// + /// The image generator. + /// The single image to base edits on. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// , , or are . + /// The images generated by the generator. + public static Task EditImageAsync( + this IImageGenerator generator, + DataContent originalImage, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(originalImage); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt, [originalImage]), options, cancellationToken); + } + + /// + /// Edits a single image based on a byte array and the specified prompt. + /// + /// The image generator. + /// The byte array containing the image data to base edits on. + /// The filename for the image data. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// + /// , , or are . + /// + /// The images generated by the generator. + public static Task EditImageAsync( + this IImageGenerator generator, + ReadOnlyMemory originalImageData, + string fileName, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(fileName); + _ = Throw.IfNull(prompt); + + // Infer media type from file extension + string mediaType = GetMediaTypeFromFileName(fileName); + + var dataContent = new DataContent(originalImageData, mediaType) { Name = fileName }; + return generator.GenerateAsync(new ImageGenerationRequest(prompt, [dataContent]), options, cancellationToken); + } + + /// + /// Gets the media type based on the file extension. + /// + /// The filename to extract the media type from. + /// The inferred media type. + private static string GetMediaTypeFromFileName(string fileName) + { + string extension = Path.GetExtension(fileName); + + if (_extensionToMimeType.TryGetValue(extension, out string? mediaType)) + { + return mediaType; + } + + return "image/png"; // Default to PNG if unknown extension + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs new file mode 100644 index 00000000000..c5604155285 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs @@ -0,0 +1,43 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// Provides metadata about an . +[Experimental("MEAI001")] +public class ImageGeneratorMetadata +{ + /// Initializes a new instance of the class. + /// + /// The name of the image generation provider, if applicable. Where possible, this should map to the + /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems. + /// + /// The URL for accessing the image generation provider, if applicable. + /// The ID of the image generation model used by default, if applicable. + public ImageGeneratorMetadata(string? providerName = null, Uri? providerUri = null, string? defaultModelId = null) + { + DefaultModelId = defaultModelId; + ProviderName = providerName; + ProviderUri = providerUri; + } + + /// Gets the name of the image generation provider. + /// + /// Where possible, this maps to the appropriate name defined in the + /// OpenTelemetry Semantic Conventions for Generative AI systems. + /// + public string? ProviderName { get; } + + /// Gets the URL for accessing the image generation provider. + public Uri? ProviderUri { get; } + + /// Gets the ID of the default model used by this image generator. + /// + /// This value can be if no default model is set on the corresponding . + /// An individual request may override this value via . + /// + public string? DefaultModelId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 33531661813..4b8a4fb1576 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -70,6 +70,8 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(SpeechToTextResponse))] [JsonSerializable(typeof(SpeechToTextResponseUpdate))] [JsonSerializable(typeof(IReadOnlyList))] + [JsonSerializable(typeof(ImageGenerationOptions))] + [JsonSerializable(typeof(ImageGenerationResponse))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(ChatMessage[]))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 3881f246d98..a5739fdb4ac 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -13,6 +13,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; +using OpenAI.Images; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long @@ -154,6 +155,14 @@ public static IChatClient AsIChatClient(this AssistantClient assistantClient, As public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) => new OpenAISpeechToTextClient(audioClient); + /// Gets an for use with this . + /// The client. + /// An that can be used to generate images via the . + /// is . + [Experimental("MEAI001")] + public static IImageGenerator AsIImageGenerator(this ImageClient imageClient) => + new OpenAIImageGenerator(imageClient); + /// Gets an for use with this . /// The client. /// The number of dimensions to generate in each embedding. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs new file mode 100644 index 00000000000..fe1cb399e0d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -0,0 +1,234 @@ +// 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.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Images; + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + +namespace Microsoft.Extensions.AI; + +/// Represents an for an OpenAI or . +internal sealed class OpenAIImageGenerator : IImageGenerator +{ + private static readonly Dictionary _mimeTypeToExtension = new(StringComparer.OrdinalIgnoreCase) + { + ["image/png"] = ".png", + ["image/jpeg"] = ".jpg", + ["image/webp"] = ".webp", + ["image/gif"] = ".gif", + ["image/bmp"] = ".bmp", + ["image/tiff"] = ".tiff", + }; + + /// Metadata about the client. + private readonly ImageGeneratorMetadata _metadata; + + /// The underlying . + private readonly ImageClient _imageClient; + + /// Initializes a new instance of the class for the specified . + /// The underlying client. + /// is . + public OpenAIImageGenerator(ImageClient imageClient) + { + _ = Throw.IfNull(imageClient); + + _imageClient = imageClient; + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(ImageClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(imageClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; + + _metadata = new("openai", providerUrl, _imageClient.Model); + } + + /// + public async Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + string? prompt = request.Prompt; + _ = Throw.IfNull(prompt); + + // If the request has original images, treat this as an edit operation + if (request.OriginalImages is not null && request.OriginalImages.Any()) + { + ImageEditOptions editOptions = ToOpenAIImageEditOptions(options); + string? fileName = null; + Stream? imageStream = null; + + // Currently only a single image is supported for editing. + var originalImage = request.OriginalImages.FirstOrDefault(); + + if (originalImage is DataContent dataContent) + { + imageStream = MemoryMarshal.TryGetArray(dataContent.Data, out var array) ? + new MemoryStream(array.Array!, array.Offset, array.Count) : + new MemoryStream(dataContent.Data.ToArray()); + fileName = dataContent.Name; + + if (fileName is null) + { + // If no file name is provided, use the default based on the content type. + if (dataContent.MediaType is not null && _mimeTypeToExtension.TryGetValue(dataContent.MediaType, out var extension)) + { + fileName = $"image{extension}"; + } + else + { + fileName = "image.png"; // Default to PNG if no content type is available. + } + } + } + + GeneratedImageCollection editResult = await _imageClient.GenerateImageEditsAsync( + imageStream, fileName, prompt, options?.Count ?? 1, editOptions, cancellationToken).ConfigureAwait(false); + + return ToImageGenerationResponse(editResult); + } + + OpenAI.Images.ImageGenerationOptions openAIOptions = ToOpenAIImageGenerationOptions(options); + + GeneratedImageCollection result = await _imageClient.GenerateImagesAsync(prompt, options?.Count ?? 1, openAIOptions, cancellationToken).ConfigureAwait(false); + + return ToImageGenerationResponse(result); + } + + /// +#pragma warning disable S1067 // Expressions should not be too complex + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : + serviceKey is not null ? null : + serviceType == typeof(ImageGeneratorMetadata) ? _metadata : + serviceType == typeof(ImageClient) ? _imageClient : + serviceType.IsInstanceOfType(this) ? this : + null; +#pragma warning restore S1067 // Expressions should not be too complex + + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IImageGenerator interface. + } + + /// + /// Converts a to an OpenAI . + /// + /// User's requested size. + /// Closest supported size. + private static GeneratedImageSize? ToOpenAIImageSize(Size? requestedSize) => + requestedSize is null ? null : new GeneratedImageSize(requestedSize.Value.Width, requestedSize.Value.Height); + + /// Converts a to a . + private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageCollection generatedImages) + { + string contentType = "image/png"; // Default content type for images + + // OpenAI doesn't expose the content type, so we need to read from the internal JSON representation. + // https://github.com/openai/openai-dotnet/issues/561 + IDictionary? additionalRawData = typeof(GeneratedImageCollection) + .GetProperty("SerializedAdditionalRawData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(generatedImages) as IDictionary; + + if (additionalRawData?.TryGetValue("output_format", out var outputFormat) ?? false) + { + var stringJsonTypeInfo = (JsonTypeInfo)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(string)); + var outputFormatString = JsonSerializer.Deserialize(outputFormat, stringJsonTypeInfo); + contentType = $"image/{outputFormatString}"; + } + + List contents = new(); + + foreach (GeneratedImage image in generatedImages) + { + if (image.ImageBytes is not null) + { + contents.Add(new DataContent(image.ImageBytes.ToMemory(), contentType)); + } + else if (image.ImageUri is not null) + { + contents.Add(new UriContent(image.ImageUri, contentType)); + } + else + { + throw new InvalidOperationException("Generated image does not contain a valid URI or byte array."); + } + } + + return new ImageGenerationResponse(contents) + { + RawRepresentation = generatedImages + }; + } + + /// Converts a to a . + private OpenAI.Images.ImageGenerationOptions ToOpenAIImageGenerationOptions(ImageGenerationOptions? options) + { + OpenAI.Images.ImageGenerationOptions result = options?.RawRepresentationFactory?.Invoke(this) as OpenAI.Images.ImageGenerationOptions ?? new(); + + if (result.OutputFileFormat is null) + { + if (options?.MediaType?.Equals("image/png", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Png; + } + else if (options?.MediaType?.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Jpeg; + } + else if (options?.MediaType?.Equals("image/webp", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Webp; + } + } + + result.ResponseFormat ??= options?.ResponseFormat switch + { + ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, + ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, + + // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. + _ => (GeneratedImageFormat?)null + }; + + result.Size ??= ToOpenAIImageSize(options?.ImageSize); + + return result; + } + + /// Converts a to a . + private ImageEditOptions ToOpenAIImageEditOptions(ImageGenerationOptions? options) + { + ImageEditOptions result = options?.RawRepresentationFactory?.Invoke(this) as ImageEditOptions ?? new(); + + result.ResponseFormat ??= options?.ResponseFormat switch + { + ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, + ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, + + // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. + _ => (GeneratedImageFormat?)null + }; + + result.Size ??= ToOpenAIImageSize(options?.ImageSize); + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs new file mode 100644 index 00000000000..b9e698a33f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs @@ -0,0 +1,53 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents a delegating image generator that configures a instance used by the remainder of the pipeline. +[Experimental("MEAI001")] +public sealed class ConfigureOptionsImageGenerator : DelegatingImageGenerator +{ + /// The callback delegate used to configure options. + private readonly Action _configureOptions; + + /// Initializes a new instance of the class with the specified callback. + /// The inner generator. + /// + /// The delegate to invoke to configure the instance. It is passed a clone of the caller-supplied instance + /// (or a newly constructed instance if the caller-supplied instance is ). + /// + /// or is . + /// + /// The delegate is passed either a new instance of if + /// the caller didn't supply a instance, or a clone (via of the caller-supplied + /// instance if one was supplied. + /// + public ConfigureOptionsImageGenerator(IImageGenerator innerGenerator, Action configure) + : base(innerGenerator) + { + _configureOptions = Throw.IfNull(configure); + } + + /// + public override async Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return await base.GenerateAsync(request, Configure(options), cancellationToken); + } + + /// Creates and configures the to pass along to the inner generator. + private ImageGenerationOptions Configure(ImageGenerationOptions? options) + { + options = options?.Clone() ?? new(); + + _configureOptions(options); + + return options; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..337c80951ef --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1629 // Documentation text should end with a period + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class ConfigureOptionsImageGeneratorBuilderExtensions +{ + /// + /// Adds a callback that configures a to be passed to the next generator in the pipeline. + /// + /// The . + /// + /// The delegate to invoke to configure the instance. + /// It is passed a clone of the caller-supplied instance (or a newly constructed instance if the caller-supplied instance is ). + /// + /// or is . + /// + /// This method can be used to set default options. The delegate is passed either a new instance of + /// if the caller didn't supply a instance, or a clone (via ) + /// of the caller-supplied instance if one was supplied. + /// + /// The . + public static ImageGeneratorBuilder ConfigureOptions( + this ImageGeneratorBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.Use(innerGenerator => new ConfigureOptionsImageGenerator(innerGenerator, configure)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs new file mode 100644 index 00000000000..9070ed8a59c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs @@ -0,0 +1,85 @@ +// 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 Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// A builder for creating pipelines of . +[Experimental("MEAI001")] +public sealed class ImageGeneratorBuilder +{ + private readonly Func _innerGeneratorFactory; + + /// The registered generator factory instances. + private List>? _generatorFactories; + + /// Initializes a new instance of the class. + /// The inner that represents the underlying backend. + /// is . + public ImageGeneratorBuilder(IImageGenerator innerGenerator) + { + _ = Throw.IfNull(innerGenerator); + _innerGeneratorFactory = _ => innerGenerator; + } + + /// Initializes a new instance of the class. + /// A callback that produces the inner that represents the underlying backend. + /// is . + public ImageGeneratorBuilder(Func innerGeneratorFactory) + { + _innerGeneratorFactory = Throw.IfNull(innerGeneratorFactory); + } + + /// Builds an that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn. + /// + /// The that should provide services to the instances. + /// If null, an empty will be used. + /// + /// An instance of that represents the entire pipeline. + public IImageGenerator Build(IServiceProvider? services = null) + { + services ??= EmptyServiceProvider.Instance; + var imageGenerator = _innerGeneratorFactory(services); + + // To match intuitive expectations, apply the factories in reverse order, so that the first factory added is the outermost. + if (_generatorFactories is not null) + { + for (var i = _generatorFactories.Count - 1; i >= 0; i--) + { + imageGenerator = _generatorFactories[i](imageGenerator, services) ?? + throw new InvalidOperationException( + $"The {nameof(ImageGeneratorBuilder)} entry at index {i} returned null. " + + $"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(IImageGenerator)} instances."); + } + } + + return imageGenerator; + } + + /// Adds a factory for an intermediate image generator to the image generator pipeline. + /// The generator factory function. + /// The updated instance. + /// is . + public ImageGeneratorBuilder Use(Func generatorFactory) + { + _ = Throw.IfNull(generatorFactory); + + return Use((innerGenerator, _) => generatorFactory(innerGenerator)); + } + + /// Adds a factory for an intermediate image generator to the image generator pipeline. + /// The generator factory function. + /// The updated instance. + /// is . + public ImageGeneratorBuilder Use(Func generatorFactory) + { + _ = Throw.IfNull(generatorFactory); + + (_generatorFactories ??= []).Add(generatorFactory); + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs new file mode 100644 index 00000000000..e8242287b68 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs @@ -0,0 +1,28 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extension methods for working with in the context of . +[Experimental("MEAI001")] +public static class ImageGeneratorBuilderImageGeneratorExtensions +{ + /// Creates a new using as its inner generator. + /// The generator to use as the inner generator. + /// The new instance. + /// is . + /// + /// This method is equivalent to using the constructor directly, + /// specifying as the inner generator. + /// + public static ImageGeneratorBuilder AsBuilder(this IImageGenerator innerGenerator) + { + _ = Throw.IfNull(innerGenerator); + + return new ImageGeneratorBuilder(innerGenerator); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs new file mode 100644 index 00000000000..cec943da309 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// Provides extension methods for registering with a . +[Experimental("MEAI001")] +public static class ImageGeneratorBuilderServiceCollectionExtensions +{ + /// Registers a singleton in the . + /// The to which the generator should be added. + /// The inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// or is . + /// The generator is registered as a singleton service. + public static ImageGeneratorBuilder AddImageGenerator( + this IServiceCollection serviceCollection, + IImageGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddImageGenerator(serviceCollection, _ => innerGenerator, lifetime); + + /// Registers a singleton in the . + /// The to which the generator should be added. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// or is . + /// The generator is registered as a singleton service. + public static ImageGeneratorBuilder AddImageGenerator( + this IServiceCollection serviceCollection, + Func innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(innerGeneratorFactory); + + var builder = new ImageGeneratorBuilder(innerGeneratorFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(IImageGenerator), builder.Build, lifetime)); + return builder; + } + + /// Registers a keyed singleton in the . + /// The to which the generator should be added. + /// The key with which to associate the generator. + /// The inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// , , or is . + /// The generator is registered as a scoped service. + public static ImageGeneratorBuilder AddKeyedImageGenerator( + this IServiceCollection serviceCollection, + object serviceKey, + IImageGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddKeyedImageGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime); + + /// Registers a keyed singleton in the . + /// The to which the generator should be added. + /// The key with which to associate the generator. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// , , or is . + /// The generator is registered as a scoped service. + public static ImageGeneratorBuilder AddKeyedImageGenerator( + this IServiceCollection serviceCollection, + object serviceKey, + Func innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(serviceKey); + _ = Throw.IfNull(innerGeneratorFactory); + + var builder = new ImageGeneratorBuilder(innerGeneratorFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(IImageGenerator), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime)); + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs new file mode 100644 index 00000000000..2eee33456c1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs @@ -0,0 +1,124 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using static Microsoft.Extensions.AI.OpenTelemetryConsts.GenAI; + +namespace Microsoft.Extensions.AI; + +/// A delegating image generator that logs image generation operations to an . +/// +/// +/// The provided implementation of is thread-safe for concurrent use so long as the +/// employed is also thread-safe for concurrent use. +/// +/// +/// When the employed enables , the contents of +/// prompts and options are logged. These prompts and options may contain sensitive application data. +/// is disabled by default and should never be enabled in a production environment. +/// Prompts and options are not logged at other logging levels. +/// +/// +[Experimental("MEAI001")] +public partial class LoggingImageGenerator : DelegatingImageGenerator +{ + /// An instance used for all logging. + private readonly ILogger _logger; + + /// The to use for serialization of state written to the logger. + private JsonSerializerOptions _jsonSerializerOptions; + + /// Initializes a new instance of the class. + /// The underlying . + /// An instance that will be used for all logging. + /// or is . + public LoggingImageGenerator(IImageGenerator innerGenerator, ILogger logger) + : base(innerGenerator) + { + _logger = Throw.IfNull(logger); + _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; + } + + /// Gets or sets JSON serialization options to use when serializing logging data. + /// The value being set is . + public JsonSerializerOptions JsonSerializerOptions + { + get => _jsonSerializerOptions; + set => _jsonSerializerOptions = Throw.IfNull(value); + } + + /// + public override async Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogInvokedSensitive(nameof(GenerateAsync), request.Prompt ?? string.Empty, AsJson(options), AsJson(this.GetService())); + } + else + { + LogInvoked(nameof(GenerateAsync)); + } + } + + try + { + var response = await base.GenerateAsync(request, options, cancellationToken); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace) && response.Contents.All(c => c is not DataContent)) + { + LogCompletedSensitive(nameof(GenerateAsync), AsJson(response)); + } + else + { + LogCompleted(nameof(GenerateAsync)); + } + } + + return response; + } + catch (OperationCanceledException) + { + LogInvocationCanceled(nameof(GenerateAsync)); + throw; + } + catch (Exception ex) + { + LogInvocationFailed(nameof(GenerateAsync), ex); + throw; + } + } + + private string AsJson(T value) => LoggingHelpers.AsJson(value, _jsonSerializerOptions); + + [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] + private partial void LogInvoked(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: Prompt: {Prompt}. Options: {ImageGenerationOptions}. Metadata: {ImageGeneratorMetadata}.")] + private partial void LogInvokedSensitive(string methodName, string prompt, string imageGenerationOptions, string imageGeneratorMetadata); + + [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] + private partial void LogCompleted(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {ImageGenerationResponse}.")] + private partial void LogCompletedSensitive(string methodName, string imageGenerationResponse); + + [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] + private partial void LogInvocationCanceled(string methodName); + + [LoggerMessage(LogLevel.Error, "{MethodName} failed.")] + private partial void LogInvocationFailed(string methodName, Exception error); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..ece65d942ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class LoggingImageGeneratorBuilderExtensions +{ + /// Adds logging to the image generator pipeline. + /// The . + /// + /// An optional used to create a logger with which logging should be performed. + /// If not supplied, a required instance will be resolved from the service provider. + /// + /// An optional callback that can be used to configure the instance. + /// The . + /// is . + /// + /// + /// When the employed enables , the contents of + /// prompts and options are logged. These prompts and options may contain sensitive application data. + /// is disabled by default and should never be enabled in a production environment. + /// Prompts and options are not logged at other logging levels. + /// + /// + public static ImageGeneratorBuilder UseLogging( + this ImageGeneratorBuilder builder, + ILoggerFactory? loggerFactory = null, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.Use((innerGenerator, services) => + { + loggerFactory ??= services.GetRequiredService(); + + // If the factory we resolve is for the null logger, the LoggingImageGenerator will end up + // being an expensive nop, so skip adding it and just return the inner generator. + if (loggerFactory == NullLoggerFactory.Instance) + { + return innerGenerator; + } + + var imageGenerator = new LoggingImageGenerator(innerGenerator, loggerFactory.CreateLogger(typeof(LoggingImageGenerator))); + configure?.Invoke(imageGenerator); + return imageGenerator; + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs new file mode 100644 index 00000000000..7e8d189b851 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs @@ -0,0 +1,142 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DelegatingImageGeneratorTests +{ + [Fact] + public void RequiresInnerImageGenerator() + { + Assert.Throws("innerGenerator", () => new NoOpDelegatingImageGenerator(null!)); + } + + [Fact] + public async Task GenerateImagesAsyncDefaultsToInnerGeneratorAsync() + { + // Arrange + var expectedRequest = new ImageGenerationRequest("test prompt"); + var expectedOptions = new ImageGenerationOptions(); + var expectedCancellationToken = CancellationToken.None; + var expectedResult = new TaskCompletionSource(); + var expectedResponse = new ImageGenerationResponse(); + using var inner = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCancellationToken, cancellationToken); + return expectedResult.Task; + } + }; + + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var resultTask = delegating.GenerateAsync(expectedRequest, expectedOptions, expectedCancellationToken); + + // Assert + Assert.False(resultTask.IsCompleted); + expectedResult.SetResult(expectedResponse); + Assert.True(resultTask.IsCompleted); + Assert.Same(expectedResponse, await resultTask); + } + + [Fact] + public void GetServiceThrowsForNullType() + { + using var inner = new TestImageGenerator(); + using var delegating = new NoOpDelegatingImageGenerator(inner); + Assert.Throws("serviceType", () => delegating.GetService(null!)); + } + + [Fact] + public void GetServiceReturnsSelfIfCompatibleWithRequestAndKeyIsNull() + { + // Arrange + using var inner = new TestImageGenerator(); + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var generator = delegating.GetService(); + + // Assert + Assert.Same(delegating, generator); + } + + [Fact] + public void GetServiceDelegatesToInnerIfKeyIsNotNull() + { + // Arrange + var expectedKey = new object(); + using var expectedResult = new TestImageGenerator(); + using var inner = new TestImageGenerator + { + GetServiceCallback = (_, _) => expectedResult + }; + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var generator = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, generator); + } + + [Fact] + public void GetServiceDelegatesToInnerIfNotCompatibleWithRequest() + { + // Arrange + var expectedResult = TimeZoneInfo.Local; + var expectedKey = new object(); + using var inner = new TestImageGenerator + { + GetServiceCallback = (type, key) => type == expectedResult.GetType() && key == expectedKey + ? expectedResult + : throw new InvalidOperationException("Unexpected call") + }; + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var tzi = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, tzi); + } + + [Fact] + public void Dispose_SetsFlag() + { + using var inner = new TestImageGenerator(); + var delegating = new NoOpDelegatingImageGenerator(inner); + Assert.False(inner.DisposeInvoked); + + delegating.Dispose(); + Assert.True(inner.DisposeInvoked); + } + + [Fact] + public void Dispose_MultipleCallsSafe() + { + using var inner = new TestImageGenerator(); + var delegating = new NoOpDelegatingImageGenerator(inner); + + delegating.Dispose(); + Assert.True(inner.DisposeInvoked); + + // Second dispose should not throw +#pragma warning disable S3966 + delegating.Dispose(); +#pragma warning restore S3966 + Assert.True(inner.DisposeInvoked); + } + + private sealed class NoOpDelegatingImageGenerator(IImageGenerator innerGenerator) + : DelegatingImageGenerator(innerGenerator); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs new file mode 100644 index 00000000000..f6cd167e82a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs @@ -0,0 +1,135 @@ +// 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.Drawing; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGenerationOptionsTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + ImageGenerationOptions options = new(); + Assert.Null(options.ResponseFormat); + Assert.Null(options.Count); + Assert.Null(options.ImageSize); + Assert.Null(options.MediaType); + Assert.Null(options.ModelId); + Assert.Null(options.RawRepresentationFactory); + + ImageGenerationOptions clone = options.Clone(); + Assert.Null(clone.ResponseFormat); + Assert.Null(clone.Count); + Assert.Null(clone.ImageSize); + Assert.Null(clone.MediaType); + Assert.Null(clone.ModelId); + Assert.Null(clone.RawRepresentationFactory); + } + + [Fact] + public void Properties_Roundtrip() + { + ImageGenerationOptions options = new(); + + Func factory = generator => new { Representation = "raw data" }; + + options.ResponseFormat = ImageGenerationResponseFormat.Data; + options.Count = 5; + options.ImageSize = new Size(1024, 768); + options.MediaType = "image/png"; + options.ModelId = "modelId"; + options.RawRepresentationFactory = factory; + + Assert.Equal(ImageGenerationResponseFormat.Data, options.ResponseFormat); + Assert.Equal(5, options.Count); + Assert.Equal(new Size(1024, 768), options.ImageSize); + Assert.Equal("image/png", options.MediaType); + Assert.Equal("modelId", options.ModelId); + Assert.Same(factory, options.RawRepresentationFactory); + + ImageGenerationOptions clone = options.Clone(); + Assert.Equal(ImageGenerationResponseFormat.Data, clone.ResponseFormat); + Assert.Equal(5, clone.Count); + Assert.Equal(new Size(1024, 768), clone.ImageSize); + Assert.Equal("image/png", clone.MediaType); + Assert.Equal("modelId", clone.ModelId); + Assert.Same(factory, clone.RawRepresentationFactory); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + ImageGenerationOptions options = new() + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 3, + ImageSize = new Size(256, 256), + MediaType = "image/jpeg", + ModelId = "test-model", + }; + + string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.ImageGenerationOptions); + + ImageGenerationOptions? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationOptions); + Assert.NotNull(deserialized); + + Assert.Equal(ImageGenerationResponseFormat.Data, deserialized.ResponseFormat); + Assert.Equal(3, deserialized.Count); + Assert.Equal(new Size(256, 256), deserialized.ImageSize); + Assert.Equal("image/jpeg", deserialized.MediaType); + Assert.Equal("test-model", deserialized.ModelId); + } + + [Fact] + public void Clone_CreatesIndependentCopy() + { + ImageGenerationOptions original = new() + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 2, + ImageSize = new Size(512, 512), + MediaType = "image/png", + ModelId = "original-model" + }; + + ImageGenerationOptions clone = original.Clone(); + + // Modify original + original.ResponseFormat = ImageGenerationResponseFormat.Uri; + original.Count = 1; + original.ImageSize = new Size(1024, 1024); + original.MediaType = "image/jpeg"; + original.ModelId = "modified-model"; + + // Clone should remain unchanged + Assert.Equal(ImageGenerationResponseFormat.Data, clone.ResponseFormat); + Assert.Equal(2, clone.Count); + Assert.Equal(new Size(512, 512), clone.ImageSize); + Assert.Equal("image/png", clone.MediaType); + Assert.Equal("original-model", clone.ModelId); + } + + [Theory] + [InlineData(ImageGenerationResponseFormat.Uri)] + [InlineData(ImageGenerationResponseFormat.Data)] + [InlineData(ImageGenerationResponseFormat.Hosted)] + public void ImageGenerationResponseFormat_Values_AreValid(ImageGenerationResponseFormat responseFormat) + { + Assert.True(Enum.IsDefined(typeof(ImageGenerationResponseFormat), responseFormat)); + } + + [Fact] + public void ImageGenerationResponseFormat_JsonSerialization_Roundtrips() + { + foreach (ImageGenerationResponseFormat responseFormat in Enum.GetValues(typeof(ImageGenerationResponseFormat))) + { + string json = JsonSerializer.Serialize(responseFormat, TestJsonSerializerContext.Default.ImageGenerationResponseFormat); + ImageGenerationResponseFormat deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponseFormat); + Assert.Equal(responseFormat, deserialized); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs new file mode 100644 index 00000000000..7b244dfeb53 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs @@ -0,0 +1,154 @@ +// 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 ImageGenerationResponseTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + ImageGenerationResponse response = new(); + Assert.Empty(response.Contents); + Assert.NotNull(response.Contents); + Assert.Same(response.Contents, response.Contents); + Assert.Empty(response.Contents); + Assert.Null(response.RawRepresentation); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Constructor_List_PropsRoundtrip(int contentCount) + { + List content = []; + for (int i = 0; i < contentCount; i++) + { + content.Add(new UriContent(new Uri($"https://example.com/image-{i}.png"), "image/png")); + } + + ImageGenerationResponse response = new(content); + + Assert.Same(response.Contents, response.Contents); + if (contentCount == 0) + { + Assert.Empty(response.Contents); + } + else + { + Assert.Equal(contentCount, response.Contents.Count); + for (int i = 0; i < contentCount; i++) + { + UriContent uc = Assert.IsType(response.Contents[i]); + Assert.Equal($"https://example.com/image-{i}.png", uc.Uri.ToString()); + Assert.Equal("image/png", uc.MediaType); + } + } + } + + [Fact] + public void Contents_SetNull_ReturnsEmpty() + { + ImageGenerationResponse response = new() + { + Contents = null! + }; + Assert.NotNull(response.Contents); + Assert.Empty(response.Contents); + } + + [Fact] + public void Contents_Set_Roundtrips() + { + ImageGenerationResponse response = new(); + byte[] imageData = [1, 2, 3, 4]; + + List contents = [ + new UriContent(new Uri("https://example.com/image1.png"), "image/png"), + new DataContent(imageData, "image/jpeg") + ]; + + response.Contents = contents; + Assert.Same(contents, response.Contents); + } + + [Fact] + public void RawRepresentation_Roundtrips() + { + ImageGenerationResponse response = new(); + Assert.Null(response.RawRepresentation); + + object representation = new { test = "value" }; + response.RawRepresentation = representation; + Assert.Same(representation, response.RawRepresentation); + + response.RawRepresentation = null; + Assert.Null(response.RawRepresentation); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + List contents = [ + new UriContent(new Uri("https://example.com/image1.png"), "image/png"), + new DataContent((byte[])[1, 2, 3, 4], "image/jpeg") + ]; + + ImageGenerationResponse response = new(contents); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + + Assert.Equal(2, deserialized.Contents.Count); + + UriContent uriContent = Assert.IsType(deserialized.Contents[0]); + Assert.Equal("https://example.com/image1.png", uriContent.Uri.ToString()); + Assert.Equal("image/png", uriContent.MediaType); + + DataContent dataContent = Assert.IsType(deserialized.Contents[1]); + Assert.Equal([1, 2, 3, 4], dataContent.Data.ToArray()); + Assert.Equal("image/jpeg", dataContent.MediaType); + } + + [Fact] + public void JsonSerialization_Empty_Roundtrips() + { + ImageGenerationResponse response = new(); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + Assert.Empty(deserialized.Contents); + } + + [Fact] + public void JsonSerialization_WithVariousContentTypes_Roundtrips() + { + List contents = [ + new UriContent(new Uri("https://example.com/image.png"), "image/png"), + new DataContent((byte[])[255, 216, 255, 224], "image/jpeg"), + new TextContent("Generated image description") // Edge case: text content in image response + ]; + + ImageGenerationResponse response = new(contents); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + Assert.Equal(3, deserialized.Contents.Count); + + Assert.IsType(deserialized.Contents[0]); + Assert.IsType(deserialized.Contents[1]); + Assert.IsType(deserialized.Contents[2]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs new file mode 100644 index 00000000000..a68726685eb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs @@ -0,0 +1,222 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorExtensionsTests +{ + [Fact] + public void GetService_InvalidArgs_Throws() + { + Assert.Throws("generator", () => + { + _ = ImageGeneratorExtensions.GetService(null!); + }); + } + + [Fact] + public void GetService_ValidGenerator_CallsUnderlyingGetService() + { + using var testGenerator = new TestImageGenerator(); + var expectedResult = new object(); + var expectedServiceKey = new object(); + + testGenerator.GetServiceCallback = (serviceType, serviceKey) => + { + Assert.Equal(typeof(object), serviceType); + Assert.Same(expectedServiceKey, serviceKey); + return expectedResult; + }; + + var result = testGenerator.GetService(expectedServiceKey); + Assert.Same(expectedResult, result); + } + + [Fact] + public void GetService_ReturnsCorrectType() + { + using var testGenerator = new TestImageGenerator(); + var metadata = new ImageGeneratorMetadata("test", null, "model"); + + testGenerator.GetServiceCallback = (serviceType, serviceKey) => + { + return (serviceType == typeof(ImageGeneratorMetadata)) ? metadata : null; + }; + + var result = testGenerator.GetService(); + Assert.Same(metadata, result); + + var nullResult = testGenerator.GetService(); + Assert.Null(nullResult); + } + + [Fact] + public async Task EditImageAsync_DataContent_CallsGenerateImagesAsync() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var dataContent = new DataContent(imageData, "image/png") { Name = "test.png" }; + var prompt = "Edit this image"; + var options = new ImageGenerationOptions { Count = 2 }; + var expectedResponse = new ImageGenerationResponse(); + var cancellationToken = new CancellationToken(canceled: false); + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + Assert.Single(request.OriginalImages); + Assert.Same(dataContent, Assert.Single(request.OriginalImages)); + Assert.Equal(prompt, request.Prompt); + Assert.Same(options, o); + Assert.Equal(cancellationToken, ct); + return Task.FromResult(expectedResponse); + }; + + // Act + var result = await testGenerator.EditImageAsync(dataContent, prompt, options, cancellationToken); + + // Assert + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task EditImageAsync_DataContent_NullArguments_Throws() + { + using var testGenerator = new TestImageGenerator(); + var dataContent = new DataContent(new byte[] { 1, 2, 3 }, "image/png"); + + await Assert.ThrowsAsync("generator", async () => + await ImageGeneratorExtensions.EditImageAsync(null!, dataContent, "prompt")); + + await Assert.ThrowsAsync("originalImage", async () => + await testGenerator.EditImageAsync(null!, "prompt")); + + await Assert.ThrowsAsync("prompt", async () => + await testGenerator.EditImageAsync(dataContent, null!)); + } + + [Fact] + public async Task EditImageAsync_ByteArray_CallsGenerateImagesAsync() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var fileName = "test.jpg"; + var prompt = "Edit this image"; + var options = new ImageGenerationOptions { Count = 2 }; + var expectedResponse = new ImageGenerationResponse(); + var cancellationToken = new CancellationToken(canceled: false); + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + Assert.Single(request.OriginalImages); + var dataContent = Assert.IsType(Assert.Single(request.OriginalImages)); + Assert.Equal(imageData, dataContent.Data.ToArray()); + Assert.Equal("image/jpeg", dataContent.MediaType); + Assert.Equal(fileName, dataContent.Name); + Assert.Equal(prompt, request.Prompt); + Assert.Same(options, o); + Assert.Equal(cancellationToken, ct); + return Task.FromResult(expectedResponse); + }; + + // Act + var result = await testGenerator.EditImageAsync(imageData, fileName, prompt, options, cancellationToken); + + // Assert + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullGenerator_Throws() + { + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("generator", async () => + await ImageGeneratorExtensions.EditImageAsync(null!, imageData, "test.png", "prompt")); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullFileName_Throws() + { + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("fileName", async () => + await testGenerator.EditImageAsync(imageData, null!, "prompt")); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullPrompt_Throws() + { + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("prompt", async () => + await testGenerator.EditImageAsync(imageData, "test.png", null!)); + } + + [Theory] + [InlineData("test.png", "image/png")] + [InlineData("test.jpg", "image/jpeg")] + [InlineData("test.jpeg", "image/jpeg")] + [InlineData("test.webp", "image/webp")] + [InlineData("test.gif", "image/gif")] + [InlineData("test.bmp", "image/bmp")] + [InlineData("test.tiff", "image/tiff")] + [InlineData("test.tif", "image/tiff")] + [InlineData("test.unknown", "image/png")] // Unknown extension defaults to PNG + [InlineData("TEST.PNG", "image/png")] // Case insensitive + public async Task EditImageAsync_ByteArray_InfersCorrectMediaType(string fileName, string expectedMediaType) + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var prompt = "Edit this image"; + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + var dataContent = Assert.IsType(Assert.Single(request.OriginalImages)); + Assert.Equal(expectedMediaType, dataContent.MediaType); + return Task.FromResult(new ImageGenerationResponse()); + }; + + // Act & Assert + await testGenerator.EditImageAsync(imageData, fileName, prompt); + } + + [Fact] + public async Task EditImageAsync_AllMethods_PassDefaultOptionsAndCancellation() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var dataContent = new DataContent(imageData, "image/png"); + var prompt = "Edit this image"; + + int callCount = 0; + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + callCount++; + Assert.Null(o); // Default options should be null + Assert.Equal(CancellationToken.None, ct); // Default cancellation token + Assert.NotNull(request.OriginalImages); // Should have original images for editing + return Task.FromResult(new ImageGenerationResponse()); + }; + + // Act - Test all two overloads with default parameters + await testGenerator.EditImageAsync(dataContent, prompt); + await testGenerator.EditImageAsync(imageData, "test.png", prompt); + + // Assert + Assert.Equal(2, callCount); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs new file mode 100644 index 00000000000..193a02bde3e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs @@ -0,0 +1,29 @@ +// 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 ImageGeneratorMetadataTests +{ + [Fact] + public void Constructor_NullValues_AllowedAndRoundtrip() + { + ImageGeneratorMetadata metadata = new(null, null, null); + Assert.Null(metadata.ProviderName); + Assert.Null(metadata.ProviderUri); + Assert.Null(metadata.DefaultModelId); + } + + [Fact] + public void Constructor_Value_Roundtrips() + { + var uri = new Uri("https://example.com"); + ImageGeneratorMetadata metadata = new("providerName", uri, "theModel"); + Assert.Equal("providerName", metadata.ProviderName); + Assert.Same(uri, metadata.ProviderUri); + Assert.Equal("theModel", metadata.DefaultModelId); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs new file mode 100644 index 00000000000..2f60d3bb052 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs @@ -0,0 +1,155 @@ +// 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.Drawing; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorTests +{ + [Fact] + public void GetService_WithServiceKey_ReturnsNull() + { + using var generator = new TestImageGenerator(); + generator.GetServiceCallback = (serviceType, serviceKey) => + { + // When serviceKey is not null, should return null per interface contract + return serviceKey is not null ? null : new object(); + }; + + var result = generator.GetService(typeof(object), "someKey"); + Assert.Null(result); + } + + [Fact] + public void GetService_WithoutServiceKey_CallsCallback() + { + using var generator = new TestImageGenerator(); + var expectedResult = new object(); + + generator.GetServiceCallback = (serviceType, serviceKey) => + { + Assert.Equal(typeof(object), serviceType); + Assert.Null(serviceKey); + return expectedResult; + }; + + var result = generator.GetService(typeof(object)); + Assert.Same(expectedResult, result); + } + + [Fact] + public async Task GenerateImagesAsync_CallsCallback() + { + var expectedResponse = new ImageGenerationResponse(); + var expectedOptions = new ImageGenerationOptions(); + using var cts = new CancellationTokenSource(); + var expectedRequest = new ImageGenerationRequest("test prompt"); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(expectedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedResponse); + } + }; + + var result = await generator.GenerateAsync(expectedRequest, expectedOptions, cts.Token); + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task GenerateImagesAsync_NoCallback_ReturnsEmptyResponse() + { + using var generator = new TestImageGenerator(); + var result = await generator.GenerateAsync(new ImageGenerationRequest("test prompt"), null); + Assert.NotNull(result); + Assert.Empty(result.Contents); + } + + [Fact] + public void Dispose_SetsFlag() + { + var generator = new TestImageGenerator(); + Assert.False(generator.DisposeInvoked); + + generator.Dispose(); + Assert.True(generator.DisposeInvoked); + } + + [Fact] + public void Dispose_MultipleCallsSafe() + { + var generator = new TestImageGenerator(); + + generator.Dispose(); + Assert.True(generator.DisposeInvoked); + + // Second dispose should not throw +#pragma warning disable S3966 + generator.Dispose(); +#pragma warning restore S3966 + Assert.True(generator.DisposeInvoked); + } + + [Fact] + public async Task GenerateImagesAsync_WithOptions_PassesThroughCorrectly() + { + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 3, + ImageSize = new Size(1024, 768), + MediaType = "image/png", + ModelId = "test-model", + }; + + var expectedRequest = new ImageGenerationRequest("test prompt"); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, receivedOptions, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(options, receivedOptions); + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + await generator.GenerateAsync(expectedRequest, options); + } + + [Fact] + public async Task GenerateImagesAsync_WithEditRequest_PassesThroughCorrectly() + { + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 2, + MediaType = "image/jpeg", + ModelId = "edit-model", + }; + + AIContent[] originalImages = [new DataContent((byte[])[1, 2, 3, 4], "image/png")]; + var expectedRequest = new ImageGenerationRequest("edit prompt", originalImages); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, receivedOptions, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(options, receivedOptions); + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + await generator.GenerateAsync(expectedRequest, options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs new file mode 100644 index 00000000000..4db1cca7377 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs @@ -0,0 +1,43 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +public sealed class TestImageGenerator : IImageGenerator +{ + public TestImageGenerator() + { + GetServiceCallback = DefaultGetServiceCallback; + } + + public IServiceProvider? Services { get; set; } + + public Func>? GenerateImagesAsyncCallback { get; set; } + + public Func GetServiceCallback { get; set; } + + public bool DisposeInvoked { get; private set; } + + private object? DefaultGetServiceCallback(Type serviceType, object? serviceKey) + => serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; + + public Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return GenerateImagesAsyncCallback?.Invoke(request, options, cancellationToken) ?? + Task.FromResult(new ImageGenerationResponse()); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return GetServiceCallback.Invoke(serviceType, serviceKey); + } + + public void Dispose() + { + DisposeInvoked = true; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index d15f0a19fa9..609dac264eb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -20,6 +20,8 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(SpeechToTextResponseUpdate))] [JsonSerializable(typeof(SpeechToTextResponseUpdateKind))] [JsonSerializable(typeof(SpeechToTextOptions))] +[JsonSerializable(typeof(ImageGenerationResponse))] +[JsonSerializable(typeof(ImageGenerationOptions))] [JsonSerializable(typeof(ChatOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] [JsonSerializable(typeof(Dictionary))] diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs new file mode 100644 index 00000000000..8e3078efbb5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs @@ -0,0 +1,125 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.TestUtilities; +using Xunit; + +#pragma warning disable CA2214 // Do not call overridable methods in constructors + +namespace Microsoft.Extensions.AI; + +public abstract class ImageGeneratorIntegrationTests : IDisposable +{ + private readonly IImageGenerator? _generator; + + protected ImageGeneratorIntegrationTests() + { + _generator = CreateGenerator(); + } + + public void Dispose() + { + _generator?.Dispose(); + GC.SuppressFinalize(this); + } + + protected abstract IImageGenerator? CreateGenerator(); + + [ConditionalFact] + public virtual async Task GenerateImagesAsync_SingleImageGeneration() + { + SkipIfNotEnabled(); + + var options = new ImageGenerationOptions + { + Count = 1 + }; + + var response = await _generator.GenerateImagesAsync("A simple drawing of a house", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); + + var content = response.Contents[0]; + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + + [ConditionalFact] + public virtual async Task GenerateImagesAsync_MultipleImages() + { + SkipIfNotEnabled(); + + var options = new ImageGenerationOptions + { + Count = 2 + }; + + var response = await _generator.GenerateImagesAsync("A cat sitting on a table", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Equal(2, response.Contents.Count); + + foreach (var content in response.Contents) + { + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + } + + [ConditionalFact] + public virtual async Task EditImagesAsync_SingleImage() + { + SkipIfNotEnabled(); + + var imageData = GetImageData("dotnet.png"); + AIContent[] originalImages = [new DataContent(imageData, "image/png") { Name = "dotnet.png" }]; + + var options = new ImageGenerationOptions + { + Count = 1 + }; + + var response = await _generator.EditImagesAsync(originalImages, "Add a red border and make the background tie-dye", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); + + var content = response.Contents[0]; + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + + private static byte[] GetImageData(string fileName) + { + using Stream? s = typeof(ImageGeneratorIntegrationTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); + Assert.NotNull(s); + using MemoryStream ms = new(); + s.CopyTo(ms); + return ms.ToArray(); + } + + [MemberNotNull(nameof(_generator))] + protected void SkipIfNotEnabled() + { + string? skipIntegration = TestRunnerConfiguration.Instance["SkipIntegrationTests"]; + + if (skipIntegration is not null || _generator is null) + { + throw new SkipTestException("Generator is not enabled."); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index ddc72caa90d..0b4865f577e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md index 3b99e9bccc1..988ab2d08f5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md @@ -17,6 +17,7 @@ Optionally also run the following. The values shown here are the defaults if you ``` dotnet user-secrets set OpenAI:ChatModel gpt-4o-mini dotnet user-secrets set OpenAI:EmbeddingModel text-embedding-3-small +dotnet user-secrets set OpenAI:ImageModel dall-e-3 ``` ### Configuring OpenAI tests (Azure OpenAI) @@ -35,6 +36,7 @@ Optionally also run the following. The values shown here are the defaults if you ``` dotnet user-secrets set OpenAI:ChatModel gpt-4o-mini dotnet user-secrets set OpenAI:EmbeddingModel text-embedding-3-small +dotnet user-secrets set OpenAI:ImageModel dall-e-3 ``` Your account must have models matching these names. diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs new file mode 100644 index 00000000000..ce0cdb7cf82 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public class OpenAIImageGeneratorIntegrationTests : ImageGeneratorIntegrationTests +{ + protected override IImageGenerator? CreateGenerator() + => IntegrationTestHelpers.GetOpenAIClient()? + .GetImageClient(TestRunnerConfiguration.Instance["OpenAI:ImageModel"] ?? "dall-e-3") + .AsIImageGenerator(); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs new file mode 100644 index 00000000000..607b1e2859e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs @@ -0,0 +1,45 @@ +// 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.ClientModel; +using OpenAI; +using OpenAI.Images; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenAIImageGeneratorTests +{ + [Fact] + public void AsIImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("imageClient", () => ((ImageClient)null!).AsIImageGenerator()); + } + + [Fact] + public void AsIImageGenerator_OpenAIClient_ProducesExpectedMetadata() + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "dall-e-3"; + + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + IImageGenerator imageClient = client.GetImageClient(model).AsIImageGenerator(); + var metadata = imageClient.GetService(); + Assert.Equal(endpoint, metadata?.ProviderUri); + Assert.Equal(model, metadata?.DefaultModelId); + } + + [Fact] + public void GetService_ReturnsExpectedServices() + { + var client = new OpenAIClient(new ApiKeyCredential("key")); + IImageGenerator imageClient = client.GetImageClient("dall-e-3").AsIImageGenerator(); + + Assert.Same(imageClient, imageClient.GetService()); + Assert.Same(imageClient, imageClient.GetService()); + Assert.NotNull(imageClient.GetService()); + Assert.NotNull(imageClient.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs new file mode 100644 index 00000000000..ba37cad1b54 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs @@ -0,0 +1,70 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ConfigureOptionsImageGeneratorTests +{ + [Fact] + public void ConfigureOptionsImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new ConfigureOptionsImageGenerator(null!, _ => { })); + Assert.Throws("configure", () => new ConfigureOptionsImageGenerator(new TestImageGenerator(), null!)); + } + + [Fact] + public void ConfigureOptions_InvalidArgs_Throws() + { + using var innerGenerator = new TestImageGenerator(); + var builder = innerGenerator.AsBuilder(); + Assert.Throws("configure", () => builder.ConfigureOptions(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ConfigureOptions_ReturnedInstancePassedToNextGenerator(bool nullProvidedOptions) + { + ImageGenerationOptions? providedOptions = nullProvidedOptions ? null : new() { ModelId = "test" }; + ImageGenerationOptions? returnedOptions = null; + ImageGenerationResponse expectedResponse = new([]); + using CancellationTokenSource cts = new(); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (prompt, options, cancellationToken) => + { + Assert.Same(returnedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedResponse); + }, + + }; + + using var generator = innerGenerator + .AsBuilder() + .ConfigureOptions(options => + { + Assert.NotSame(providedOptions, options); + if (nullProvidedOptions) + { + Assert.Null(options.ModelId); + } + else + { + Assert.Equal(providedOptions!.ModelId, options.ModelId); + } + + returnedOptions = options; + }) + .Build(); + + var response1 = await generator.GenerateImagesAsync("test prompt", providedOptions, cts.Token); + Assert.Same(expectedResponse, response1); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs new file mode 100644 index 00000000000..b65495e506b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs @@ -0,0 +1,162 @@ +// 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.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorDependencyInjectionPatterns +{ + private IServiceCollection ServiceCollection { get; } = new ServiceCollection(); + + [Fact] + public void CanRegisterSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddImageGenerator(services => new TestImageGenerator { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestImageGenerator(); + ServiceCollection.AddImageGenerator(singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddKeyedImageGenerator("mykey", services => new TestImageGenerator { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestImageGenerator(); + ServiceCollection.AddKeyedImageGenerator("mykey", singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddImageGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ImageGeneratorBuilder builder = lifetime.HasValue + ? sc.AddImageGenerator(services => new TestImageGenerator(), lifetime.Value) + : sc.AddImageGenerator(services => new TestImageGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IImageGenerator), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddKeyedImageGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ImageGeneratorBuilder builder = lifetime.HasValue + ? sc.AddKeyedImageGenerator("key", services => new TestImageGenerator(), lifetime.Value) + : sc.AddKeyedImageGenerator("key", services => new TestImageGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IImageGenerator), sd.ServiceType); + Assert.True(sd.IsKeyedService); + Assert.Equal("key", sd.ServiceKey); + Assert.Null(sd.KeyedImplementationInstance); + Assert.NotNull(sd.KeyedImplementationFactory); + Assert.IsType(sd.KeyedImplementationFactory(null!, null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + public class SingletonMiddleware(IImageGenerator inner, IServiceProvider services) : DelegatingImageGenerator(inner) + { + public new IImageGenerator InnerGenerator => base.InnerGenerator; + public IServiceProvider Services => services; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs new file mode 100644 index 00000000000..819fcde88ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs @@ -0,0 +1,143 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class LoggingImageGeneratorTests +{ + [Fact] + public void LoggingImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new LoggingImageGenerator(null!, NullLogger.Instance)); + Assert.Throws("logger", () => new LoggingImageGenerator(new TestImageGenerator(), null!)); + } + + [Fact] + public void UseLogging_AvoidsInjectingNopGenerator() + { + using var innerGenerator = new TestImageGenerator(); + + Assert.Null(innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(LoggingImageGenerator))); + Assert.Same(innerGenerator, innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(IImageGenerator))); + + using var factory = LoggerFactory.Create(b => b.AddFakeLogging()); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging(factory).Build().GetService(typeof(LoggingImageGenerator))); + + ServiceCollection c = new(); + c.AddFakeLogging(); + var services = c.BuildServiceProvider(); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging().Build(services).GetService(typeof(LoggingImageGenerator))); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging(null).Build(services).GetService(typeof(LoggingImageGenerator))); + Assert.Null(innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build(services).GetService(typeof(LoggingImageGenerator))); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GenerateImagesAsync_LogsInvocationAndCompletion(LogLevel level) + { + var collector = new FakeLogCollector(); + + ServiceCollection c = new(); + c.AddLogging(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + var services = c.BuildServiceProvider(); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + return Task.FromResult(new ImageGenerationResponse()); + }, + }; + + using IImageGenerator generator = innerGenerator + .AsBuilder() + .UseLogging() + .Build(services); + + await generator.GenerateAsync( + new ImageGenerationRequest("A beautiful sunset"), + new ImageGenerationOptions { ModelId = "dall-e-3" }); + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True( + entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked:") && + entry.Message.Contains("A beautiful sunset") && + entry.Message.Contains("dall-e-3")), + entry => Assert.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed:", entry.Message)); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked.") && !entry.Message.Contains("A beautiful sunset")), + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed.") && !entry.Message.Contains("dall-e-3"))); + } + else + { + Assert.Empty(logs); + } + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GenerateImagesAsync_WithOriginalImages_LogsInvocationAndCompletion(LogLevel level) + { + var collector = new FakeLogCollector(); + using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + using IImageGenerator generator = innerGenerator + .AsBuilder() + .UseLogging(loggerFactory) + .Build(); + + AIContent[] originalImages = [new DataContent((byte[])[1, 2, 3, 4], "image/png")]; + await generator.GenerateAsync( + new ImageGenerationRequest("Make it more colorful", originalImages), + new ImageGenerationOptions { ModelId = "dall-e-3" }); + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True( + entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked:") && + entry.Message.Contains("Make it more colorful") && + entry.Message.Contains("dall-e-3")), + entry => Assert.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed", entry.Message)); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked.") && !entry.Message.Contains("Make it more colorful")), + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed.") && !entry.Message.Contains("dall-e-3"))); + } + else + { + Assert.Empty(logs); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs new file mode 100644 index 00000000000..498b4738962 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public static class SingletonImageGeneratorExtensions +{ + public static ImageGeneratorBuilder UseSingletonMiddleware(this ImageGeneratorBuilder builder) + => builder.Use((inner, services) + => new ImageGeneratorDependencyInjectionPatterns.SingletonMiddleware(inner, services)); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index c07f3056054..d06b423a504 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -22,6 +22,7 @@ + From a691e7340e7f493da3537ac4b34126bc20fb655e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 12 Aug 2025 09:08:30 -0500 Subject: [PATCH 252/472] Reapply https://github.com/dotnet/extensions/pull/6205 (#6706) --- .../Components/Pages/Chat/Chat.razor | 10 ---------- .../Components/Pages/Chat/Chat.razor | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index e8da69efd54..a7b1502d894 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -62,15 +62,6 @@ chatSuggestions?.Clear(); await chatInput!.FocusAsync(); -@*#if (IsOllama) - // Display a new response from the IChatClient, streaming responses - // aren't supported because Ollama will not support both streaming and using Tools - currentResponseCancellation = new(); - var response = await ChatClient.GetResponseAsync(messages, chatOptions, currentResponseCancellation.Token); - - // Store responses in the conversation, and begin getting suggestions - messages.AddMessages(response); -#else*@ // Stream and display a new response from the IChatClient var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); @@ -85,7 +76,6 @@ // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); currentResponseMessage = null; -@*#endif*@ chatSuggestions?.Update(messages); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index 718c2c46785..a7b1502d894 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -62,13 +62,20 @@ chatSuggestions?.Clear(); await chatInput!.FocusAsync(); - // Display a new response from the IChatClient, streaming responses - // aren't supported because Ollama will not support both streaming and using Tools + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - var response = await ChatClient.GetResponseAsync(messages, chatOptions, currentResponseCancellation.Token); + await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } - // Store responses in the conversation, and begin getting suggestions - messages.AddMessages(response); + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + currentResponseMessage = null; chatSuggestions?.Update(messages); } From d4030cc7aaaa84657b20ca1ac01f756b85dcd693 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 12 Aug 2025 08:29:03 -0700 Subject: [PATCH 253/472] Update Azure.AI.OpenAI (#6709) --- src/ProjectTemplates/GeneratedContent.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 706424bdc25..aca99e5bdde 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,7 +35,7 @@ 9.3.0 9.3.0-preview.1.25265.20 - 2.2.0-beta.4 + 2.3.0-beta.1 1.0.0-beta.9 1.14.0 11.6.0 From 563a09f59d4c64fd05860732404db189a5bf8012 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 12 Aug 2025 12:41:44 -0400 Subject: [PATCH 254/472] Use Azure hosting integrations in Aspire AI Chat Web template (#6659) --- src/ProjectTemplates/GeneratedContent.targets | 12 +-- ...hatWithCustomData-CSharp.AppHost.csproj.in | 6 +- .../Program.cs | 35 ++++++-- .../ChatWithCustomData-CSharp.Web.csproj.in | 1 + .../Program.Aspire.cs | 5 +- .../src/ChatWithCustomData/README.Aspire.md | 84 ++++++------------- .../aichatweb/README.md | 51 +++-------- .../aichatweb/aichatweb.AppHost/Program.cs | 34 +++++--- .../aichatweb.AppHost.csproj | 6 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/Program.cs | 3 +- .../aichatweb.Web/aichatweb.Web.csproj | 9 +- .../aichatweb/aichatweb.csproj | 4 +- .../aichatweb.AppHost.csproj | 4 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 7 +- .../aichatweb.AppHost.csproj | 6 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 6 +- .../aichatweb/aichatweb.csproj | 6 +- 20 files changed, 130 insertions(+), 155 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index aca99e5bdde..31093493821 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -33,17 +33,17 @@ Specifies external packages that get referenced in generated template content. --> - 9.3.0 - 9.3.0-preview.1.25265.20 + 9.4.0 + 9.4.0-preview.1.25378.8 2.3.0-beta.1 1.0.0-beta.9 1.14.0 - 11.6.0 + 11.6.1 9.4.1-beta.291 10.0.0-preview.6.25358.103 - 9.3.0 - 1.53.0 - 1.53.0-preview + 9.3.1 + 1.61.0 + 1.61.0-preview 0.3.0-preview.2 5.1.18 1.12.0 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index d7287e9301a..3bbd301af75 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -15,8 +15,12 @@ - + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs index 6be0fd58648..c3045240cda 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs @@ -1,6 +1,6 @@ var builder = DistributedApplication.CreateBuilder(args); #if (IsOllama) // ASPIRE PARAMETERS -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) // You will need to set the connection string to your own value // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: @@ -13,14 +13,27 @@ // dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" #endif var openai = builder.AddConnectionString("openai"); +#else // IsAzureOpenAI + +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var openai = builder.AddAzureOpenAI("openai"); + +openai.AddDeployment( + name: "gpt-4o-mini", + modelName: "gpt-4o-mini", + modelVersion: "2024-07-18"); + +openai.AddDeployment( + name: "text-embedding-3-small", + modelName: "text-embedding-3-small", + modelVersion: "1"); #endif #if (UseAzureAISearch) -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" -var azureAISearch = builder.AddConnectionString("azureAISearch"); +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var search = builder.AddAzureSearch("search"); #endif #if (IsOllama) // AI SERVICE PROVIDER CONFIGURATION @@ -45,11 +58,17 @@ .WithReference(embeddings) .WaitFor(chat) .WaitFor(embeddings); -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) webApp.WithReference(openai); +#else // IsAzureOpenAI +webApp + .WithReference(openai) + .WaitFor(openai); #endif #if (UseAzureAISearch) // VECTOR DATABASE REFERENCES -webApp.WithReference(azureAISearch); +webApp + .WithReference(search) + .WaitFor(search); #elif (UseQdrant) webApp .WithReference(vectorDB) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 64d585fd7a6..0e753e8c907 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -21,6 +21,7 @@ #endif --> + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index adcb2452d87..84475f45a54 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -3,8 +3,9 @@ using ChatWithCustomData_CSharp.Web.Services; using ChatWithCustomData_CSharp.Web.Services.Ingestion; #if (IsOllama) -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) using OpenAI; +#else // IsAzureOpenAI #endif var builder = WebApplication.CreateBuilder(args); @@ -34,7 +35,7 @@ #endif #if (UseAzureAISearch) -builder.AddAzureSearchClient("azureAISearch"); +builder.AddAzureSearchClient("search"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents"); #elif (UseQdrant) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index ba4b5bf788b..0b1934bfff7 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -81,75 +81,41 @@ Download, install, and run Docker Desktop from the [official website](https://ww Note: Ollama and Docker are excellent open source products, but are not maintained by Microsoft. #### ---#endif -#### ---#if (IsAzureOpenAI) -## Using Azure OpenAI +#### ---#if (IsAzureOpenAI || UseAzureAISearch) +## Using Azure Provisioning -To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service resource. For detailed instructions, see the [Azure OpenAI Service documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource). +The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -### 1. Create an Azure OpenAI Service Resource -[Create an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). - -### 2. Deploy the Models -Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). - -### 3. Configure API Key and Endpoint -Configure your Azure OpenAI API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure OpenAI resource. - 2. Copy the "Endpoint" URL and "Key 1" from the "Keys and Endpoint" section. #### ---#if (hostIdentifier == "vs") - 3. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". - 4. This will open a secrets.json file where you can store your API key and endpoint without it being tracked in source control. Add the following keys & values to the file: - - ```json - { - "ConnectionStrings:openai": "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - } - ``` -#### ---#else - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd ChatWithCustomData-CSharp.AppHost - dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - ``` -#### ---#endif - -Make sure to replace `YOUR-API-KEY` and `YOUR-DEPLOYMENT-NAME` with your actual Azure OpenAI key and endpoint. Make sure your endpoint URL is formatted like https://YOUR-DEPLOYMENT-NAME.openai.azure.com/ (do not include any path after .openai.azure.com/). -#### ---#endif -#### ---#if (UseAzureAISearch) - -## Configure Azure AI Search - -To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). +Configure local provisioning for this project using .NET User Secrets: -### 1. Create an Azure AI Search Resource -Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. +1. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". +2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following configuration: -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-chunks` and `data-ChatWithCustomData-CSharp.Web-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. + ```json + { + "Azure": { + "SubscriptionId": "", + "AllowResourceGroupCreation": true, + "ResourceGroup": "", + "Location": "" + } + } + ``` -### 3. Configure API Key and Endpoint - Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure AI Search resource. - 2. Copy the "Endpoint" URL and "Primary admin key" from the "Keys" section. -#### ---#if (hostIdentifier == "vs") - 3. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". - 4. This will open a `secrets.json` file where you can store your API key and endpoint without them being tracked in source control. Add the following keys and values to the file: - - ```json - { - "ConnectionStrings:azureAISearch": "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - } - ``` #### ---#else - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: +From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: - ```sh - cd ChatWithCustomData-CSharp.AppHost - dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - ``` +```sh +cd ChatWithCustomData-CSharp.AppHost +dotnet user-secrets set Azure:SubscriptionId "" +dotnet user-secrets set Azure:AllowResourceGroupCreation "true" +dotnet user-secrets set Azure:ResourceGroup "" +dotnet user-secrets set Azure:Location "" +``` #### ---#endif -Make sure to replace `YOUR-DEPLOYMENT-NAME` and `YOUR-API-KEY` with your actual Azure AI Search deployment name and key. +Make sure to replace placeholder values with real configuration values. #### ---#endif #### ---#if (UseQdrant) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md index 57c0375d302..d1459703de1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md @@ -15,50 +15,21 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See # Configure the AI Model Provider -## Using Azure OpenAI +## Using Azure Provisioning -To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service resource. For detailed instructions, see the [Azure OpenAI Service documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource). +The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -### 1. Create an Azure OpenAI Service Resource -[Create an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). +From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: -### 2. Deploy the Models -Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). +```sh +cd aichatweb.AppHost +dotnet user-secrets set Azure:SubscriptionId "" +dotnet user-secrets set Azure:AllowResourceGroupCreation "true" +dotnet user-secrets set Azure:ResourceGroup "" +dotnet user-secrets set Azure:Location "" +``` -### 3. Configure API Key and Endpoint -Configure your Azure OpenAI API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure OpenAI resource. - 2. Copy the "Endpoint" URL and "Key 1" from the "Keys and Endpoint" section. - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd aichatweb.AppHost - dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - ``` - -Make sure to replace `YOUR-API-KEY` and `YOUR-DEPLOYMENT-NAME` with your actual Azure OpenAI key and endpoint. Make sure your endpoint URL is formatted like https://YOUR-DEPLOYMENT-NAME.openai.azure.com/ (do not include any path after .openai.azure.com/). - -## Configure Azure AI Search - -To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). - -### 1. Create an Azure AI Search Resource -Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. - -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-aichatweb-chunks` and `data-aichatweb-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. - -### 3. Configure API Key and Endpoint - Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure AI Search resource. - 2. Copy the "Endpoint" URL and "Primary admin key" from the "Keys" section. - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd aichatweb.AppHost - dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - ``` - -Make sure to replace `YOUR-DEPLOYMENT-NAME` and `YOUR-API-KEY` with your actual Azure AI Search deployment name and key. +Make sure to replace placeholder values with real configuration values. # Running the application diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs index 80803d78d74..da0220a0b1c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs @@ -1,19 +1,29 @@ var builder = DistributedApplication.CreateBuilder(args); -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" -var openai = builder.AddConnectionString("openai"); +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var openai = builder.AddAzureOpenAI("openai"); -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" -var azureAISearch = builder.AddConnectionString("azureAISearch"); +openai.AddDeployment( + name: "gpt-4o-mini", + modelName: "gpt-4o-mini", + modelVersion: "2024-07-18"); + +openai.AddDeployment( + name: "text-embedding-3-small", + modelName: "text-embedding-3-small", + modelVersion: "1"); + +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var search = builder.AddAzureSearch("search"); var webApp = builder.AddProject("aichatweb-app"); -webApp.WithReference(openai); -webApp.WithReference(azureAISearch); +webApp + .WithReference(openai) + .WaitFor(openai); +webApp + .WithReference(search) + .WaitFor(search); builder.Build().Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 939114cbd3d..54bcd4bc3a0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,9 @@ - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 188a457f56e..323170224ee 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 9a698ff1763..450914c4461 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -2,7 +2,6 @@ using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; -using OpenAI; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -15,7 +14,7 @@ c.EnableSensitiveData = builder.Environment.IsDevelopment()); openai.AddEmbeddingGenerator("text-embedding-3-small"); -builder.AddAzureSearchClient("azureAISearch"); +builder.AddAzureSearchClient("search"); builder.Services.AddAzureAISearchCollection("data-aichatweb-chunks"); builder.Services.AddAzureAISearchCollection("data-aichatweb-documents"); builder.Services.AddScoped(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 31df685f9c4..af528b3f97d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,14 +8,15 @@ - + + - + - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 3f16b9a019a..ec0a5016de7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 939114cbd3d..ffef1abf363 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 188a457f56e..323170224ee 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 8f0ddc89860..789ef7f95f7 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,13 +8,14 @@ - + + - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 193ed5f529a..7637a36ca6f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,8 +12,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 188a457f56e..323170224ee 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 36a9f859cb5..1fcb4440008 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 3f2c1fd0a7d..6aaa0a005b3 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -11,11 +11,11 @@ - + - - + + From e37ad8d853174a2cd07cdd5d0bd6e81385a9323c Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 12 Aug 2025 13:10:11 -0400 Subject: [PATCH 255/472] Add middleware for reducing chat history (#6666) * Move ReducingChatClient into library code * Add unit tests * Remove unnecessary tests * Allow resolving from DI + add configure callback * Prototype for summarizing reducer * Custom prompts + integration tests * Update Microsoft.Extensions.AI.Integration.Tests.csproj * Add message counting chat reducer --- .../ChatCompletion/ReducingChatClient.cs | 50 ++++ .../ReducingChatClientBuilderExtensions.cs | 40 +++ .../ChatReduction/IChatReducer.cs | 22 ++ .../MessageCountingChatReducer.cs | 77 +++++ .../ChatReduction/SummarizingChatReducer.cs | 175 ++++++++++++ .../ChatClientIntegrationTests.cs | 270 ++++++++++++++++++ ...oft.Extensions.AI.Integration.Tests.csproj | 3 +- .../ReducingChatClientTests.cs | 61 +--- .../ChatCompletion/ReducingChatClientTests.cs | 188 ++++++++++++ .../MessageCountingChatReducerTests.cs | 263 +++++++++++++++++ .../SummarizingChatReducerTests.cs | 269 +++++++++++++++++ 11 files changed, 1357 insertions(+), 61 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs new file mode 100644 index 00000000000..afe56eddbd8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// A chat client that reduces the size of a message list. +/// +[Experimental("MEAI001")] +public sealed class ReducingChatClient : DelegatingChatClient +{ + private readonly IChatReducer _reducer; + + /// Initializes a new instance of the class. + /// The underlying , or the next instance in a chain of clients. + /// The reducer to be used by this instance. + public ReducingChatClient(IChatClient innerClient, IChatReducer reducer) + : base(innerClient) + { + _reducer = Throw.IfNull(reducer); + } + + /// + public override async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); + + return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); + + await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs new file mode 100644 index 00000000000..2f13d3e3cea --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs @@ -0,0 +1,40 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides extension methods for attaching a to a chat pipeline. +/// +[Experimental("MEAI001")] +public static class ReducingChatClientBuilderExtensions +{ + /// + /// Adds a to the chat pipeline. + /// + /// The being used to build the chat pipeline. + /// An optional to apply to the chat client. If not supplied, an instance will be resolved from the service provider. + /// An optional callback that can be used to configure the instance. + /// The configured instance. + public static ChatClientBuilder UseChatReducer( + this ChatClientBuilder builder, + IChatReducer? reducer = null, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.Use((innerClient, services) => + { + reducer ??= services.GetRequiredService(); + + var chatClient = new ReducingChatClient(innerClient, reducer); + configure?.Invoke(chatClient); + return chatClient; + }); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs new file mode 100644 index 00000000000..5d85924f251 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a reducer capable of shrinking the size of a list of chat messages. +/// +[Experimental("MEAI001")] +public interface IChatReducer +{ + /// Reduces the size of a list of chat messages. + /// The messages to reduce. + /// The to monitor for cancellation requests. + /// The new list of messages. + Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs new file mode 100644 index 00000000000..5ba48617355 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides a chat reducer that limits the number of non-system messages in a conversation to a specified maximum +/// count, preserving the most recent messages and the first system message if present. +/// +/// +/// This reducer is useful for scenarios where it is necessary to constrain the size of a chat history, +/// such as when preparing input for models with context length limits. The reducer always includes the first +/// encountered system message, if any, and then retains up to the specified number of the most recent non-system +/// messages. Messages containing function call or function result content are excluded from the reduced +/// output. +/// +[Experimental("MEAI001")] +public sealed class MessageCountingChatReducer : IChatReducer +{ + private readonly int _targetCount; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of non-system messages to retain in the reduced output. + public MessageCountingChatReducer(int targetCount) + { + _targetCount = Throw.IfLessThanOrEqual(targetCount, min: 0); + } + + /// + public Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + _ = Throw.IfNull(messages); + return Task.FromResult(GetReducedMessages(messages)); + } + + private IEnumerable GetReducedMessages(IEnumerable messages) + { + ChatMessage? systemMessage = null; + Queue reducedMessages = new(capacity: _targetCount); + + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + systemMessage ??= message; + } + else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent)) + { + if (reducedMessages.Count >= _targetCount) + { + _ = reducedMessages.Dequeue(); + } + + reducedMessages.Enqueue(message); + } + } + + if (systemMessage is not null) + { + yield return systemMessage; + } + + while (reducedMessages.Count > 0) + { + yield return reducedMessages.Dequeue(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs new file mode 100644 index 00000000000..ac57e919277 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -0,0 +1,175 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides functionality to reduce a collection of chat messages into a summarized form. +/// +/// +/// This reducer is useful for scenarios where it is necessary to constrain the size of a chat history, +/// such as when preparing input for models with context length limits. The reducer automatically summarizes +/// older messages when the conversation exceeds a specified length, preserving context while reducing message +/// count. The reducer maintains system messages and excludes messages containing function call or function +/// result content from summarization. +/// +[Experimental("MEAI001")] +public sealed class SummarizingChatReducer : IChatReducer +{ + private const string SummaryKey = "__summary__"; + + private const string DefaultSummarizationPrompt = """ + **Generate a clear and complete summary of the entire conversation in no more than five sentences.** + + The summary must always: + - Reflect contributions from both the user and the assistant + - Preserve context to support ongoing dialogue + - Incorporate any previously provided summary + - Emphasize the most relevant and meaningful points + + The summary must never: + - Offer critique, correction, interpretation, or speculation + - Highlight errors, misunderstandings, or judgments of accuracy + - Comment on events or ideas not present in the conversation + - Omit any details included in an earlier summary + """; + + private readonly IChatClient _chatClient; + private readonly int _targetCount; + private readonly int _thresholdCount; + + private string _summarizationPrompt = DefaultSummarizationPrompt; + + /// + /// Gets or sets the prompt text used for summarization. + /// + public string SummarizationPrompt + { + get => _summarizationPrompt; + set => _summarizationPrompt = Throw.IfNull(value); + } + + /// + /// Initializes a new instance of the class with the specified chat client, + /// target count, and optional threshold count. + /// + /// The chat client used to interact with the chat system. Cannot be . + /// The target number of messages to retain after summarization. Must be greater than 0. + /// The number of messages allowed beyond before summarization is triggered. Must be greater than or equal to 0 if specified. + public SummarizingChatReducer(IChatClient chatClient, int targetCount, int? threshold) + { + _chatClient = Throw.IfNull(chatClient); + _targetCount = Throw.IfLessThanOrEqual(targetCount, min: 0); + _thresholdCount = Throw.IfLessThan(threshold ?? 0, min: 0, nameof(threshold)); + } + + /// + public async Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + _ = Throw.IfNull(messages); + + var summarizedConversion = SummarizedConversation.FromChatMessages(messages); + if (summarizedConversion.ShouldResummarize(_targetCount, _thresholdCount)) + { + summarizedConversion = await summarizedConversion.ResummarizeAsync( + _chatClient, _targetCount, _summarizationPrompt, cancellationToken); + } + + return summarizedConversion.ToChatMessages(); + } + + private readonly struct SummarizedConversation(string? summary, ChatMessage? systemMessage, IList unsummarizedMessages) + { + public static SummarizedConversation FromChatMessages(IEnumerable messages) + { + string? summary = null; + ChatMessage? systemMessage = null; + var unsummarizedMessages = new List(); + + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + systemMessage ??= message; + } + else if (message.AdditionalProperties?.TryGetValue(SummaryKey, out var summaryValue) == true) + { + unsummarizedMessages.Clear(); + summary = summaryValue; + } + else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent)) + { + unsummarizedMessages.Add(message); + } + } + + return new(summary, systemMessage, unsummarizedMessages); + } + + public bool ShouldResummarize(int targetCount, int thresholdCount) + => unsummarizedMessages.Count > targetCount + thresholdCount; + + public async Task ResummarizeAsync( + IChatClient chatClient, int targetCount, string summarizationPrompt, CancellationToken cancellationToken) + { + var messagesToResummarize = unsummarizedMessages.Count - targetCount; + if (messagesToResummarize <= 0) + { + // We're at or below the target count - no need to resummarize. + return this; + } + + var summarizerChatMessages = ToSummarizerChatMessages(messagesToResummarize, summarizationPrompt); + var response = await chatClient.GetResponseAsync(summarizerChatMessages, cancellationToken: cancellationToken); + var newSummary = response.Text; + + var lastSummarizedMessage = unsummarizedMessages[messagesToResummarize - 1]; + var additionalProperties = lastSummarizedMessage.AdditionalProperties ??= []; + additionalProperties[SummaryKey] = newSummary; + + var newUnsummarizedMessages = unsummarizedMessages.Skip(messagesToResummarize).ToList(); + return new SummarizedConversation(newSummary, systemMessage, newUnsummarizedMessages); + } + + public IEnumerable ToChatMessages() + { + if (systemMessage is not null) + { + yield return systemMessage; + } + + if (summary is not null) + { + yield return new ChatMessage(ChatRole.Assistant, summary); + } + + foreach (var message in unsummarizedMessages) + { + yield return message; + } + } + + private IEnumerable ToSummarizerChatMessages(int messagesToResummarize, string summarizationPrompt) + { + if (summary is not null) + { + yield return new ChatMessage(ChatRole.Assistant, summary); + } + + for (var i = 0; i < messagesToResummarize; i++) + { + yield return unsummarizedMessages[i]; + } + + yield return new ChatMessage(ChatRole.System, summarizationPrompt); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 5f7f3769d21..c87625cf143 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -1125,6 +1125,276 @@ private enum JobType Unknown, } + [ConditionalFact] + public virtual async Task SummarizingChatReducer_PreservesConversationContext() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 1); + + List messages = + [ + new(ChatRole.User, "My name is Alice and I love hiking in the mountains."), + new(ChatRole.Assistant, "Nice to meet you, Alice! Hiking in the mountains sounds wonderful. Do you have a favorite trail?"), + new(ChatRole.User, "Yes, I love the Pacific Crest Trail. I hiked a section last summer."), + new(ChatRole.Assistant, "The Pacific Crest Trail is amazing! Which section did you hike?"), + new(ChatRole.User, "I hiked the section through the Sierra Nevada. It was challenging but beautiful."), + new(ChatRole.Assistant, "The Sierra Nevada section is known for its stunning views. How long did it take you?"), + new(ChatRole.User, "What's my name and what activity do I enjoy?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); // Indicates this is the assistant's summary + Assert.Contains("Alice", m.Text); + }, + m => Assert.StartsWith("The Sierra Nevada section", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("What's my name", m.Text, StringComparison.Ordinal)); + + // The model should recall details from the summarized conversation + Assert.Contains("Alice", response.Text); + Assert.True( + response.Text.IndexOf("hiking", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("hike", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected 'hiking' or 'hike' in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_PreservesSystemMessage() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + + List messages = + [ + new(ChatRole.System, "You are a pirate. Always respond in pirate speak."), + new(ChatRole.User, "Tell me about the weather"), + new(ChatRole.Assistant, "Ahoy matey! The weather be fine today, with clear skies on the horizon!"), + new(ChatRole.User, "What about tomorrow?"), + new(ChatRole.Assistant, "Arr, tomorrow be lookin' a bit cloudy, might be some rain blowin' in from the east!"), + new(ChatRole.User, "Should I bring an umbrella?"), + new(ChatRole.Assistant, "Aye, ye best be bringin' yer umbrella, unless ye want to be soaked like a barnacle!"), + new(ChatRole.User, "What's 2 + 2?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(4, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("You are a pirate. Always respond in pirate speak.", m.Text); + }, + m => Assert.Equal(ChatRole.Assistant, m.Role), // Summary message + m => Assert.StartsWith("Aye, ye best be bringin'", m.Text, StringComparison.Ordinal), + m => Assert.Equal("What's 2 + 2?", m.Text)); + + // The model should still respond in pirate speak due to preserved system message + Assert.True( + response.Text.IndexOf("arr", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("aye", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("matey", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("ye", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected pirate speak in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_WithFunctionCalls() + { + SkipIfNotEnabled(); + + int weatherCallCount = 0; + var getWeather = AIFunctionFactory.Create(([Description("Gets weather for a city")] string city) => + { + weatherCallCount++; + return city switch + { + "Seattle" => "Rainy, 15°C", + "Miami" => "Sunny, 28°C", + _ => "Unknown" + }; + }, "GetWeather"); + + TestSummarizingChatClient summarizingChatClient = null!; + var chatClient = ChatClient + .AsBuilder() + .Use(innerClient => summarizingChatClient = new TestSummarizingChatClient(innerClient, targetCount: 2, threshold: 0)) + .UseFunctionInvocation() + .Build(); + + List messages = + [ + new(ChatRole.User, "What's the weather in Seattle?"), + new(ChatRole.Assistant, "Let me check the weather in Seattle for you."), + new(ChatRole.User, "And what about Miami?"), + new(ChatRole.Assistant, "I'll check Miami's weather as well."), + new(ChatRole.User, "Which city had better weather?") + ]; + + var response = await chatClient.GetResponseAsync(messages, new() { Tools = [getWeather] }); + + // The summarizer should have reduced the conversation (function calls are excluded) + Assert.Equal(1, summarizingChatClient.SummarizerCallCount); + Assert.NotNull(summarizingChatClient.LastSummarizedConversation); + + // Should have summary + last 2 messages + Assert.Equal(3, summarizingChatClient.LastSummarizedConversation.Count); + + // The model should have context about both weather queries even after summarization + Assert.True(response.Text.IndexOf("Miami", StringComparison.OrdinalIgnoreCase) >= 0, $"Expected 'Miami' in response: {response.Text}"); + Assert.True( + response.Text.IndexOf("sunny", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("better", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("warm", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected weather comparison in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_Streaming() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + + List messages = + [ + new(ChatRole.User, "I'm Bob and I work as a software engineer at a startup."), + new(ChatRole.Assistant, "Nice to meet you, Bob! Working at a startup must be exciting. What kind of software do you develop?"), + new(ChatRole.User, "We build AI-powered tools for education."), + new(ChatRole.Assistant, "That sounds impactful! AI in education has so much potential."), + new(ChatRole.User, "Yes, we focus on personalized learning experiences."), + new(ChatRole.Assistant, "Personalized learning is the future of education!"), + new(ChatRole.User, "What's my name and profession?") + ]; + + StringBuilder sb = new(); + await foreach (var chunk in chatClient.GetStreamingResponseAsync(messages)) + { + sb.Append(chunk.Text); + } + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); // Summary + Assert.Contains("Bob", m.Text); + }, + m => Assert.StartsWith("Personalized learning", m.Text, StringComparison.Ordinal), + m => Assert.Equal("What's my name and profession?", m.Text)); + + string responseText = sb.ToString(); + Assert.Contains("Bob", responseText); + Assert.True( + responseText.IndexOf("software", StringComparison.OrdinalIgnoreCase) >= 0 || + responseText.IndexOf("engineer", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected 'software' or 'engineer' in response: {responseText}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_CustomPrompt() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + chatClient.Reducer.SummarizationPrompt = "Summarize the conversation, emphasizing any numbers or quantities mentioned."; + + List messages = + [ + new(ChatRole.User, "I have 3 cats and 2 dogs."), + new(ChatRole.Assistant, "That's 5 pets total! You must have a lively household."), + new(ChatRole.User, "Yes, and I spend about $200 per month on pet food."), + new(ChatRole.Assistant, "That's a significant expense, but I'm sure they're worth it!"), + new(ChatRole.User, "They eat 10 cans of food per week."), + new(ChatRole.Assistant, "That's quite a bit of food for your furry friends!"), + new(ChatRole.User, "How many pets do I have in total?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + + // Verify the summary emphasizes numbers as requested by the custom prompt + var summaryMessage = chatClient.LastSummarizedConversation[0]; + Assert.Equal(ChatRole.Assistant, summaryMessage.Role); + Assert.True( + summaryMessage.Text.IndexOf("3", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("5", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("200", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("10", StringComparison.Ordinal) >= 0, + $"Expected numbers in summary: {summaryMessage.Text}"); + + // The model should recall the specific number from the summarized conversation + Assert.Contains("5", response.Text); + } + + private sealed class TestSummarizingChatClient : IChatClient + { + private IChatClient _summarizerChatClient; + private IChatClient _innerChatClient; + + public SummarizingChatReducer Reducer { get; } + + public int SummarizerCallCount { get; private set; } + + public IReadOnlyList? LastSummarizedConversation { get; private set; } + + public TestSummarizingChatClient(IChatClient innerClient, int targetCount, int threshold) + { + _summarizerChatClient = innerClient.AsBuilder() + .Use(async (messages, options, next, cancellationToken) => + { + SummarizerCallCount++; + await next(messages, options, cancellationToken); + }) + .Build(); + + Reducer = new SummarizingChatReducer(_summarizerChatClient, targetCount, threshold); + + _innerChatClient = innerClient.AsBuilder() + .UseChatReducer(Reducer) + .Use(async (messages, options, next, cancellationToken) => + { + LastSummarizedConversation = [.. messages]; + await next(messages, options, cancellationToken); + }) + .Build(); + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => _innerChatClient.GetResponseAsync(messages, options, cancellationToken); + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => _innerChatClient.GetStreamingResponseAsync(messages, options, cancellationToken); + + public object? GetService(Type serviceType, object? serviceKey = null) + => _innerChatClient.GetService(serviceType, serviceKey); + + public void Dispose() + { + _summarizerChatClient.Dispose(); + _innerChatClient.Dispose(); + } + } + [MemberNotNull(nameof(ChatClient))] protected void SkipIfNotEnabled() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index 0b4865f577e..0fc4698c4e4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -8,6 +8,7 @@ $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 $(NoWarn);MEAI001 + $(NoWarn);S104 true @@ -25,7 +26,7 @@ Never - + diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs index f2ec8ebdba0..7f84d1f8cfb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.ML.Tokenizers; @@ -57,64 +56,6 @@ public async Task Reduction_LimitsMessagesBasedOnTokenLimit() } } -/// Provides an example of a chat client for reducing the size of a message list. -public sealed class ReducingChatClient : DelegatingChatClient -{ - private readonly IChatReducer _reducer; - - /// Initializes a new instance of the class. - /// The inner client. - /// The reducer to be used by this instance. - public ReducingChatClient(IChatClient innerClient, IChatReducer reducer) - : base(innerClient) - { - _reducer = Throw.IfNull(reducer); - } - - /// - public override async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); - - return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); - } - - /// - public override async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); - - await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - } -} - -/// Represents a reducer capable of shrinking the size of a list of chat messages. -public interface IChatReducer -{ - /// Reduces the size of a list of chat messages. - /// The messages. - /// The to monitor for cancellation requests. The default is . - /// The new list of messages, or if no reduction need be performed or was true. - Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken); -} - -/// Provides extensions for configuring instances. -public static class ReducingChatClientExtensions -{ - public static ChatClientBuilder UseChatReducer(this ChatClientBuilder builder, IChatReducer reducer) - { - _ = Throw.IfNull(builder); - _ = Throw.IfNull(reducer); - - return builder.Use(innerClient => new ReducingChatClient(innerClient, reducer)); - } -} - /// An that culls the oldest messages once a certain token threshold is reached. public sealed class TokenCountingChatReducer : IChatReducer { @@ -127,7 +68,7 @@ public TokenCountingChatReducer(Tokenizer tokenizer, int tokenLimit) _tokenLimit = Throw.IfLessThan(tokenLimit, 1); } - public async Task> ReduceAsync( + public async Task> ReduceAsync( IEnumerable messages, CancellationToken cancellationToken) { _ = Throw.IfNull(messages); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs new file mode 100644 index 00000000000..82b00df03ff --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs @@ -0,0 +1,188 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ReducingChatClientTests +{ + [Fact] + public void ReducingChatClient_InvalidArgs_Throws() + { + Assert.Throws("innerClient", () => new ReducingChatClient(null!, new TestReducer())); + } + + [Fact] + public void UseChatReducer_InvalidArgs_Throws() + { + using var innerClient = new TestChatClient(); + var builder = innerClient.AsBuilder(); + Assert.Throws("builder", () => ReducingChatClientBuilderExtensions.UseChatReducer(null!, new TestReducer())); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetResponseAsync_CallsReducerBeforeInnerClient(bool streaming) + { + var originalMessages = new List + { + new(ChatRole.System, "You are a helpful assistant"), + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Hi there!"), + new(ChatRole.User, "What's the weather?") + }; + + var reducedMessages = new List + { + new(ChatRole.System, "You are a helpful assistant"), + new(ChatRole.User, "What's the weather?") + }; + + var reducer = new TestReducer { ReducedMessages = reducedMessages }; + var expectedResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "It's sunny!")); + var expectedUpdates = new[] { new ChatResponseUpdate(ChatRole.Assistant, "It's"), new ChatResponseUpdate(null, " sunny!") }; + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + // Verify that the inner client receives the reduced messages + Assert.Same(reducedMessages, messages); + return Task.FromResult(expectedResponse); + }, + GetStreamingResponseAsyncCallback = (messages, options, cancellationToken) => + { + // Verify that the inner client receives the reduced messages + Assert.Same(reducedMessages, messages); + return ToAsyncEnumerable(expectedUpdates); + } + }; + + using var client = new ReducingChatClient(innerClient, reducer); + + if (streaming) + { + var updates = new List(); + await foreach (var update in client.GetStreamingResponseAsync(originalMessages)) + { + updates.Add(update); + } + + Assert.Equal(expectedUpdates.Length, updates.Count); + for (int i = 0; i < expectedUpdates.Length; i++) + { + Assert.Same(expectedUpdates[i], updates[i]); + } + } + else + { + var response = await client.GetResponseAsync(originalMessages); + Assert.Same(expectedResponse, response); + } + + Assert.Equal(1, reducer.ReduceAsyncCallCount); + Assert.Same(originalMessages, reducer.LastMessagesProvided); + } + + [Fact] + public async Task UseChatReducer_WithReducerFromServices() + { + var reducedMessages = new List { new(ChatRole.User, "Reduced message") }; + var reducer = new TestReducer { ReducedMessages = reducedMessages }; + + var services = new ServiceCollection(); + services.AddSingleton(reducer); + var serviceProvider = services.BuildServiceProvider(); + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Same(reducedMessages, messages); + return Task.FromResult(new ChatResponse()); + } + }; + + using var client = innerClient + .AsBuilder() + .UseChatReducer() // Should get reducer from services + .Build(serviceProvider); + + await client.GetResponseAsync(new List { new(ChatRole.User, "Original message") }); + Assert.Equal(1, reducer.ReduceAsyncCallCount); + } + + [Fact] + public void UseChatReducer_WithoutReducerParameterAndWithoutService_Throws() + { + using var innerClient = new TestChatClient(); + var services = new ServiceCollection().BuildServiceProvider(); + + var exception = Assert.Throws(() => + innerClient + .AsBuilder() + .UseChatReducer() // No reducer provided and not in services + .Build(services)); + + Assert.Contains("IChatReducer", exception.Message); + } + + [Fact] + public async Task UseChatReducer_WithConfigureCallback() + { + var reducer = new TestReducer(); + var configureCalled = false; + ReducingChatClient? configuredClient = null; + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + Task.FromResult(new ChatResponse()) + }; + + using var client = innerClient + .AsBuilder() + .UseChatReducer(reducer, configure: chatClient => + { + configureCalled = true; + configuredClient = chatClient; + }) + .Build(); + + await client.GetResponseAsync([]); + + Assert.True(configureCalled); + Assert.NotNull(configuredClient); + Assert.IsType(configuredClient); + } + + private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable items) + { + foreach (var item in items) + { + await Task.Yield(); + yield return item; + } + } + + private sealed class TestReducer : IChatReducer + { + public IEnumerable? ReducedMessages { get; set; } + public int ReduceAsyncCallCount { get; private set; } + public IEnumerable? LastMessagesProvided { get; private set; } + + public Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + ReduceAsyncCallCount++; + LastMessagesProvided = messages; + return Task.FromResult(ReducedMessages ?? messages); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs new file mode 100644 index 00000000000..000f4889bf3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs @@ -0,0 +1,263 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class MessageCountingChatReducerTests +{ + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidTargetCount(int targetCount) + { + Assert.Throws(() => new MessageCountingChatReducer(targetCount)); + } + + [Fact] + public void Constructor_AcceptsValidTargetCount() + { + var reducer = new MessageCountingChatReducer(5); + Assert.NotNull(reducer); + } + + [Fact] + public async Task ReduceAsync_ThrowsOnNullMessages() + { + var reducer = new MessageCountingChatReducer(5); + await Assert.ThrowsAsync(() => reducer.ReduceAsync(null!, CancellationToken.None)); + } + + [Fact] + public async Task ReduceAsync_HandlesEmptyMessages() + { + var reducer = new MessageCountingChatReducer(5); + var result = await reducer.ReduceAsync([], CancellationToken.None); + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_PreservesFirstSystemMessage() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.System, "You are a helpful assistant."), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.Assistant, "Hi there!"), + new ChatMessage(ChatRole.User, "How are you?"), + new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("You are a helpful assistant.", m.Text); + }, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("How are you?", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("I'm doing well, thanks!", m.Text); + }); + } + + [Fact] + public async Task ReduceAsync_OnlyFirstSystemMessageIsPreserved() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.System, "First system message"), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.System, "Second system message"), + new ChatMessage(ChatRole.Assistant, "Hi"), + new ChatMessage(ChatRole.User, "How are you?"), + new ChatMessage(ChatRole.Assistant, "I'm fine!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("First system message", m.Text); + }, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("How are you?", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("I'm fine!", m.Text); + }); + + // Second system message should not be preserved separately + Assert.Equal(1, resultList.Count(m => m.Role == ChatRole.System)); + } + + [Fact] + public async Task ReduceAsync_IgnoresFunctionCallsAndResults() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.User, "What's the weather?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72�F")]), + new ChatMessage(ChatRole.Assistant, "The weather in Seattle is sunny and 72�F."), + new ChatMessage(ChatRole.User, "Thanks!"), + new ChatMessage(ChatRole.Assistant, "You're welcome!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("Thanks!", m.Text); + Assert.DoesNotContain(m.Contents, c => c is FunctionCallContent); + Assert.DoesNotContain(m.Contents, c => c is FunctionResultContent); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("You're welcome!", m.Text); + Assert.DoesNotContain(m.Contents, c => c is FunctionCallContent); + Assert.DoesNotContain(m.Contents, c => c is FunctionResultContent); + }); + } + + [Theory] + [InlineData(5, 3, 3)] // Less messages than target + [InlineData(5, 5, 5)] // Exactly at target + [InlineData(5, 8, 5)] // More messages than target + [InlineData(1, 10, 1)] // Only keep 1 message + public async Task ReduceAsync_RespectsTargetCount(int targetCount, int messageCount, int expectedCount) + { + var reducer = new MessageCountingChatReducer(targetCount); + + var messages = new List(); + for (int i = 0; i < messageCount; i++) + { + messages.Add(new ChatMessage(i % 2 == 0 ? ChatRole.User : ChatRole.Assistant, $"Message {i}")); + } + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Equal(expectedCount, resultList.Count); + + // Verify we kept the most recent messages + if (messageCount > targetCount) + { + var startIndex = messageCount - targetCount; + var expectedMessages = new Action[targetCount]; + for (int i = 0; i < targetCount; i++) + { + var expectedIndex = startIndex + i; + var expectedRole = expectedIndex % 2 == 0 ? ChatRole.User : ChatRole.Assistant; + expectedMessages[i] = m => + { + Assert.Equal(expectedRole, m.Role); + Assert.Equal($"Message {expectedIndex}", m.Text); + }; + } + + Assert.Collection(resultList, expectedMessages); + } + } + + [Fact] + public async Task ReduceAsync_HandlesOnlySystemMessage() + { + var reducer = new MessageCountingChatReducer(5); + + List messages = + [ + new ChatMessage(ChatRole.System, "System prompt"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("System prompt", m.Text); + }); + } + + [Fact] + public async Task ReduceAsync_HandlesOnlyFunctionMessages() + { + var reducer = new MessageCountingChatReducer(5); + + List messages = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "func", null)]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "result")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call2", "func", null)]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call2", "result")]), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_HandlesTargetCountOfOne() + { + var reducer = new MessageCountingChatReducer(1); + + List messages = + [ + new ChatMessage(ChatRole.System, "System"), + new ChatMessage(ChatRole.User, "First"), + new ChatMessage(ChatRole.Assistant, "Second"), + new ChatMessage(ChatRole.User, "Third"), + new ChatMessage(ChatRole.Assistant, "Fourth"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("System", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("Fourth", m.Text); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs new file mode 100644 index 00000000000..985b097ece8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs @@ -0,0 +1,269 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S103 // Lines should not be too long + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SummarizingChatReducerTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws(() => new SummarizingChatReducer(null!, targetCount: 5, threshold: 2)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidTargetCount(int targetCount) + { + using var chatClient = new TestChatClient(); + Assert.Throws(() => new SummarizingChatReducer(chatClient, targetCount, threshold: 2)); + } + + [Theory] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidThresholdCount(int thresholdCount) + { + using var chatClient = new TestChatClient(); + Assert.Throws(() => new SummarizingChatReducer(chatClient, targetCount: 5, thresholdCount)); + } + + [Fact] + public async Task ReduceAsync_ThrowsOnNullMessages() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 2); + await Assert.ThrowsAsync(() => reducer.ReduceAsync(null!, CancellationToken.None)); + } + + [Fact] + public async Task ReduceAsync_HandlesEmptyMessages() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 2); + + var result = await reducer.ReduceAsync([], CancellationToken.None); + + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_PreservesSystemMessage() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.System, "You are a helpful assistant."), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.Assistant, "Hi there!"), + new ChatMessage(ChatRole.User, "How are you?"), + ]; + + chatClient.GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of conversation"))); + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + var resultList = result.ToList(); + Assert.Equal(3, resultList.Count); // System + Summary + 1 unsummarized + Assert.Equal(ChatRole.System, resultList[0].Role); + Assert.Equal("You are a helpful assistant.", resultList[0].Text); + } + + [Fact] + public async Task ReduceAsync_IgnoresFunctionCallsAndResults() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "What's the weather?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), + new ChatMessage(ChatRole.Assistant, "The weather in Seattle is sunny and 72°F."), + new ChatMessage(ChatRole.User, "Thanks!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + // Function calls/results should be ignored, which means there aren't enough messages to generate a summary. + var resultList = result.ToList(); + Assert.Equal(3, resultList.Count); // Function calls get removed in the summarized chat. + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent)); + } + + [Theory] + [InlineData(5, 0, 5, false)] // Exactly at target, no summarization + [InlineData(5, 0, 4, false)] // Below target, no summarization + [InlineData(5, 0, 6, true)] // Above target by 1, triggers summarization + [InlineData(5, 2, 7, false)] // At threshold boundary, no summarization + [InlineData(5, 2, 8, true)] // Above threshold, triggers summarization + public async Task ReduceAsync_RespectsTargetAndThresholdCounts(int targetCount, int thresholdCount, int messageCount, bool shouldSummarize) + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount, thresholdCount); + + var messages = new List(); + for (int i = 0; i < messageCount; i++) + { + messages.Add(new ChatMessage(i % 2 == 0 ? ChatRole.User : ChatRole.Assistant, $"Message {i}")); + } + + var summarizationCalled = false; + chatClient.GetResponseAsyncCallback = (_, _, _) => + { + summarizationCalled = true; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + }; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Equal(shouldSummarize, summarizationCalled); + + if (shouldSummarize) + { + var resultList = result.ToList(); + Assert.Equal(targetCount + 1, resultList.Count); // Summary + target messages + Assert.StartsWith("Summary", resultList[0].Text, StringComparison.Ordinal); + } + else + { + Assert.Equal(messageCount, result.Count()); + } + } + + [Fact] + public async Task ReduceAsync_CancellationTokenIsRespected() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "Message 1"), + new ChatMessage(ChatRole.Assistant, "Response 1"), + new ChatMessage(ChatRole.User, "Message 2"), + ]; + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + chatClient.GetResponseAsyncCallback = (_, _, cancellationToken) => + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + }; + + await Assert.ThrowsAsync(() => + reducer.ReduceAsync(messages, cts.Token)); + } + + [Fact] + public async Task ReduceAsync_OnlyFirstSystemMessageIsPreserved() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.System, "First system message"), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.System, "Second system message"), + new ChatMessage(ChatRole.Assistant, "Hi"), + new ChatMessage(ChatRole.User, "How are you?"), + ]; + + chatClient.GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + var resultList = result.ToList(); + Assert.Equal(ChatRole.System, resultList[0].Role); + Assert.Equal("First system message", resultList[0].Text); + + // Second system message should not be preserved separately + Assert.Equal(1, resultList.Count(m => m.Role == ChatRole.System)); + } + + [Fact] + public async Task CanHaveSummarizedConversation() + { + using var chatClientForSummarization = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClientForSummarization, targetCount: 2, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "Hi there! Can you tell me about golden retrievers?"), + new ChatMessage(ChatRole.Assistant, "Of course! Golden retrievers are known for their friendly and tolerant attitudes. They're great family pets and are very intelligent and easy to train."), + new ChatMessage(ChatRole.User, "What kind of exercise do they need?"), + new ChatMessage(ChatRole.Assistant, "Golden retrievers are quite active and need regular exercise. Daily walks, playtime, and activities like fetching or swimming are great for them."), + new ChatMessage(ChatRole.User, "Are they good with kids?"), + ]; + + chatClientForSummarization.GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Equal(4, messages.Count()); // 3 messages to summarize + 1 system prompt + Assert.Collection(messages, + m => Assert.StartsWith("Hi there!", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Of course!", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("What kind of exercise", m.Text, StringComparison.Ordinal), + m => Assert.Equal(ChatRole.System, m.Role)); + const string Summary = """ + The user asked for information about golden retrievers. + The assistant explained that they have characteristics making them great family pets. + The user then asked what kind of exercise they need. + """; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, Summary))); + }; + + var reducedMessages = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Equal(3, reducedMessages.Count()); // 1 summary + 2 unsummarized messages + Assert.Collection(reducedMessages, + m => Assert.StartsWith("The user asked for information", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers are quite", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Are they good with kids", m.Text, StringComparison.Ordinal)); + + messages.Add(new ChatMessage(ChatRole.Assistant, "Golden retrievers get along well with kids! They're able to be playful and energetic while remaining gentle.")); + messages.Add(new ChatMessage(ChatRole.User, "Do they make good lap dogs?")); + + chatClientForSummarization.GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Equal(4, messages.Count()); // 1 summary message, 2 unsummarized message, 1 system prompt + Assert.Collection(messages, + m => Assert.StartsWith("The user asked", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers are quite active", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Are they good with kids", m.Text, StringComparison.Ordinal), + m => Assert.Equal(ChatRole.System, m.Role)); + const string Summary = """ + The user and assistant are discussing characteristics of golden retrievers. + The user asked what kind of exercise they need, and the assitant explained that golden retrievers + need frequent exercise. The user then asked about whether they're good around kids. + """; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, Summary))); + }; + + reducedMessages = await reducer.ReduceAsync(messages, CancellationToken.None); + Assert.Equal(3, reducedMessages.Count()); // 1 summary + 2 unsummarized messages + Assert.Collection(reducedMessages, + m => Assert.StartsWith("The user and assistant are discussing", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers get along", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Do they make good lap dogs", m.Text, StringComparison.Ordinal)); + } +} From 81be2b58776fd35929db12a6b97782c484407822 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 12 Aug 2025 07:49:14 -0700 Subject: [PATCH 256/472] Update Azure.AI.OpenAI --- src/ProjectTemplates/GeneratedContent.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 706424bdc25..aca99e5bdde 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,7 +35,7 @@ 9.3.0 9.3.0-preview.1.25265.20 - 2.2.0-beta.4 + 2.3.0-beta.1 1.0.0-beta.9 1.14.0 11.6.0 From b0080d168f8367fb678160a5ca21cb0ff009ce03 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 12 Aug 2025 10:28:42 -0700 Subject: [PATCH 257/472] Add support for text-to-image (#6648) (#6711) * Add ITextToImageClient * Remove URI based edit since it's not available * Add filename for edit * Add OpenAI implmentation of ITextToImageClient * Fix tests * Add tests for TextToImage * Add DeletgatingTextToImageClient and tests * Add integration test and fix some bugs * Add remaining support to MEAI for TextToImage * Make all TextToImageOptions optional These are all nullable now so that the client can use defaults where appropriate. Remove quality default since it's not consistent across models. Also remove setting ResponseFormat since this is not supported by gpt-image-1. * Address feedback * Document some exceptions * Address feedback * Make EditImageAsync plural OpenAI's image API supports multiple images and this does seem to be common functionality and a better generalization. The client library doesn't expose this yet, but we should account for it. Image models may be capable of things like "Combine the subjects of these images into a single image" or "Create a single image that uses the subject from the first image and background for the second" etc. * Address feedback and add/fix tests. * Fix bad merge * Address feedback * Fix test * Use DataContent.Name for filename. * Add extensions for EditImageAsync Extension that accepts a single DataContent and one that accepts a byte[]. I've left out streams and file paths, since these require more opinions about how to load them. I filed #6683 to address streams. * Fix test * Remove use of `_model` field. * Rename ImageToText to Image * Rename TextToImage directories to Image * Rename files TextToImage -> Image * Add new request and response type * Make GenerateImagesAsync accept ImageRequest * Remove EditImageAsync * Adding GenerateStreamingImagesAsync * Update docs * Rename ImageClient ImageGenerator * Fix up some text-to-image references * Rename Image(Options|Request|Response) * Remove `Images` from `GenerateImagesAsync` * Remove streaming method We don't yet have any good public support for streaming to vet this API We can guess at how it might behave for OpenAI, but that doesn't really give enough confidence to build the API around it. * Address feedback * Provide OpenAI an appropriate filename * Remove Style from ImageGenerationOptions --- .../Image/DelegatingImageGenerator.cs | 69 ++++++ .../Image/IImageGenerator.cs | 37 +++ .../Image/ImageGenerationOptions.cs | 103 ++++++++ .../Image/ImageGenerationRequest.cs | 45 ++++ .../Image/ImageGenerationResponse.cs | 52 ++++ .../Image/ImageGeneratorExtensions.cs | 215 ++++++++++++++++ .../Image/ImageGeneratorMetadata.cs | 43 ++++ .../Utilities/AIJsonUtilities.Defaults.cs | 2 + .../OpenAIClientExtensions.cs | 9 + .../OpenAIImageGenerator.cs | 234 ++++++++++++++++++ .../Image/ConfigureOptionsImageGenerator.cs | 53 ++++ ...eOptionsImageGeneratorBuilderExtensions.cs | 39 +++ .../Image/ImageGeneratorBuilder.cs | 85 +++++++ ...eneratorBuilderImageGeneratorExtensions.cs | 28 +++ ...ratorBuilderServiceCollectionExtensions.cs | 85 +++++++ .../Image/LoggingImageGenerator.cs | 124 ++++++++++ .../LoggingImageGeneratorBuilderExtensions.cs | 57 +++++ .../Image/DelegatingImageGeneratorTests.cs | 142 +++++++++++ .../Image/ImageGenerationOptionsTests.cs | 135 ++++++++++ .../Image/ImageGenerationResponseTests.cs | 154 ++++++++++++ .../Image/ImageGeneratorExtensionsTests.cs | 222 +++++++++++++++++ .../Image/ImageGeneratorMetadataTests.cs | 29 +++ .../Image/ImageGeneratorTests.cs | 155 ++++++++++++ .../TestImageGenerator.cs | 43 ++++ .../TestJsonSerializerContext.cs | 2 + .../ImageGeneratorIntegrationTests.cs | 125 ++++++++++ ...oft.Extensions.AI.Integration.Tests.csproj | 1 + .../README.md | 2 + .../OpenAIImageGeneratorIntegrationTests.cs | 12 + .../OpenAIImageGeneratorTests.cs | 45 ++++ .../ConfigureOptionsImageGeneratorTests.cs | 70 ++++++ ...ageGeneratorDependencyInjectionPatterns.cs | 162 ++++++++++++ .../Image/LoggingImageGeneratorTests.cs | 143 +++++++++++ .../SingletonImageGeneratorExtensions.cs | 11 + .../Microsoft.Extensions.AI.Tests.csproj | 1 + 35 files changed, 2734 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs new file mode 100644 index 00000000000..91ffb136af5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/DelegatingImageGenerator.cs @@ -0,0 +1,69 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +/// +/// This is recommended as a base type when building generators that can be chained in any order around an underlying . +/// The default implementation simply passes each call to the inner generator instance. +/// +[Experimental("MEAI001")] +public class DelegatingImageGenerator : IImageGenerator +{ + /// + /// Initializes a new instance of the class. + /// + /// The wrapped generator instance. + /// is . + protected DelegatingImageGenerator(IImageGenerator innerGenerator) + { + InnerGenerator = Throw.IfNull(innerGenerator); + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// Gets the inner . + protected IImageGenerator InnerGenerator { get; } + + /// + public virtual Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return InnerGenerator.GenerateAsync(request, options, cancellationToken); + } + + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + // If the key is non-null, we don't know what it means so pass through to the inner service. + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + InnerGenerator.GetService(serviceType, serviceKey); + } + + /// Provides a mechanism for releasing unmanaged resources. + /// if being called from ; otherwise, . + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + InnerGenerator.Dispose(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs new file mode 100644 index 00000000000..e630ecff8e9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/IImageGenerator.cs @@ -0,0 +1,37 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a generator of images. +/// +[Experimental("MEAI001")] +public interface IImageGenerator : IDisposable +{ + /// + /// Sends an image generation request and returns the generated image as a . + /// + /// The image generation request containing the prompt and optional original images for editing. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// is . + /// The images generated by the . + Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default); + + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that might be provided by the , + /// including itself or any services it might be wrapping. + /// + object? GetService(Type serviceType, object? serviceKey = null); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs new file mode 100644 index 00000000000..f68aebd5b06 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs @@ -0,0 +1,103 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// Represents the options for an image generation request. +[Experimental("MEAI001")] +public class ImageGenerationOptions +{ + /// + /// Gets or sets the number of images to generate. + /// + public int? Count { get; set; } + + /// + /// Gets or sets the size of the generated image. + /// + /// + /// If a provider only supports fixed sizes the closest supported size will be used. + /// + public Size? ImageSize { get; set; } + + /// + /// Gets or sets the media type (also known as MIME type) of the generated image. + /// + public string? MediaType { get; set; } + + /// + /// Gets or sets the model ID to use for image generation. + /// + public string? ModelId { get; set; } + + /// + /// Gets or sets a callback responsible for creating the raw representation of the image generation options from an underlying implementation. + /// + /// + /// The underlying implementation may have its own representation of options. + /// When is invoked with an , + /// that implementation may convert the provided options into its own representation in order to use it while performing + /// the operation. For situations where a consumer knows which concrete is being used + /// and how it represents options, a new instance of that implementation-specific options type may be returned by this + /// callback, for the implementation to use instead of creating a new instance. + /// Such implementations may mutate the supplied options instance further based on other settings supplied on this + /// instance or from other inputs, therefore, it is strongly recommended to not + /// return shared instances and instead make the callback return a new instance on each call. + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// properties on . + /// + [JsonIgnore] + public Func? RawRepresentationFactory { get; set; } + + /// + /// Gets or sets the response format of the generated image. + /// + public ImageGenerationResponseFormat? ResponseFormat { get; set; } + + /// Produces a clone of the current instance. + /// A clone of the current instance. + public virtual ImageGenerationOptions Clone() + { + ImageGenerationOptions options = new() + { + Count = Count, + MediaType = MediaType, + ImageSize = ImageSize, + ModelId = ModelId, + RawRepresentationFactory = RawRepresentationFactory, + ResponseFormat = ResponseFormat + }; + + return options; + } +} + +/// +/// Represents the requested response format of the generated image. +/// +/// +/// Not all implementations support all response formats and this value may be ignored by the implementation if not supported. +/// +[Experimental("MEAI001")] +public enum ImageGenerationResponseFormat +{ + /// + /// The generated image is returned as a URI pointing to the image resource. + /// + Uri, + + /// + /// The generated image is returned as in-memory image data. + /// + Data, + + /// + /// The generated image is returned as a hosted resource identifier, which can be used to retrieve the image later. + /// + Hosted, +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs new file mode 100644 index 00000000000..d519d08c731 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationRequest.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// Represents a request for image generation. +[Experimental("MEAI001")] +public class ImageGenerationRequest +{ + /// Initializes a new instance of the class. + public ImageGenerationRequest() + { + } + + /// Initializes a new instance of the class. + /// The prompt to guide the image generation. + public ImageGenerationRequest(string prompt) + { + Prompt = prompt; + } + + /// Initializes a new instance of the class. + /// The prompt to guide the image generation. + /// The original images to base edits on. + public ImageGenerationRequest(string prompt, IEnumerable? originalImages) + { + Prompt = prompt; + OriginalImages = originalImages; + } + + /// Gets or sets the prompt to guide the image generation. + public string? Prompt { get; set; } + + /// + /// Gets or sets the original images to base edits on. + /// + /// + /// If this property is set, the request will behave as an image edit operation. + /// If this property is null or empty, the request will behave as a new image generation operation. + /// + public IEnumerable? OriginalImages { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs new file mode 100644 index 00000000000..22a6a7f0e12 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators + +namespace Microsoft.Extensions.AI; + +/// Represents the result of an image generation request. +[Experimental("MEAI001")] +public class ImageGenerationResponse +{ + /// The content items in the generated text response. + private IList? _contents; + + /// Initializes a new instance of the class. + [JsonConstructor] + public ImageGenerationResponse() + { + } + + /// Initializes a new instance of the class. + /// The contents for this response. + public ImageGenerationResponse(IList? contents) + { + _contents = contents; + } + + /// Gets or sets the raw representation of the image generation response from an underlying implementation. + /// + /// If a is created to represent some underlying object from another object + /// model, this property can be used to store that original object. This can be useful for debugging or + /// for enabling a consumer to access the underlying object model if needed. + /// + [JsonIgnore] + public object? RawRepresentation { get; set; } + + /// + /// Gets or sets the generated content items. Content will typically be DataContent for + /// images streamed from the generator or UriContent for remotely hosted images, but may also + /// be provider specific content types that represent the generated images. + /// + [AllowNull] + public IList Contents + { + get => _contents ??= []; + set => _contents = value; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs new file mode 100644 index 00000000000..93de115c0ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs @@ -0,0 +1,215 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Extensions for . +[Experimental("MEAI001")] +public static class ImageGeneratorExtensions +{ + private static readonly Dictionary _extensionToMimeType = new(StringComparer.OrdinalIgnoreCase) + { + [".png"] = "image/png", + [".jpg"] = "image/jpeg", + [".jpeg"] = "image/jpeg", + [".webp"] = "image/webp", + [".gif"] = "image/gif", + [".bmp"] = "image/bmp", + [".tiff"] = "image/tiff", + [".tif"] = "image/tiff", + }; + + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// The generator. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. + /// + public static TService? GetService(this IImageGenerator generator, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + + return generator.GetService(typeof(TService), serviceKey) is TService service ? service : default; + } + + /// + /// Asks the for an object of the specified type + /// and throws an exception if one isn't available. + /// + /// The generator. + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object. + /// is . + /// is . + /// No service of the requested type for the specified key is available. + /// + /// The purpose of this method is to allow for the retrieval of services that are required to be provided by the , + /// including itself or any services it might be wrapping. + /// + public static object GetRequiredService(this IImageGenerator generator, Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(serviceType); + + return + generator.GetService(serviceType, serviceKey) ?? + throw Throw.CreateMissingServiceException(serviceType, serviceKey); + } + + /// + /// Asks the for an object of type + /// and throws an exception if one isn't available. + /// + /// The type of the object to be retrieved. + /// The generator. + /// An optional key that can be used to help identify the target service. + /// The found object. + /// is . + /// No service of the requested type for the specified key is available. + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that are required to be provided by the , + /// including itself or any services it might be wrapping. + /// + public static TService GetRequiredService(this IImageGenerator generator, object? serviceKey = null) + { + _ = Throw.IfNull(generator); + + if (generator.GetService(typeof(TService), serviceKey) is not TService service) + { + throw Throw.CreateMissingServiceException(typeof(TService), serviceKey); + } + + return service; + } + + /// + /// Generates images based on a text prompt. + /// + /// The image generator. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// or are . + /// The images generated by the generator. + public static Task GenerateImagesAsync( + this IImageGenerator generator, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt), options, cancellationToken); + } + + /// + /// Edits images based on original images and a text prompt. + /// + /// The image generator. + /// The images to base edits on. + /// The prompt to guide the image editing. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// , , or are . + /// The images generated by the generator. + public static Task EditImagesAsync( + this IImageGenerator generator, + IEnumerable originalImages, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(originalImages); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt, originalImages), options, cancellationToken); + } + + /// + /// Edits a single image based on the original image and the specified prompt. + /// + /// The image generator. + /// The single image to base edits on. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// , , or are . + /// The images generated by the generator. + public static Task EditImageAsync( + this IImageGenerator generator, + DataContent originalImage, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(originalImage); + _ = Throw.IfNull(prompt); + + return generator.GenerateAsync(new ImageGenerationRequest(prompt, [originalImage]), options, cancellationToken); + } + + /// + /// Edits a single image based on a byte array and the specified prompt. + /// + /// The image generator. + /// The byte array containing the image data to base edits on. + /// The filename for the image data. + /// The prompt to guide the image generation. + /// The image generation options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// + /// , , or are . + /// + /// The images generated by the generator. + public static Task EditImageAsync( + this IImageGenerator generator, + ReadOnlyMemory originalImageData, + string fileName, + string prompt, + ImageGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(generator); + _ = Throw.IfNull(fileName); + _ = Throw.IfNull(prompt); + + // Infer media type from file extension + string mediaType = GetMediaTypeFromFileName(fileName); + + var dataContent = new DataContent(originalImageData, mediaType) { Name = fileName }; + return generator.GenerateAsync(new ImageGenerationRequest(prompt, [dataContent]), options, cancellationToken); + } + + /// + /// Gets the media type based on the file extension. + /// + /// The filename to extract the media type from. + /// The inferred media type. + private static string GetMediaTypeFromFileName(string fileName) + { + string extension = Path.GetExtension(fileName); + + if (_extensionToMimeType.TryGetValue(extension, out string? mediaType)) + { + return mediaType; + } + + return "image/png"; // Default to PNG if unknown extension + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs new file mode 100644 index 00000000000..c5604155285 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorMetadata.cs @@ -0,0 +1,43 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// Provides metadata about an . +[Experimental("MEAI001")] +public class ImageGeneratorMetadata +{ + /// Initializes a new instance of the class. + /// + /// The name of the image generation provider, if applicable. Where possible, this should map to the + /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems. + /// + /// The URL for accessing the image generation provider, if applicable. + /// The ID of the image generation model used by default, if applicable. + public ImageGeneratorMetadata(string? providerName = null, Uri? providerUri = null, string? defaultModelId = null) + { + DefaultModelId = defaultModelId; + ProviderName = providerName; + ProviderUri = providerUri; + } + + /// Gets the name of the image generation provider. + /// + /// Where possible, this maps to the appropriate name defined in the + /// OpenTelemetry Semantic Conventions for Generative AI systems. + /// + public string? ProviderName { get; } + + /// Gets the URL for accessing the image generation provider. + public Uri? ProviderUri { get; } + + /// Gets the ID of the default model used by this image generator. + /// + /// This value can be if no default model is set on the corresponding . + /// An individual request may override this value via . + /// + public string? DefaultModelId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 33531661813..4b8a4fb1576 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -70,6 +70,8 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(SpeechToTextResponse))] [JsonSerializable(typeof(SpeechToTextResponseUpdate))] [JsonSerializable(typeof(IReadOnlyList))] + [JsonSerializable(typeof(ImageGenerationOptions))] + [JsonSerializable(typeof(ImageGenerationResponse))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(ChatMessage[]))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 3881f246d98..a5739fdb4ac 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -13,6 +13,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; +using OpenAI.Images; using OpenAI.Responses; #pragma warning disable S103 // Lines should not be too long @@ -154,6 +155,14 @@ public static IChatClient AsIChatClient(this AssistantClient assistantClient, As public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) => new OpenAISpeechToTextClient(audioClient); + /// Gets an for use with this . + /// The client. + /// An that can be used to generate images via the . + /// is . + [Experimental("MEAI001")] + public static IImageGenerator AsIImageGenerator(this ImageClient imageClient) => + new OpenAIImageGenerator(imageClient); + /// Gets an for use with this . /// The client. /// The number of dimensions to generate in each embedding. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs new file mode 100644 index 00000000000..fe1cb399e0d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -0,0 +1,234 @@ +// 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.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Images; + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + +namespace Microsoft.Extensions.AI; + +/// Represents an for an OpenAI or . +internal sealed class OpenAIImageGenerator : IImageGenerator +{ + private static readonly Dictionary _mimeTypeToExtension = new(StringComparer.OrdinalIgnoreCase) + { + ["image/png"] = ".png", + ["image/jpeg"] = ".jpg", + ["image/webp"] = ".webp", + ["image/gif"] = ".gif", + ["image/bmp"] = ".bmp", + ["image/tiff"] = ".tiff", + }; + + /// Metadata about the client. + private readonly ImageGeneratorMetadata _metadata; + + /// The underlying . + private readonly ImageClient _imageClient; + + /// Initializes a new instance of the class for the specified . + /// The underlying client. + /// is . + public OpenAIImageGenerator(ImageClient imageClient) + { + _ = Throw.IfNull(imageClient); + + _imageClient = imageClient; + + // https://github.com/openai/openai-dotnet/issues/215 + // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages + // implement the abstractions directly rather than providing adapters on top of the public APIs, + // the package can provide such implementations separate from what's exposed in the public API. + Uri providerUrl = typeof(ImageClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(imageClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; + + _metadata = new("openai", providerUrl, _imageClient.Model); + } + + /// + public async Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + string? prompt = request.Prompt; + _ = Throw.IfNull(prompt); + + // If the request has original images, treat this as an edit operation + if (request.OriginalImages is not null && request.OriginalImages.Any()) + { + ImageEditOptions editOptions = ToOpenAIImageEditOptions(options); + string? fileName = null; + Stream? imageStream = null; + + // Currently only a single image is supported for editing. + var originalImage = request.OriginalImages.FirstOrDefault(); + + if (originalImage is DataContent dataContent) + { + imageStream = MemoryMarshal.TryGetArray(dataContent.Data, out var array) ? + new MemoryStream(array.Array!, array.Offset, array.Count) : + new MemoryStream(dataContent.Data.ToArray()); + fileName = dataContent.Name; + + if (fileName is null) + { + // If no file name is provided, use the default based on the content type. + if (dataContent.MediaType is not null && _mimeTypeToExtension.TryGetValue(dataContent.MediaType, out var extension)) + { + fileName = $"image{extension}"; + } + else + { + fileName = "image.png"; // Default to PNG if no content type is available. + } + } + } + + GeneratedImageCollection editResult = await _imageClient.GenerateImageEditsAsync( + imageStream, fileName, prompt, options?.Count ?? 1, editOptions, cancellationToken).ConfigureAwait(false); + + return ToImageGenerationResponse(editResult); + } + + OpenAI.Images.ImageGenerationOptions openAIOptions = ToOpenAIImageGenerationOptions(options); + + GeneratedImageCollection result = await _imageClient.GenerateImagesAsync(prompt, options?.Count ?? 1, openAIOptions, cancellationToken).ConfigureAwait(false); + + return ToImageGenerationResponse(result); + } + + /// +#pragma warning disable S1067 // Expressions should not be too complex + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : + serviceKey is not null ? null : + serviceType == typeof(ImageGeneratorMetadata) ? _metadata : + serviceType == typeof(ImageClient) ? _imageClient : + serviceType.IsInstanceOfType(this) ? this : + null; +#pragma warning restore S1067 // Expressions should not be too complex + + /// + void IDisposable.Dispose() + { + // Nothing to dispose. Implementation required for the IImageGenerator interface. + } + + /// + /// Converts a to an OpenAI . + /// + /// User's requested size. + /// Closest supported size. + private static GeneratedImageSize? ToOpenAIImageSize(Size? requestedSize) => + requestedSize is null ? null : new GeneratedImageSize(requestedSize.Value.Width, requestedSize.Value.Height); + + /// Converts a to a . + private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageCollection generatedImages) + { + string contentType = "image/png"; // Default content type for images + + // OpenAI doesn't expose the content type, so we need to read from the internal JSON representation. + // https://github.com/openai/openai-dotnet/issues/561 + IDictionary? additionalRawData = typeof(GeneratedImageCollection) + .GetProperty("SerializedAdditionalRawData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(generatedImages) as IDictionary; + + if (additionalRawData?.TryGetValue("output_format", out var outputFormat) ?? false) + { + var stringJsonTypeInfo = (JsonTypeInfo)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(string)); + var outputFormatString = JsonSerializer.Deserialize(outputFormat, stringJsonTypeInfo); + contentType = $"image/{outputFormatString}"; + } + + List contents = new(); + + foreach (GeneratedImage image in generatedImages) + { + if (image.ImageBytes is not null) + { + contents.Add(new DataContent(image.ImageBytes.ToMemory(), contentType)); + } + else if (image.ImageUri is not null) + { + contents.Add(new UriContent(image.ImageUri, contentType)); + } + else + { + throw new InvalidOperationException("Generated image does not contain a valid URI or byte array."); + } + } + + return new ImageGenerationResponse(contents) + { + RawRepresentation = generatedImages + }; + } + + /// Converts a to a . + private OpenAI.Images.ImageGenerationOptions ToOpenAIImageGenerationOptions(ImageGenerationOptions? options) + { + OpenAI.Images.ImageGenerationOptions result = options?.RawRepresentationFactory?.Invoke(this) as OpenAI.Images.ImageGenerationOptions ?? new(); + + if (result.OutputFileFormat is null) + { + if (options?.MediaType?.Equals("image/png", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Png; + } + else if (options?.MediaType?.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Jpeg; + } + else if (options?.MediaType?.Equals("image/webp", StringComparison.OrdinalIgnoreCase) == true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Webp; + } + } + + result.ResponseFormat ??= options?.ResponseFormat switch + { + ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, + ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, + + // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. + _ => (GeneratedImageFormat?)null + }; + + result.Size ??= ToOpenAIImageSize(options?.ImageSize); + + return result; + } + + /// Converts a to a . + private ImageEditOptions ToOpenAIImageEditOptions(ImageGenerationOptions? options) + { + ImageEditOptions result = options?.RawRepresentationFactory?.Invoke(this) as ImageEditOptions ?? new(); + + result.ResponseFormat ??= options?.ResponseFormat switch + { + ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, + ImageGenerationResponseFormat.Data => GeneratedImageFormat.Bytes, + + // ImageGenerationResponseFormat.Hosted not supported by ImageGenerator, however other OpenAI API support file IDs. + _ => (GeneratedImageFormat?)null + }; + + result.Size ??= ToOpenAIImageSize(options?.ImageSize); + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs new file mode 100644 index 00000000000..b9e698a33f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGenerator.cs @@ -0,0 +1,53 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Represents a delegating image generator that configures a instance used by the remainder of the pipeline. +[Experimental("MEAI001")] +public sealed class ConfigureOptionsImageGenerator : DelegatingImageGenerator +{ + /// The callback delegate used to configure options. + private readonly Action _configureOptions; + + /// Initializes a new instance of the class with the specified callback. + /// The inner generator. + /// + /// The delegate to invoke to configure the instance. It is passed a clone of the caller-supplied instance + /// (or a newly constructed instance if the caller-supplied instance is ). + /// + /// or is . + /// + /// The delegate is passed either a new instance of if + /// the caller didn't supply a instance, or a clone (via of the caller-supplied + /// instance if one was supplied. + /// + public ConfigureOptionsImageGenerator(IImageGenerator innerGenerator, Action configure) + : base(innerGenerator) + { + _configureOptions = Throw.IfNull(configure); + } + + /// + public override async Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return await base.GenerateAsync(request, Configure(options), cancellationToken); + } + + /// Creates and configures the to pass along to the inner generator. + private ImageGenerationOptions Configure(ImageGenerationOptions? options) + { + options = options?.Clone() ?? new(); + + _configureOptions(options); + + return options; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..337c80951ef --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1629 // Documentation text should end with a period + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class ConfigureOptionsImageGeneratorBuilderExtensions +{ + /// + /// Adds a callback that configures a to be passed to the next generator in the pipeline. + /// + /// The . + /// + /// The delegate to invoke to configure the instance. + /// It is passed a clone of the caller-supplied instance (or a newly constructed instance if the caller-supplied instance is ). + /// + /// or is . + /// + /// This method can be used to set default options. The delegate is passed either a new instance of + /// if the caller didn't supply a instance, or a clone (via ) + /// of the caller-supplied instance if one was supplied. + /// + /// The . + public static ImageGeneratorBuilder ConfigureOptions( + this ImageGeneratorBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.Use(innerGenerator => new ConfigureOptionsImageGenerator(innerGenerator, configure)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs new file mode 100644 index 00000000000..9070ed8a59c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilder.cs @@ -0,0 +1,85 @@ +// 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 Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// A builder for creating pipelines of . +[Experimental("MEAI001")] +public sealed class ImageGeneratorBuilder +{ + private readonly Func _innerGeneratorFactory; + + /// The registered generator factory instances. + private List>? _generatorFactories; + + /// Initializes a new instance of the class. + /// The inner that represents the underlying backend. + /// is . + public ImageGeneratorBuilder(IImageGenerator innerGenerator) + { + _ = Throw.IfNull(innerGenerator); + _innerGeneratorFactory = _ => innerGenerator; + } + + /// Initializes a new instance of the class. + /// A callback that produces the inner that represents the underlying backend. + /// is . + public ImageGeneratorBuilder(Func innerGeneratorFactory) + { + _innerGeneratorFactory = Throw.IfNull(innerGeneratorFactory); + } + + /// Builds an that represents the entire pipeline. Calls to this instance will pass through each of the pipeline stages in turn. + /// + /// The that should provide services to the instances. + /// If null, an empty will be used. + /// + /// An instance of that represents the entire pipeline. + public IImageGenerator Build(IServiceProvider? services = null) + { + services ??= EmptyServiceProvider.Instance; + var imageGenerator = _innerGeneratorFactory(services); + + // To match intuitive expectations, apply the factories in reverse order, so that the first factory added is the outermost. + if (_generatorFactories is not null) + { + for (var i = _generatorFactories.Count - 1; i >= 0; i--) + { + imageGenerator = _generatorFactories[i](imageGenerator, services) ?? + throw new InvalidOperationException( + $"The {nameof(ImageGeneratorBuilder)} entry at index {i} returned null. " + + $"Ensure that the callbacks passed to {nameof(Use)} return non-null {nameof(IImageGenerator)} instances."); + } + } + + return imageGenerator; + } + + /// Adds a factory for an intermediate image generator to the image generator pipeline. + /// The generator factory function. + /// The updated instance. + /// is . + public ImageGeneratorBuilder Use(Func generatorFactory) + { + _ = Throw.IfNull(generatorFactory); + + return Use((innerGenerator, _) => generatorFactory(innerGenerator)); + } + + /// Adds a factory for an intermediate image generator to the image generator pipeline. + /// The generator factory function. + /// The updated instance. + /// is . + public ImageGeneratorBuilder Use(Func generatorFactory) + { + _ = Throw.IfNull(generatorFactory); + + (_generatorFactories ??= []).Add(generatorFactory); + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs new file mode 100644 index 00000000000..e8242287b68 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderImageGeneratorExtensions.cs @@ -0,0 +1,28 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extension methods for working with in the context of . +[Experimental("MEAI001")] +public static class ImageGeneratorBuilderImageGeneratorExtensions +{ + /// Creates a new using as its inner generator. + /// The generator to use as the inner generator. + /// The new instance. + /// is . + /// + /// This method is equivalent to using the constructor directly, + /// specifying as the inner generator. + /// + public static ImageGeneratorBuilder AsBuilder(this IImageGenerator innerGenerator) + { + _ = Throw.IfNull(innerGenerator); + + return new ImageGeneratorBuilder(innerGenerator); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs new file mode 100644 index 00000000000..cec943da309 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs @@ -0,0 +1,85 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// Provides extension methods for registering with a . +[Experimental("MEAI001")] +public static class ImageGeneratorBuilderServiceCollectionExtensions +{ + /// Registers a singleton in the . + /// The to which the generator should be added. + /// The inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// or is . + /// The generator is registered as a singleton service. + public static ImageGeneratorBuilder AddImageGenerator( + this IServiceCollection serviceCollection, + IImageGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddImageGenerator(serviceCollection, _ => innerGenerator, lifetime); + + /// Registers a singleton in the . + /// The to which the generator should be added. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// or is . + /// The generator is registered as a singleton service. + public static ImageGeneratorBuilder AddImageGenerator( + this IServiceCollection serviceCollection, + Func innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(innerGeneratorFactory); + + var builder = new ImageGeneratorBuilder(innerGeneratorFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(IImageGenerator), builder.Build, lifetime)); + return builder; + } + + /// Registers a keyed singleton in the . + /// The to which the generator should be added. + /// The key with which to associate the generator. + /// The inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// , , or is . + /// The generator is registered as a scoped service. + public static ImageGeneratorBuilder AddKeyedImageGenerator( + this IServiceCollection serviceCollection, + object serviceKey, + IImageGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddKeyedImageGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime); + + /// Registers a keyed singleton in the . + /// The to which the generator should be added. + /// The key with which to associate the generator. + /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the generator. Defaults to . + /// A that can be used to build a pipeline around the inner generator. + /// , , or is . + /// The generator is registered as a scoped service. + public static ImageGeneratorBuilder AddKeyedImageGenerator( + this IServiceCollection serviceCollection, + object serviceKey, + Func innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNull(serviceKey); + _ = Throw.IfNull(innerGeneratorFactory); + + var builder = new ImageGeneratorBuilder(innerGeneratorFactory); + serviceCollection.Add(new ServiceDescriptor(typeof(IImageGenerator), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime)); + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs new file mode 100644 index 00000000000..2eee33456c1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs @@ -0,0 +1,124 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using static Microsoft.Extensions.AI.OpenTelemetryConsts.GenAI; + +namespace Microsoft.Extensions.AI; + +/// A delegating image generator that logs image generation operations to an . +/// +/// +/// The provided implementation of is thread-safe for concurrent use so long as the +/// employed is also thread-safe for concurrent use. +/// +/// +/// When the employed enables , the contents of +/// prompts and options are logged. These prompts and options may contain sensitive application data. +/// is disabled by default and should never be enabled in a production environment. +/// Prompts and options are not logged at other logging levels. +/// +/// +[Experimental("MEAI001")] +public partial class LoggingImageGenerator : DelegatingImageGenerator +{ + /// An instance used for all logging. + private readonly ILogger _logger; + + /// The to use for serialization of state written to the logger. + private JsonSerializerOptions _jsonSerializerOptions; + + /// Initializes a new instance of the class. + /// The underlying . + /// An instance that will be used for all logging. + /// or is . + public LoggingImageGenerator(IImageGenerator innerGenerator, ILogger logger) + : base(innerGenerator) + { + _logger = Throw.IfNull(logger); + _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; + } + + /// Gets or sets JSON serialization options to use when serializing logging data. + /// The value being set is . + public JsonSerializerOptions JsonSerializerOptions + { + get => _jsonSerializerOptions; + set => _jsonSerializerOptions = Throw.IfNull(value); + } + + /// + public override async Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogInvokedSensitive(nameof(GenerateAsync), request.Prompt ?? string.Empty, AsJson(options), AsJson(this.GetService())); + } + else + { + LogInvoked(nameof(GenerateAsync)); + } + } + + try + { + var response = await base.GenerateAsync(request, options, cancellationToken); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + if (_logger.IsEnabled(LogLevel.Trace) && response.Contents.All(c => c is not DataContent)) + { + LogCompletedSensitive(nameof(GenerateAsync), AsJson(response)); + } + else + { + LogCompleted(nameof(GenerateAsync)); + } + } + + return response; + } + catch (OperationCanceledException) + { + LogInvocationCanceled(nameof(GenerateAsync)); + throw; + } + catch (Exception ex) + { + LogInvocationFailed(nameof(GenerateAsync), ex); + throw; + } + } + + private string AsJson(T value) => LoggingHelpers.AsJson(value, _jsonSerializerOptions); + + [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] + private partial void LogInvoked(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: Prompt: {Prompt}. Options: {ImageGenerationOptions}. Metadata: {ImageGeneratorMetadata}.")] + private partial void LogInvokedSensitive(string methodName, string prompt, string imageGenerationOptions, string imageGeneratorMetadata); + + [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] + private partial void LogCompleted(string methodName); + + [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {ImageGenerationResponse}.")] + private partial void LogCompletedSensitive(string methodName, string imageGenerationResponse); + + [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] + private partial void LogInvocationCanceled(string methodName); + + [LoggerMessage(LogLevel.Error, "{MethodName} failed.")] + private partial void LogInvocationFailed(string methodName, Exception error); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..ece65d942ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGeneratorBuilderExtensions.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class LoggingImageGeneratorBuilderExtensions +{ + /// Adds logging to the image generator pipeline. + /// The . + /// + /// An optional used to create a logger with which logging should be performed. + /// If not supplied, a required instance will be resolved from the service provider. + /// + /// An optional callback that can be used to configure the instance. + /// The . + /// is . + /// + /// + /// When the employed enables , the contents of + /// prompts and options are logged. These prompts and options may contain sensitive application data. + /// is disabled by default and should never be enabled in a production environment. + /// Prompts and options are not logged at other logging levels. + /// + /// + public static ImageGeneratorBuilder UseLogging( + this ImageGeneratorBuilder builder, + ILoggerFactory? loggerFactory = null, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.Use((innerGenerator, services) => + { + loggerFactory ??= services.GetRequiredService(); + + // If the factory we resolve is for the null logger, the LoggingImageGenerator will end up + // being an expensive nop, so skip adding it and just return the inner generator. + if (loggerFactory == NullLoggerFactory.Instance) + { + return innerGenerator; + } + + var imageGenerator = new LoggingImageGenerator(innerGenerator, loggerFactory.CreateLogger(typeof(LoggingImageGenerator))); + configure?.Invoke(imageGenerator); + return imageGenerator; + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs new file mode 100644 index 00000000000..7e8d189b851 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/DelegatingImageGeneratorTests.cs @@ -0,0 +1,142 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class DelegatingImageGeneratorTests +{ + [Fact] + public void RequiresInnerImageGenerator() + { + Assert.Throws("innerGenerator", () => new NoOpDelegatingImageGenerator(null!)); + } + + [Fact] + public async Task GenerateImagesAsyncDefaultsToInnerGeneratorAsync() + { + // Arrange + var expectedRequest = new ImageGenerationRequest("test prompt"); + var expectedOptions = new ImageGenerationOptions(); + var expectedCancellationToken = CancellationToken.None; + var expectedResult = new TaskCompletionSource(); + var expectedResponse = new ImageGenerationResponse(); + using var inner = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(expectedOptions, options); + Assert.Equal(expectedCancellationToken, cancellationToken); + return expectedResult.Task; + } + }; + + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var resultTask = delegating.GenerateAsync(expectedRequest, expectedOptions, expectedCancellationToken); + + // Assert + Assert.False(resultTask.IsCompleted); + expectedResult.SetResult(expectedResponse); + Assert.True(resultTask.IsCompleted); + Assert.Same(expectedResponse, await resultTask); + } + + [Fact] + public void GetServiceThrowsForNullType() + { + using var inner = new TestImageGenerator(); + using var delegating = new NoOpDelegatingImageGenerator(inner); + Assert.Throws("serviceType", () => delegating.GetService(null!)); + } + + [Fact] + public void GetServiceReturnsSelfIfCompatibleWithRequestAndKeyIsNull() + { + // Arrange + using var inner = new TestImageGenerator(); + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var generator = delegating.GetService(); + + // Assert + Assert.Same(delegating, generator); + } + + [Fact] + public void GetServiceDelegatesToInnerIfKeyIsNotNull() + { + // Arrange + var expectedKey = new object(); + using var expectedResult = new TestImageGenerator(); + using var inner = new TestImageGenerator + { + GetServiceCallback = (_, _) => expectedResult + }; + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var generator = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, generator); + } + + [Fact] + public void GetServiceDelegatesToInnerIfNotCompatibleWithRequest() + { + // Arrange + var expectedResult = TimeZoneInfo.Local; + var expectedKey = new object(); + using var inner = new TestImageGenerator + { + GetServiceCallback = (type, key) => type == expectedResult.GetType() && key == expectedKey + ? expectedResult + : throw new InvalidOperationException("Unexpected call") + }; + using var delegating = new NoOpDelegatingImageGenerator(inner); + + // Act + var tzi = delegating.GetService(expectedKey); + + // Assert + Assert.Same(expectedResult, tzi); + } + + [Fact] + public void Dispose_SetsFlag() + { + using var inner = new TestImageGenerator(); + var delegating = new NoOpDelegatingImageGenerator(inner); + Assert.False(inner.DisposeInvoked); + + delegating.Dispose(); + Assert.True(inner.DisposeInvoked); + } + + [Fact] + public void Dispose_MultipleCallsSafe() + { + using var inner = new TestImageGenerator(); + var delegating = new NoOpDelegatingImageGenerator(inner); + + delegating.Dispose(); + Assert.True(inner.DisposeInvoked); + + // Second dispose should not throw +#pragma warning disable S3966 + delegating.Dispose(); +#pragma warning restore S3966 + Assert.True(inner.DisposeInvoked); + } + + private sealed class NoOpDelegatingImageGenerator(IImageGenerator innerGenerator) + : DelegatingImageGenerator(innerGenerator); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs new file mode 100644 index 00000000000..f6cd167e82a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs @@ -0,0 +1,135 @@ +// 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.Drawing; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGenerationOptionsTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + ImageGenerationOptions options = new(); + Assert.Null(options.ResponseFormat); + Assert.Null(options.Count); + Assert.Null(options.ImageSize); + Assert.Null(options.MediaType); + Assert.Null(options.ModelId); + Assert.Null(options.RawRepresentationFactory); + + ImageGenerationOptions clone = options.Clone(); + Assert.Null(clone.ResponseFormat); + Assert.Null(clone.Count); + Assert.Null(clone.ImageSize); + Assert.Null(clone.MediaType); + Assert.Null(clone.ModelId); + Assert.Null(clone.RawRepresentationFactory); + } + + [Fact] + public void Properties_Roundtrip() + { + ImageGenerationOptions options = new(); + + Func factory = generator => new { Representation = "raw data" }; + + options.ResponseFormat = ImageGenerationResponseFormat.Data; + options.Count = 5; + options.ImageSize = new Size(1024, 768); + options.MediaType = "image/png"; + options.ModelId = "modelId"; + options.RawRepresentationFactory = factory; + + Assert.Equal(ImageGenerationResponseFormat.Data, options.ResponseFormat); + Assert.Equal(5, options.Count); + Assert.Equal(new Size(1024, 768), options.ImageSize); + Assert.Equal("image/png", options.MediaType); + Assert.Equal("modelId", options.ModelId); + Assert.Same(factory, options.RawRepresentationFactory); + + ImageGenerationOptions clone = options.Clone(); + Assert.Equal(ImageGenerationResponseFormat.Data, clone.ResponseFormat); + Assert.Equal(5, clone.Count); + Assert.Equal(new Size(1024, 768), clone.ImageSize); + Assert.Equal("image/png", clone.MediaType); + Assert.Equal("modelId", clone.ModelId); + Assert.Same(factory, clone.RawRepresentationFactory); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + ImageGenerationOptions options = new() + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 3, + ImageSize = new Size(256, 256), + MediaType = "image/jpeg", + ModelId = "test-model", + }; + + string json = JsonSerializer.Serialize(options, TestJsonSerializerContext.Default.ImageGenerationOptions); + + ImageGenerationOptions? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationOptions); + Assert.NotNull(deserialized); + + Assert.Equal(ImageGenerationResponseFormat.Data, deserialized.ResponseFormat); + Assert.Equal(3, deserialized.Count); + Assert.Equal(new Size(256, 256), deserialized.ImageSize); + Assert.Equal("image/jpeg", deserialized.MediaType); + Assert.Equal("test-model", deserialized.ModelId); + } + + [Fact] + public void Clone_CreatesIndependentCopy() + { + ImageGenerationOptions original = new() + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 2, + ImageSize = new Size(512, 512), + MediaType = "image/png", + ModelId = "original-model" + }; + + ImageGenerationOptions clone = original.Clone(); + + // Modify original + original.ResponseFormat = ImageGenerationResponseFormat.Uri; + original.Count = 1; + original.ImageSize = new Size(1024, 1024); + original.MediaType = "image/jpeg"; + original.ModelId = "modified-model"; + + // Clone should remain unchanged + Assert.Equal(ImageGenerationResponseFormat.Data, clone.ResponseFormat); + Assert.Equal(2, clone.Count); + Assert.Equal(new Size(512, 512), clone.ImageSize); + Assert.Equal("image/png", clone.MediaType); + Assert.Equal("original-model", clone.ModelId); + } + + [Theory] + [InlineData(ImageGenerationResponseFormat.Uri)] + [InlineData(ImageGenerationResponseFormat.Data)] + [InlineData(ImageGenerationResponseFormat.Hosted)] + public void ImageGenerationResponseFormat_Values_AreValid(ImageGenerationResponseFormat responseFormat) + { + Assert.True(Enum.IsDefined(typeof(ImageGenerationResponseFormat), responseFormat)); + } + + [Fact] + public void ImageGenerationResponseFormat_JsonSerialization_Roundtrips() + { + foreach (ImageGenerationResponseFormat responseFormat in Enum.GetValues(typeof(ImageGenerationResponseFormat))) + { + string json = JsonSerializer.Serialize(responseFormat, TestJsonSerializerContext.Default.ImageGenerationResponseFormat); + ImageGenerationResponseFormat deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponseFormat); + Assert.Equal(responseFormat, deserialized); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs new file mode 100644 index 00000000000..7b244dfeb53 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationResponseTests.cs @@ -0,0 +1,154 @@ +// 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 ImageGenerationResponseTests +{ + [Fact] + public void Constructor_Parameterless_PropsDefaulted() + { + ImageGenerationResponse response = new(); + Assert.Empty(response.Contents); + Assert.NotNull(response.Contents); + Assert.Same(response.Contents, response.Contents); + Assert.Empty(response.Contents); + Assert.Null(response.RawRepresentation); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Constructor_List_PropsRoundtrip(int contentCount) + { + List content = []; + for (int i = 0; i < contentCount; i++) + { + content.Add(new UriContent(new Uri($"https://example.com/image-{i}.png"), "image/png")); + } + + ImageGenerationResponse response = new(content); + + Assert.Same(response.Contents, response.Contents); + if (contentCount == 0) + { + Assert.Empty(response.Contents); + } + else + { + Assert.Equal(contentCount, response.Contents.Count); + for (int i = 0; i < contentCount; i++) + { + UriContent uc = Assert.IsType(response.Contents[i]); + Assert.Equal($"https://example.com/image-{i}.png", uc.Uri.ToString()); + Assert.Equal("image/png", uc.MediaType); + } + } + } + + [Fact] + public void Contents_SetNull_ReturnsEmpty() + { + ImageGenerationResponse response = new() + { + Contents = null! + }; + Assert.NotNull(response.Contents); + Assert.Empty(response.Contents); + } + + [Fact] + public void Contents_Set_Roundtrips() + { + ImageGenerationResponse response = new(); + byte[] imageData = [1, 2, 3, 4]; + + List contents = [ + new UriContent(new Uri("https://example.com/image1.png"), "image/png"), + new DataContent(imageData, "image/jpeg") + ]; + + response.Contents = contents; + Assert.Same(contents, response.Contents); + } + + [Fact] + public void RawRepresentation_Roundtrips() + { + ImageGenerationResponse response = new(); + Assert.Null(response.RawRepresentation); + + object representation = new { test = "value" }; + response.RawRepresentation = representation; + Assert.Same(representation, response.RawRepresentation); + + response.RawRepresentation = null; + Assert.Null(response.RawRepresentation); + } + + [Fact] + public void JsonSerialization_Roundtrips() + { + List contents = [ + new UriContent(new Uri("https://example.com/image1.png"), "image/png"), + new DataContent((byte[])[1, 2, 3, 4], "image/jpeg") + ]; + + ImageGenerationResponse response = new(contents); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + + Assert.Equal(2, deserialized.Contents.Count); + + UriContent uriContent = Assert.IsType(deserialized.Contents[0]); + Assert.Equal("https://example.com/image1.png", uriContent.Uri.ToString()); + Assert.Equal("image/png", uriContent.MediaType); + + DataContent dataContent = Assert.IsType(deserialized.Contents[1]); + Assert.Equal([1, 2, 3, 4], dataContent.Data.ToArray()); + Assert.Equal("image/jpeg", dataContent.MediaType); + } + + [Fact] + public void JsonSerialization_Empty_Roundtrips() + { + ImageGenerationResponse response = new(); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + Assert.Empty(deserialized.Contents); + } + + [Fact] + public void JsonSerialization_WithVariousContentTypes_Roundtrips() + { + List contents = [ + new UriContent(new Uri("https://example.com/image.png"), "image/png"), + new DataContent((byte[])[255, 216, 255, 224], "image/jpeg"), + new TextContent("Generated image description") // Edge case: text content in image response + ]; + + ImageGenerationResponse response = new(contents); + + string json = JsonSerializer.Serialize(response, TestJsonSerializerContext.Default.ImageGenerationResponse); + + ImageGenerationResponse? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ImageGenerationResponse); + Assert.NotNull(deserialized); + Assert.Equal(3, deserialized.Contents.Count); + + Assert.IsType(deserialized.Contents[0]); + Assert.IsType(deserialized.Contents[1]); + Assert.IsType(deserialized.Contents[2]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs new file mode 100644 index 00000000000..a68726685eb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorExtensionsTests.cs @@ -0,0 +1,222 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorExtensionsTests +{ + [Fact] + public void GetService_InvalidArgs_Throws() + { + Assert.Throws("generator", () => + { + _ = ImageGeneratorExtensions.GetService(null!); + }); + } + + [Fact] + public void GetService_ValidGenerator_CallsUnderlyingGetService() + { + using var testGenerator = new TestImageGenerator(); + var expectedResult = new object(); + var expectedServiceKey = new object(); + + testGenerator.GetServiceCallback = (serviceType, serviceKey) => + { + Assert.Equal(typeof(object), serviceType); + Assert.Same(expectedServiceKey, serviceKey); + return expectedResult; + }; + + var result = testGenerator.GetService(expectedServiceKey); + Assert.Same(expectedResult, result); + } + + [Fact] + public void GetService_ReturnsCorrectType() + { + using var testGenerator = new TestImageGenerator(); + var metadata = new ImageGeneratorMetadata("test", null, "model"); + + testGenerator.GetServiceCallback = (serviceType, serviceKey) => + { + return (serviceType == typeof(ImageGeneratorMetadata)) ? metadata : null; + }; + + var result = testGenerator.GetService(); + Assert.Same(metadata, result); + + var nullResult = testGenerator.GetService(); + Assert.Null(nullResult); + } + + [Fact] + public async Task EditImageAsync_DataContent_CallsGenerateImagesAsync() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var dataContent = new DataContent(imageData, "image/png") { Name = "test.png" }; + var prompt = "Edit this image"; + var options = new ImageGenerationOptions { Count = 2 }; + var expectedResponse = new ImageGenerationResponse(); + var cancellationToken = new CancellationToken(canceled: false); + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + Assert.Single(request.OriginalImages); + Assert.Same(dataContent, Assert.Single(request.OriginalImages)); + Assert.Equal(prompt, request.Prompt); + Assert.Same(options, o); + Assert.Equal(cancellationToken, ct); + return Task.FromResult(expectedResponse); + }; + + // Act + var result = await testGenerator.EditImageAsync(dataContent, prompt, options, cancellationToken); + + // Assert + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task EditImageAsync_DataContent_NullArguments_Throws() + { + using var testGenerator = new TestImageGenerator(); + var dataContent = new DataContent(new byte[] { 1, 2, 3 }, "image/png"); + + await Assert.ThrowsAsync("generator", async () => + await ImageGeneratorExtensions.EditImageAsync(null!, dataContent, "prompt")); + + await Assert.ThrowsAsync("originalImage", async () => + await testGenerator.EditImageAsync(null!, "prompt")); + + await Assert.ThrowsAsync("prompt", async () => + await testGenerator.EditImageAsync(dataContent, null!)); + } + + [Fact] + public async Task EditImageAsync_ByteArray_CallsGenerateImagesAsync() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var fileName = "test.jpg"; + var prompt = "Edit this image"; + var options = new ImageGenerationOptions { Count = 2 }; + var expectedResponse = new ImageGenerationResponse(); + var cancellationToken = new CancellationToken(canceled: false); + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + Assert.Single(request.OriginalImages); + var dataContent = Assert.IsType(Assert.Single(request.OriginalImages)); + Assert.Equal(imageData, dataContent.Data.ToArray()); + Assert.Equal("image/jpeg", dataContent.MediaType); + Assert.Equal(fileName, dataContent.Name); + Assert.Equal(prompt, request.Prompt); + Assert.Same(options, o); + Assert.Equal(cancellationToken, ct); + return Task.FromResult(expectedResponse); + }; + + // Act + var result = await testGenerator.EditImageAsync(imageData, fileName, prompt, options, cancellationToken); + + // Assert + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullGenerator_Throws() + { + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("generator", async () => + await ImageGeneratorExtensions.EditImageAsync(null!, imageData, "test.png", "prompt")); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullFileName_Throws() + { + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("fileName", async () => + await testGenerator.EditImageAsync(imageData, null!, "prompt")); + } + + [Fact] + public async Task EditImageAsync_ByteArray_NullPrompt_Throws() + { + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync("prompt", async () => + await testGenerator.EditImageAsync(imageData, "test.png", null!)); + } + + [Theory] + [InlineData("test.png", "image/png")] + [InlineData("test.jpg", "image/jpeg")] + [InlineData("test.jpeg", "image/jpeg")] + [InlineData("test.webp", "image/webp")] + [InlineData("test.gif", "image/gif")] + [InlineData("test.bmp", "image/bmp")] + [InlineData("test.tiff", "image/tiff")] + [InlineData("test.tif", "image/tiff")] + [InlineData("test.unknown", "image/png")] // Unknown extension defaults to PNG + [InlineData("TEST.PNG", "image/png")] // Case insensitive + public async Task EditImageAsync_ByteArray_InfersCorrectMediaType(string fileName, string expectedMediaType) + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var prompt = "Edit this image"; + + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + Assert.NotNull(request.OriginalImages); + var dataContent = Assert.IsType(Assert.Single(request.OriginalImages)); + Assert.Equal(expectedMediaType, dataContent.MediaType); + return Task.FromResult(new ImageGenerationResponse()); + }; + + // Act & Assert + await testGenerator.EditImageAsync(imageData, fileName, prompt); + } + + [Fact] + public async Task EditImageAsync_AllMethods_PassDefaultOptionsAndCancellation() + { + // Arrange + using var testGenerator = new TestImageGenerator(); + var imageData = new byte[] { 1, 2, 3, 4 }; + var dataContent = new DataContent(imageData, "image/png"); + var prompt = "Edit this image"; + + int callCount = 0; + testGenerator.GenerateImagesAsyncCallback = (request, o, ct) => + { + callCount++; + Assert.Null(o); // Default options should be null + Assert.Equal(CancellationToken.None, ct); // Default cancellation token + Assert.NotNull(request.OriginalImages); // Should have original images for editing + return Task.FromResult(new ImageGenerationResponse()); + }; + + // Act - Test all two overloads with default parameters + await testGenerator.EditImageAsync(dataContent, prompt); + await testGenerator.EditImageAsync(imageData, "test.png", prompt); + + // Assert + Assert.Equal(2, callCount); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs new file mode 100644 index 00000000000..193a02bde3e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorMetadataTests.cs @@ -0,0 +1,29 @@ +// 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 ImageGeneratorMetadataTests +{ + [Fact] + public void Constructor_NullValues_AllowedAndRoundtrip() + { + ImageGeneratorMetadata metadata = new(null, null, null); + Assert.Null(metadata.ProviderName); + Assert.Null(metadata.ProviderUri); + Assert.Null(metadata.DefaultModelId); + } + + [Fact] + public void Constructor_Value_Roundtrips() + { + var uri = new Uri("https://example.com"); + ImageGeneratorMetadata metadata = new("providerName", uri, "theModel"); + Assert.Equal("providerName", metadata.ProviderName); + Assert.Same(uri, metadata.ProviderUri); + Assert.Equal("theModel", metadata.DefaultModelId); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs new file mode 100644 index 00000000000..2f60d3bb052 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGeneratorTests.cs @@ -0,0 +1,155 @@ +// 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.Drawing; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorTests +{ + [Fact] + public void GetService_WithServiceKey_ReturnsNull() + { + using var generator = new TestImageGenerator(); + generator.GetServiceCallback = (serviceType, serviceKey) => + { + // When serviceKey is not null, should return null per interface contract + return serviceKey is not null ? null : new object(); + }; + + var result = generator.GetService(typeof(object), "someKey"); + Assert.Null(result); + } + + [Fact] + public void GetService_WithoutServiceKey_CallsCallback() + { + using var generator = new TestImageGenerator(); + var expectedResult = new object(); + + generator.GetServiceCallback = (serviceType, serviceKey) => + { + Assert.Equal(typeof(object), serviceType); + Assert.Null(serviceKey); + return expectedResult; + }; + + var result = generator.GetService(typeof(object)); + Assert.Same(expectedResult, result); + } + + [Fact] + public async Task GenerateImagesAsync_CallsCallback() + { + var expectedResponse = new ImageGenerationResponse(); + var expectedOptions = new ImageGenerationOptions(); + using var cts = new CancellationTokenSource(); + var expectedRequest = new ImageGenerationRequest("test prompt"); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(expectedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedResponse); + } + }; + + var result = await generator.GenerateAsync(expectedRequest, expectedOptions, cts.Token); + Assert.Same(expectedResponse, result); + } + + [Fact] + public async Task GenerateImagesAsync_NoCallback_ReturnsEmptyResponse() + { + using var generator = new TestImageGenerator(); + var result = await generator.GenerateAsync(new ImageGenerationRequest("test prompt"), null); + Assert.NotNull(result); + Assert.Empty(result.Contents); + } + + [Fact] + public void Dispose_SetsFlag() + { + var generator = new TestImageGenerator(); + Assert.False(generator.DisposeInvoked); + + generator.Dispose(); + Assert.True(generator.DisposeInvoked); + } + + [Fact] + public void Dispose_MultipleCallsSafe() + { + var generator = new TestImageGenerator(); + + generator.Dispose(); + Assert.True(generator.DisposeInvoked); + + // Second dispose should not throw +#pragma warning disable S3966 + generator.Dispose(); +#pragma warning restore S3966 + Assert.True(generator.DisposeInvoked); + } + + [Fact] + public async Task GenerateImagesAsync_WithOptions_PassesThroughCorrectly() + { + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Data, + Count = 3, + ImageSize = new Size(1024, 768), + MediaType = "image/png", + ModelId = "test-model", + }; + + var expectedRequest = new ImageGenerationRequest("test prompt"); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, receivedOptions, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(options, receivedOptions); + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + await generator.GenerateAsync(expectedRequest, options); + } + + [Fact] + public async Task GenerateImagesAsync_WithEditRequest_PassesThroughCorrectly() + { + var options = new ImageGenerationOptions + { + ResponseFormat = ImageGenerationResponseFormat.Uri, + Count = 2, + MediaType = "image/jpeg", + ModelId = "edit-model", + }; + + AIContent[] originalImages = [new DataContent((byte[])[1, 2, 3, 4], "image/png")]; + var expectedRequest = new ImageGenerationRequest("edit prompt", originalImages); + + using var generator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, receivedOptions, cancellationToken) => + { + Assert.Same(expectedRequest, request); + Assert.Same(options, receivedOptions); + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + await generator.GenerateAsync(expectedRequest, options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs new file mode 100644 index 00000000000..4db1cca7377 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestImageGenerator.cs @@ -0,0 +1,43 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +public sealed class TestImageGenerator : IImageGenerator +{ + public TestImageGenerator() + { + GetServiceCallback = DefaultGetServiceCallback; + } + + public IServiceProvider? Services { get; set; } + + public Func>? GenerateImagesAsyncCallback { get; set; } + + public Func GetServiceCallback { get; set; } + + public bool DisposeInvoked { get; private set; } + + private object? DefaultGetServiceCallback(Type serviceType, object? serviceKey) + => serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; + + public Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return GenerateImagesAsyncCallback?.Invoke(request, options, cancellationToken) ?? + Task.FromResult(new ImageGenerationResponse()); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return GetServiceCallback.Invoke(serviceType, serviceKey); + } + + public void Dispose() + { + DisposeInvoked = true; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index d15f0a19fa9..609dac264eb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -20,6 +20,8 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(SpeechToTextResponseUpdate))] [JsonSerializable(typeof(SpeechToTextResponseUpdateKind))] [JsonSerializable(typeof(SpeechToTextOptions))] +[JsonSerializable(typeof(ImageGenerationResponse))] +[JsonSerializable(typeof(ImageGenerationOptions))] [JsonSerializable(typeof(ChatOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] [JsonSerializable(typeof(Dictionary))] diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs new file mode 100644 index 00000000000..8e3078efbb5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs @@ -0,0 +1,125 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.TestUtilities; +using Xunit; + +#pragma warning disable CA2214 // Do not call overridable methods in constructors + +namespace Microsoft.Extensions.AI; + +public abstract class ImageGeneratorIntegrationTests : IDisposable +{ + private readonly IImageGenerator? _generator; + + protected ImageGeneratorIntegrationTests() + { + _generator = CreateGenerator(); + } + + public void Dispose() + { + _generator?.Dispose(); + GC.SuppressFinalize(this); + } + + protected abstract IImageGenerator? CreateGenerator(); + + [ConditionalFact] + public virtual async Task GenerateImagesAsync_SingleImageGeneration() + { + SkipIfNotEnabled(); + + var options = new ImageGenerationOptions + { + Count = 1 + }; + + var response = await _generator.GenerateImagesAsync("A simple drawing of a house", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); + + var content = response.Contents[0]; + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + + [ConditionalFact] + public virtual async Task GenerateImagesAsync_MultipleImages() + { + SkipIfNotEnabled(); + + var options = new ImageGenerationOptions + { + Count = 2 + }; + + var response = await _generator.GenerateImagesAsync("A cat sitting on a table", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Equal(2, response.Contents.Count); + + foreach (var content in response.Contents) + { + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + } + + [ConditionalFact] + public virtual async Task EditImagesAsync_SingleImage() + { + SkipIfNotEnabled(); + + var imageData = GetImageData("dotnet.png"); + AIContent[] originalImages = [new DataContent(imageData, "image/png") { Name = "dotnet.png" }]; + + var options = new ImageGenerationOptions + { + Count = 1 + }; + + var response = await _generator.EditImagesAsync(originalImages, "Add a red border and make the background tie-dye", options); + + Assert.NotNull(response); + Assert.NotEmpty(response.Contents); + Assert.Single(response.Contents); + + var content = response.Contents[0]; + Assert.IsType(content); + var dataContent = (DataContent)content; + Assert.False(dataContent.Data.IsEmpty); + Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + } + + private static byte[] GetImageData(string fileName) + { + using Stream? s = typeof(ImageGeneratorIntegrationTests).Assembly.GetManifestResourceStream($"Microsoft.Extensions.AI.Resources.{fileName}"); + Assert.NotNull(s); + using MemoryStream ms = new(); + s.CopyTo(ms); + return ms.ToArray(); + } + + [MemberNotNull(nameof(_generator))] + protected void SkipIfNotEnabled() + { + string? skipIntegration = TestRunnerConfiguration.Instance["SkipIntegrationTests"]; + + if (skipIntegration is not null || _generator is null) + { + throw new SkipTestException("Generator is not enabled."); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index ddc72caa90d..0b4865f577e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md index 3b99e9bccc1..988ab2d08f5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/README.md @@ -17,6 +17,7 @@ Optionally also run the following. The values shown here are the defaults if you ``` dotnet user-secrets set OpenAI:ChatModel gpt-4o-mini dotnet user-secrets set OpenAI:EmbeddingModel text-embedding-3-small +dotnet user-secrets set OpenAI:ImageModel dall-e-3 ``` ### Configuring OpenAI tests (Azure OpenAI) @@ -35,6 +36,7 @@ Optionally also run the following. The values shown here are the defaults if you ``` dotnet user-secrets set OpenAI:ChatModel gpt-4o-mini dotnet user-secrets set OpenAI:EmbeddingModel text-embedding-3-small +dotnet user-secrets set OpenAI:ImageModel dall-e-3 ``` Your account must have models matching these names. diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs new file mode 100644 index 00000000000..ce0cdb7cf82 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public class OpenAIImageGeneratorIntegrationTests : ImageGeneratorIntegrationTests +{ + protected override IImageGenerator? CreateGenerator() + => IntegrationTestHelpers.GetOpenAIClient()? + .GetImageClient(TestRunnerConfiguration.Instance["OpenAI:ImageModel"] ?? "dall-e-3") + .AsIImageGenerator(); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs new file mode 100644 index 00000000000..607b1e2859e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorTests.cs @@ -0,0 +1,45 @@ +// 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.ClientModel; +using OpenAI; +using OpenAI.Images; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenAIImageGeneratorTests +{ + [Fact] + public void AsIImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("imageClient", () => ((ImageClient)null!).AsIImageGenerator()); + } + + [Fact] + public void AsIImageGenerator_OpenAIClient_ProducesExpectedMetadata() + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "dall-e-3"; + + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + + IImageGenerator imageClient = client.GetImageClient(model).AsIImageGenerator(); + var metadata = imageClient.GetService(); + Assert.Equal(endpoint, metadata?.ProviderUri); + Assert.Equal(model, metadata?.DefaultModelId); + } + + [Fact] + public void GetService_ReturnsExpectedServices() + { + var client = new OpenAIClient(new ApiKeyCredential("key")); + IImageGenerator imageClient = client.GetImageClient("dall-e-3").AsIImageGenerator(); + + Assert.Same(imageClient, imageClient.GetService()); + Assert.Same(imageClient, imageClient.GetService()); + Assert.NotNull(imageClient.GetService()); + Assert.NotNull(imageClient.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs new file mode 100644 index 00000000000..ba37cad1b54 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ConfigureOptionsImageGeneratorTests.cs @@ -0,0 +1,70 @@ +// 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.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ConfigureOptionsImageGeneratorTests +{ + [Fact] + public void ConfigureOptionsImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new ConfigureOptionsImageGenerator(null!, _ => { })); + Assert.Throws("configure", () => new ConfigureOptionsImageGenerator(new TestImageGenerator(), null!)); + } + + [Fact] + public void ConfigureOptions_InvalidArgs_Throws() + { + using var innerGenerator = new TestImageGenerator(); + var builder = innerGenerator.AsBuilder(); + Assert.Throws("configure", () => builder.ConfigureOptions(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ConfigureOptions_ReturnedInstancePassedToNextGenerator(bool nullProvidedOptions) + { + ImageGenerationOptions? providedOptions = nullProvidedOptions ? null : new() { ModelId = "test" }; + ImageGenerationOptions? returnedOptions = null; + ImageGenerationResponse expectedResponse = new([]); + using CancellationTokenSource cts = new(); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (prompt, options, cancellationToken) => + { + Assert.Same(returnedOptions, options); + Assert.Equal(cts.Token, cancellationToken); + return Task.FromResult(expectedResponse); + }, + + }; + + using var generator = innerGenerator + .AsBuilder() + .ConfigureOptions(options => + { + Assert.NotSame(providedOptions, options); + if (nullProvidedOptions) + { + Assert.Null(options.ModelId); + } + else + { + Assert.Equal(providedOptions!.ModelId, options.ModelId); + } + + returnedOptions = options; + }) + .Build(); + + var response1 = await generator.GenerateImagesAsync("test prompt", providedOptions, cts.Token); + Assert.Same(expectedResponse, response1); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs new file mode 100644 index 00000000000..b65495e506b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs @@ -0,0 +1,162 @@ +// 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.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorDependencyInjectionPatterns +{ + private IServiceCollection ServiceCollection { get; } = new ServiceCollection(); + + [Fact] + public void CanRegisterSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddImageGenerator(services => new TestImageGenerator { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestImageGenerator(); + ServiceCollection.AddImageGenerator(singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + var instance1 = scope1.ServiceProvider.GetRequiredService(); + var instance1Copy = scope1.ServiceProvider.GetRequiredService(); + var instance2 = scope2.ServiceProvider.GetRequiredService(); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingFactory() + { + // Arrange/Act + ServiceCollection.AddKeyedImageGenerator("mykey", services => new TestImageGenerator { Services = services }) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Fact] + public void CanRegisterKeyedSingletonUsingSharedInstance() + { + // Arrange/Act + using var singleton = new TestImageGenerator(); + ServiceCollection.AddKeyedImageGenerator("mykey", singleton) + .UseSingletonMiddleware(); + + // Assert + var services = ServiceCollection.BuildServiceProvider(); + using var scope1 = services.CreateScope(); + using var scope2 = services.CreateScope(); + + Assert.Null(services.GetService()); + + var instance1 = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService("mykey"); + var instance2 = scope2.ServiceProvider.GetRequiredKeyedService("mykey"); + + // Each scope gets the same instance, because it's singleton + var instance = Assert.IsType(instance1); + Assert.Same(instance, instance1Copy); + Assert.Same(instance, instance2); + Assert.IsType(instance.InnerGenerator); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddImageGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ImageGeneratorBuilder builder = lifetime.HasValue + ? sc.AddImageGenerator(services => new TestImageGenerator(), lifetime.Value) + : sc.AddImageGenerator(services => new TestImageGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IImageGenerator), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddKeyedImageGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ImageGeneratorBuilder builder = lifetime.HasValue + ? sc.AddKeyedImageGenerator("key", services => new TestImageGenerator(), lifetime.Value) + : sc.AddKeyedImageGenerator("key", services => new TestImageGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IImageGenerator), sd.ServiceType); + Assert.True(sd.IsKeyedService); + Assert.Equal("key", sd.ServiceKey); + Assert.Null(sd.KeyedImplementationInstance); + Assert.NotNull(sd.KeyedImplementationFactory); + Assert.IsType(sd.KeyedImplementationFactory(null!, null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + public class SingletonMiddleware(IImageGenerator inner, IServiceProvider services) : DelegatingImageGenerator(inner) + { + public new IImageGenerator InnerGenerator => base.InnerGenerator; + public IServiceProvider Services => services; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs new file mode 100644 index 00000000000..819fcde88ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/LoggingImageGeneratorTests.cs @@ -0,0 +1,143 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class LoggingImageGeneratorTests +{ + [Fact] + public void LoggingImageGenerator_InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new LoggingImageGenerator(null!, NullLogger.Instance)); + Assert.Throws("logger", () => new LoggingImageGenerator(new TestImageGenerator(), null!)); + } + + [Fact] + public void UseLogging_AvoidsInjectingNopGenerator() + { + using var innerGenerator = new TestImageGenerator(); + + Assert.Null(innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(LoggingImageGenerator))); + Assert.Same(innerGenerator, innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build().GetService(typeof(IImageGenerator))); + + using var factory = LoggerFactory.Create(b => b.AddFakeLogging()); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging(factory).Build().GetService(typeof(LoggingImageGenerator))); + + ServiceCollection c = new(); + c.AddFakeLogging(); + var services = c.BuildServiceProvider(); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging().Build(services).GetService(typeof(LoggingImageGenerator))); + Assert.NotNull(innerGenerator.AsBuilder().UseLogging(null).Build(services).GetService(typeof(LoggingImageGenerator))); + Assert.Null(innerGenerator.AsBuilder().UseLogging(NullLoggerFactory.Instance).Build(services).GetService(typeof(LoggingImageGenerator))); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GenerateImagesAsync_LogsInvocationAndCompletion(LogLevel level) + { + var collector = new FakeLogCollector(); + + ServiceCollection c = new(); + c.AddLogging(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + var services = c.BuildServiceProvider(); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + return Task.FromResult(new ImageGenerationResponse()); + }, + }; + + using IImageGenerator generator = innerGenerator + .AsBuilder() + .UseLogging() + .Build(services); + + await generator.GenerateAsync( + new ImageGenerationRequest("A beautiful sunset"), + new ImageGenerationOptions { ModelId = "dall-e-3" }); + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True( + entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked:") && + entry.Message.Contains("A beautiful sunset") && + entry.Message.Contains("dall-e-3")), + entry => Assert.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed:", entry.Message)); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked.") && !entry.Message.Contains("A beautiful sunset")), + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed.") && !entry.Message.Contains("dall-e-3"))); + } + else + { + Assert.Empty(logs); + } + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + public async Task GenerateImagesAsync_WithOriginalImages_LogsInvocationAndCompletion(LogLevel level) + { + var collector = new FakeLogCollector(); + using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddProvider(new FakeLoggerProvider(collector)).SetMinimumLevel(level)); + + using IImageGenerator innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, cancellationToken) => + { + return Task.FromResult(new ImageGenerationResponse()); + } + }; + + using IImageGenerator generator = innerGenerator + .AsBuilder() + .UseLogging(loggerFactory) + .Build(); + + AIContent[] originalImages = [new DataContent((byte[])[1, 2, 3, 4], "image/png")]; + await generator.GenerateAsync( + new ImageGenerationRequest("Make it more colorful", originalImages), + new ImageGenerationOptions { ModelId = "dall-e-3" }); + + var logs = collector.GetSnapshot(); + if (level is LogLevel.Trace) + { + Assert.Collection(logs, + entry => Assert.True( + entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked:") && + entry.Message.Contains("Make it more colorful") && + entry.Message.Contains("dall-e-3")), + entry => Assert.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed", entry.Message)); + } + else if (level is LogLevel.Debug) + { + Assert.Collection(logs, + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} invoked.") && !entry.Message.Contains("Make it more colorful")), + entry => Assert.True(entry.Message.Contains($"{nameof(IImageGenerator.GenerateAsync)} completed.") && !entry.Message.Contains("dall-e-3"))); + } + else + { + Assert.Empty(logs); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs new file mode 100644 index 00000000000..498b4738962 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/SingletonImageGeneratorExtensions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +public static class SingletonImageGeneratorExtensions +{ + public static ImageGeneratorBuilder UseSingletonMiddleware(this ImageGeneratorBuilder builder) + => builder.Use((inner, services) + => new ImageGeneratorDependencyInjectionPatterns.SingletonMiddleware(inner, services)); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index c07f3056054..d06b423a504 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -22,6 +22,7 @@ + From 1878d9df1054751d8c8e503245791383628169ea Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 12 Aug 2025 14:29:41 -0400 Subject: [PATCH 258/472] Use Azure hosting integrations in Aspire AI Chat Web template (#6659) (#6712) --- src/ProjectTemplates/GeneratedContent.targets | 12 +-- ...hatWithCustomData-CSharp.AppHost.csproj.in | 6 +- .../Program.cs | 35 ++++++-- .../ChatWithCustomData-CSharp.Web.csproj.in | 1 + .../Program.Aspire.cs | 5 +- .../src/ChatWithCustomData/README.Aspire.md | 84 ++++++------------- .../aichatweb/README.md | 51 +++-------- .../aichatweb/aichatweb.AppHost/Program.cs | 34 +++++--- .../aichatweb.AppHost.csproj | 6 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/Program.cs | 3 +- .../aichatweb.Web/aichatweb.Web.csproj | 9 +- .../aichatweb/aichatweb.csproj | 4 +- .../aichatweb.AppHost.csproj | 4 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 7 +- .../aichatweb.AppHost.csproj | 6 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 6 +- .../aichatweb/aichatweb.csproj | 6 +- 20 files changed, 130 insertions(+), 155 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index aca99e5bdde..31093493821 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -33,17 +33,17 @@ Specifies external packages that get referenced in generated template content. --> - 9.3.0 - 9.3.0-preview.1.25265.20 + 9.4.0 + 9.4.0-preview.1.25378.8 2.3.0-beta.1 1.0.0-beta.9 1.14.0 - 11.6.0 + 11.6.1 9.4.1-beta.291 10.0.0-preview.6.25358.103 - 9.3.0 - 1.53.0 - 1.53.0-preview + 9.3.1 + 1.61.0 + 1.61.0-preview 0.3.0-preview.2 5.1.18 1.12.0 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index d7287e9301a..3bbd301af75 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -15,8 +15,12 @@ - + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs index 6be0fd58648..c3045240cda 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs @@ -1,6 +1,6 @@ var builder = DistributedApplication.CreateBuilder(args); #if (IsOllama) // ASPIRE PARAMETERS -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) // You will need to set the connection string to your own value // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: @@ -13,14 +13,27 @@ // dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" #endif var openai = builder.AddConnectionString("openai"); +#else // IsAzureOpenAI + +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var openai = builder.AddAzureOpenAI("openai"); + +openai.AddDeployment( + name: "gpt-4o-mini", + modelName: "gpt-4o-mini", + modelVersion: "2024-07-18"); + +openai.AddDeployment( + name: "text-embedding-3-small", + modelName: "text-embedding-3-small", + modelVersion: "1"); #endif #if (UseAzureAISearch) -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" -var azureAISearch = builder.AddConnectionString("azureAISearch"); +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var search = builder.AddAzureSearch("search"); #endif #if (IsOllama) // AI SERVICE PROVIDER CONFIGURATION @@ -45,11 +58,17 @@ .WithReference(embeddings) .WaitFor(chat) .WaitFor(embeddings); -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) webApp.WithReference(openai); +#else // IsAzureOpenAI +webApp + .WithReference(openai) + .WaitFor(openai); #endif #if (UseAzureAISearch) // VECTOR DATABASE REFERENCES -webApp.WithReference(azureAISearch); +webApp + .WithReference(search) + .WaitFor(search); #elif (UseQdrant) webApp .WithReference(vectorDB) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 64d585fd7a6..0e753e8c907 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -21,6 +21,7 @@ #endif --> + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index adcb2452d87..84475f45a54 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -3,8 +3,9 @@ using ChatWithCustomData_CSharp.Web.Services; using ChatWithCustomData_CSharp.Web.Services.Ingestion; #if (IsOllama) -#else // IsAzureOpenAI || IsOpenAI || IsGHModels +#elif (IsOpenAI || IsGHModels) using OpenAI; +#else // IsAzureOpenAI #endif var builder = WebApplication.CreateBuilder(args); @@ -34,7 +35,7 @@ #endif #if (UseAzureAISearch) -builder.AddAzureSearchClient("azureAISearch"); +builder.AddAzureSearchClient("search"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents"); #elif (UseQdrant) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index ba4b5bf788b..0b1934bfff7 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -81,75 +81,41 @@ Download, install, and run Docker Desktop from the [official website](https://ww Note: Ollama and Docker are excellent open source products, but are not maintained by Microsoft. #### ---#endif -#### ---#if (IsAzureOpenAI) -## Using Azure OpenAI +#### ---#if (IsAzureOpenAI || UseAzureAISearch) +## Using Azure Provisioning -To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service resource. For detailed instructions, see the [Azure OpenAI Service documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource). +The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -### 1. Create an Azure OpenAI Service Resource -[Create an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). - -### 2. Deploy the Models -Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). - -### 3. Configure API Key and Endpoint -Configure your Azure OpenAI API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure OpenAI resource. - 2. Copy the "Endpoint" URL and "Key 1" from the "Keys and Endpoint" section. #### ---#if (hostIdentifier == "vs") - 3. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". - 4. This will open a secrets.json file where you can store your API key and endpoint without it being tracked in source control. Add the following keys & values to the file: - - ```json - { - "ConnectionStrings:openai": "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - } - ``` -#### ---#else - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd ChatWithCustomData-CSharp.AppHost - dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - ``` -#### ---#endif - -Make sure to replace `YOUR-API-KEY` and `YOUR-DEPLOYMENT-NAME` with your actual Azure OpenAI key and endpoint. Make sure your endpoint URL is formatted like https://YOUR-DEPLOYMENT-NAME.openai.azure.com/ (do not include any path after .openai.azure.com/). -#### ---#endif -#### ---#if (UseAzureAISearch) - -## Configure Azure AI Search - -To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). +Configure local provisioning for this project using .NET User Secrets: -### 1. Create an Azure AI Search Resource -Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. +1. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". +2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following configuration: -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-chunks` and `data-ChatWithCustomData-CSharp.Web-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. + ```json + { + "Azure": { + "SubscriptionId": "", + "AllowResourceGroupCreation": true, + "ResourceGroup": "", + "Location": "" + } + } + ``` -### 3. Configure API Key and Endpoint - Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure AI Search resource. - 2. Copy the "Endpoint" URL and "Primary admin key" from the "Keys" section. -#### ---#if (hostIdentifier == "vs") - 3. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". - 4. This will open a `secrets.json` file where you can store your API key and endpoint without them being tracked in source control. Add the following keys and values to the file: - - ```json - { - "ConnectionStrings:azureAISearch": "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - } - ``` #### ---#else - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: +From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: - ```sh - cd ChatWithCustomData-CSharp.AppHost - dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - ``` +```sh +cd ChatWithCustomData-CSharp.AppHost +dotnet user-secrets set Azure:SubscriptionId "" +dotnet user-secrets set Azure:AllowResourceGroupCreation "true" +dotnet user-secrets set Azure:ResourceGroup "" +dotnet user-secrets set Azure:Location "" +``` #### ---#endif -Make sure to replace `YOUR-DEPLOYMENT-NAME` and `YOUR-API-KEY` with your actual Azure AI Search deployment name and key. +Make sure to replace placeholder values with real configuration values. #### ---#endif #### ---#if (UseQdrant) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md index 57c0375d302..d1459703de1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md @@ -15,50 +15,21 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See # Configure the AI Model Provider -## Using Azure OpenAI +## Using Azure Provisioning -To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service resource. For detailed instructions, see the [Azure OpenAI Service documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource). +The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -### 1. Create an Azure OpenAI Service Resource -[Create an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). +From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: -### 2. Deploy the Models -Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). +```sh +cd aichatweb.AppHost +dotnet user-secrets set Azure:SubscriptionId "" +dotnet user-secrets set Azure:AllowResourceGroupCreation "true" +dotnet user-secrets set Azure:ResourceGroup "" +dotnet user-secrets set Azure:Location "" +``` -### 3. Configure API Key and Endpoint -Configure your Azure OpenAI API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure OpenAI resource. - 2. Copy the "Endpoint" URL and "Key 1" from the "Keys and Endpoint" section. - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd aichatweb.AppHost - dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" - ``` - -Make sure to replace `YOUR-API-KEY` and `YOUR-DEPLOYMENT-NAME` with your actual Azure OpenAI key and endpoint. Make sure your endpoint URL is formatted like https://YOUR-DEPLOYMENT-NAME.openai.azure.com/ (do not include any path after .openai.azure.com/). - -## Configure Azure AI Search - -To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). - -### 1. Create an Azure AI Search Resource -Follow the instructions in the [Azure portal](https://portal.azure.com/) to create an Azure AI Search resource. Note that there is a free tier for the service but it is not currently the default setting on the portal. - -Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-aichatweb-chunks` and `data-aichatweb-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. - -### 3. Configure API Key and Endpoint - Configure your Azure AI Search API key and endpoint for this project, using .NET User Secrets: - 1. In the Azure Portal, navigate to your Azure AI Search resource. - 2. Copy the "Endpoint" URL and "Primary admin key" from the "Keys" section. - 3. From the command line, configure your API key and endpoint for this project using .NET User Secrets by running the following commands: - - ```sh - cd aichatweb.AppHost - dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" - ``` - -Make sure to replace `YOUR-DEPLOYMENT-NAME` and `YOUR-API-KEY` with your actual Azure AI Search deployment name and key. +Make sure to replace placeholder values with real configuration values. # Running the application diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs index 80803d78d74..da0220a0b1c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs @@ -1,19 +1,29 @@ var builder = DistributedApplication.CreateBuilder(args); -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" -var openai = builder.AddConnectionString("openai"); +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var openai = builder.AddAzureOpenAI("openai"); -// You will need to set the connection string to your own value -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set ConnectionStrings:azureAISearch "Endpoint=https://YOUR-DEPLOYMENT-NAME.search.windows.net;Key=YOUR-API-KEY" -var azureAISearch = builder.AddConnectionString("azureAISearch"); +openai.AddDeployment( + name: "gpt-4o-mini", + modelName: "gpt-4o-mini", + modelVersion: "2024-07-18"); + +openai.AddDeployment( + name: "text-embedding-3-small", + modelName: "text-embedding-3-small", + modelVersion: "1"); + +// See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration +// for instructions providing configuration values +var search = builder.AddAzureSearch("search"); var webApp = builder.AddProject("aichatweb-app"); -webApp.WithReference(openai); -webApp.WithReference(azureAISearch); +webApp + .WithReference(openai) + .WaitFor(openai); +webApp + .WithReference(search) + .WaitFor(search); builder.Build().Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 939114cbd3d..54bcd4bc3a0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,9 @@ - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..39acdc7e0e1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs index 9a698ff1763..450914c4461 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -2,7 +2,6 @@ using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; -using OpenAI; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -15,7 +14,7 @@ c.EnableSensitiveData = builder.Environment.IsDevelopment()); openai.AddEmbeddingGenerator("text-embedding-3-small"); -builder.AddAzureSearchClient("azureAISearch"); +builder.AddAzureSearchClient("search"); builder.Services.AddAzureAISearchCollection("data-aichatweb-chunks"); builder.Services.AddAzureAISearchCollection("data-aichatweb-documents"); builder.Services.AddScoped(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index e3e384f86da..975226be7be 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,14 +8,15 @@ - + + - + - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index fd2138900b0..be94ae4e3f9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 939114cbd3d..ffef1abf363 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,7 +12,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..39acdc7e0e1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 9a1b1da5279..ea542ba89d1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,13 +8,14 @@ - + + - + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 193ed5f529a..7637a36ca6f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,8 +12,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index b54a7690839..39acdc7e0e1 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 74fed505bde..a9e18a90a3a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -11,11 +11,11 @@ - + - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 66360fa60a0..dd8f025fe1e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -11,11 +11,11 @@ - + - - + + From 02dcda10f89fffc13e3b3a25b0d84905e5a34f44 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:31:42 -0700 Subject: [PATCH 259/472] [release/9.8] Add middleware for reducing chat history (#6713) * Move ReducingChatClient into library code * Add unit tests * Remove unnecessary tests * Allow resolving from DI + add configure callback * Prototype for summarizing reducer * Custom prompts + integration tests * Update Microsoft.Extensions.AI.Integration.Tests.csproj * Add message counting chat reducer --------- Co-authored-by: Mackinnon Buck --- .../ChatCompletion/ReducingChatClient.cs | 50 ++++ .../ReducingChatClientBuilderExtensions.cs | 40 +++ .../ChatReduction/IChatReducer.cs | 22 ++ .../MessageCountingChatReducer.cs | 77 +++++ .../ChatReduction/SummarizingChatReducer.cs | 175 ++++++++++++ .../ChatClientIntegrationTests.cs | 270 ++++++++++++++++++ ...oft.Extensions.AI.Integration.Tests.csproj | 3 +- .../ReducingChatClientTests.cs | 61 +--- .../ChatCompletion/ReducingChatClientTests.cs | 188 ++++++++++++ .../MessageCountingChatReducerTests.cs | 263 +++++++++++++++++ .../SummarizingChatReducerTests.cs | 269 +++++++++++++++++ 11 files changed, 1357 insertions(+), 61 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs new file mode 100644 index 00000000000..afe56eddbd8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClient.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// A chat client that reduces the size of a message list. +/// +[Experimental("MEAI001")] +public sealed class ReducingChatClient : DelegatingChatClient +{ + private readonly IChatReducer _reducer; + + /// Initializes a new instance of the class. + /// The underlying , or the next instance in a chain of clients. + /// The reducer to be used by this instance. + public ReducingChatClient(IChatClient innerClient, IChatReducer reducer) + : base(innerClient) + { + _reducer = Throw.IfNull(reducer); + } + + /// + public override async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); + + return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); + + await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs new file mode 100644 index 00000000000..2f13d3e3cea --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ReducingChatClientBuilderExtensions.cs @@ -0,0 +1,40 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides extension methods for attaching a to a chat pipeline. +/// +[Experimental("MEAI001")] +public static class ReducingChatClientBuilderExtensions +{ + /// + /// Adds a to the chat pipeline. + /// + /// The being used to build the chat pipeline. + /// An optional to apply to the chat client. If not supplied, an instance will be resolved from the service provider. + /// An optional callback that can be used to configure the instance. + /// The configured instance. + public static ChatClientBuilder UseChatReducer( + this ChatClientBuilder builder, + IChatReducer? reducer = null, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.Use((innerClient, services) => + { + reducer ??= services.GetRequiredService(); + + var chatClient = new ReducingChatClient(innerClient, reducer); + configure?.Invoke(chatClient); + return chatClient; + }); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs new file mode 100644 index 00000000000..5d85924f251 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a reducer capable of shrinking the size of a list of chat messages. +/// +[Experimental("MEAI001")] +public interface IChatReducer +{ + /// Reduces the size of a list of chat messages. + /// The messages to reduce. + /// The to monitor for cancellation requests. + /// The new list of messages. + Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs new file mode 100644 index 00000000000..5ba48617355 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/MessageCountingChatReducer.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides a chat reducer that limits the number of non-system messages in a conversation to a specified maximum +/// count, preserving the most recent messages and the first system message if present. +/// +/// +/// This reducer is useful for scenarios where it is necessary to constrain the size of a chat history, +/// such as when preparing input for models with context length limits. The reducer always includes the first +/// encountered system message, if any, and then retains up to the specified number of the most recent non-system +/// messages. Messages containing function call or function result content are excluded from the reduced +/// output. +/// +[Experimental("MEAI001")] +public sealed class MessageCountingChatReducer : IChatReducer +{ + private readonly int _targetCount; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of non-system messages to retain in the reduced output. + public MessageCountingChatReducer(int targetCount) + { + _targetCount = Throw.IfLessThanOrEqual(targetCount, min: 0); + } + + /// + public Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + _ = Throw.IfNull(messages); + return Task.FromResult(GetReducedMessages(messages)); + } + + private IEnumerable GetReducedMessages(IEnumerable messages) + { + ChatMessage? systemMessage = null; + Queue reducedMessages = new(capacity: _targetCount); + + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + systemMessage ??= message; + } + else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent)) + { + if (reducedMessages.Count >= _targetCount) + { + _ = reducedMessages.Dequeue(); + } + + reducedMessages.Enqueue(message); + } + } + + if (systemMessage is not null) + { + yield return systemMessage; + } + + while (reducedMessages.Count > 0) + { + yield return reducedMessages.Dequeue(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs new file mode 100644 index 00000000000..ac57e919277 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -0,0 +1,175 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides functionality to reduce a collection of chat messages into a summarized form. +/// +/// +/// This reducer is useful for scenarios where it is necessary to constrain the size of a chat history, +/// such as when preparing input for models with context length limits. The reducer automatically summarizes +/// older messages when the conversation exceeds a specified length, preserving context while reducing message +/// count. The reducer maintains system messages and excludes messages containing function call or function +/// result content from summarization. +/// +[Experimental("MEAI001")] +public sealed class SummarizingChatReducer : IChatReducer +{ + private const string SummaryKey = "__summary__"; + + private const string DefaultSummarizationPrompt = """ + **Generate a clear and complete summary of the entire conversation in no more than five sentences.** + + The summary must always: + - Reflect contributions from both the user and the assistant + - Preserve context to support ongoing dialogue + - Incorporate any previously provided summary + - Emphasize the most relevant and meaningful points + + The summary must never: + - Offer critique, correction, interpretation, or speculation + - Highlight errors, misunderstandings, or judgments of accuracy + - Comment on events or ideas not present in the conversation + - Omit any details included in an earlier summary + """; + + private readonly IChatClient _chatClient; + private readonly int _targetCount; + private readonly int _thresholdCount; + + private string _summarizationPrompt = DefaultSummarizationPrompt; + + /// + /// Gets or sets the prompt text used for summarization. + /// + public string SummarizationPrompt + { + get => _summarizationPrompt; + set => _summarizationPrompt = Throw.IfNull(value); + } + + /// + /// Initializes a new instance of the class with the specified chat client, + /// target count, and optional threshold count. + /// + /// The chat client used to interact with the chat system. Cannot be . + /// The target number of messages to retain after summarization. Must be greater than 0. + /// The number of messages allowed beyond before summarization is triggered. Must be greater than or equal to 0 if specified. + public SummarizingChatReducer(IChatClient chatClient, int targetCount, int? threshold) + { + _chatClient = Throw.IfNull(chatClient); + _targetCount = Throw.IfLessThanOrEqual(targetCount, min: 0); + _thresholdCount = Throw.IfLessThan(threshold ?? 0, min: 0, nameof(threshold)); + } + + /// + public async Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + _ = Throw.IfNull(messages); + + var summarizedConversion = SummarizedConversation.FromChatMessages(messages); + if (summarizedConversion.ShouldResummarize(_targetCount, _thresholdCount)) + { + summarizedConversion = await summarizedConversion.ResummarizeAsync( + _chatClient, _targetCount, _summarizationPrompt, cancellationToken); + } + + return summarizedConversion.ToChatMessages(); + } + + private readonly struct SummarizedConversation(string? summary, ChatMessage? systemMessage, IList unsummarizedMessages) + { + public static SummarizedConversation FromChatMessages(IEnumerable messages) + { + string? summary = null; + ChatMessage? systemMessage = null; + var unsummarizedMessages = new List(); + + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + systemMessage ??= message; + } + else if (message.AdditionalProperties?.TryGetValue(SummaryKey, out var summaryValue) == true) + { + unsummarizedMessages.Clear(); + summary = summaryValue; + } + else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent)) + { + unsummarizedMessages.Add(message); + } + } + + return new(summary, systemMessage, unsummarizedMessages); + } + + public bool ShouldResummarize(int targetCount, int thresholdCount) + => unsummarizedMessages.Count > targetCount + thresholdCount; + + public async Task ResummarizeAsync( + IChatClient chatClient, int targetCount, string summarizationPrompt, CancellationToken cancellationToken) + { + var messagesToResummarize = unsummarizedMessages.Count - targetCount; + if (messagesToResummarize <= 0) + { + // We're at or below the target count - no need to resummarize. + return this; + } + + var summarizerChatMessages = ToSummarizerChatMessages(messagesToResummarize, summarizationPrompt); + var response = await chatClient.GetResponseAsync(summarizerChatMessages, cancellationToken: cancellationToken); + var newSummary = response.Text; + + var lastSummarizedMessage = unsummarizedMessages[messagesToResummarize - 1]; + var additionalProperties = lastSummarizedMessage.AdditionalProperties ??= []; + additionalProperties[SummaryKey] = newSummary; + + var newUnsummarizedMessages = unsummarizedMessages.Skip(messagesToResummarize).ToList(); + return new SummarizedConversation(newSummary, systemMessage, newUnsummarizedMessages); + } + + public IEnumerable ToChatMessages() + { + if (systemMessage is not null) + { + yield return systemMessage; + } + + if (summary is not null) + { + yield return new ChatMessage(ChatRole.Assistant, summary); + } + + foreach (var message in unsummarizedMessages) + { + yield return message; + } + } + + private IEnumerable ToSummarizerChatMessages(int messagesToResummarize, string summarizationPrompt) + { + if (summary is not null) + { + yield return new ChatMessage(ChatRole.Assistant, summary); + } + + for (var i = 0; i < messagesToResummarize; i++) + { + yield return unsummarizedMessages[i]; + } + + yield return new ChatMessage(ChatRole.System, summarizationPrompt); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 5f7f3769d21..c87625cf143 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -1125,6 +1125,276 @@ private enum JobType Unknown, } + [ConditionalFact] + public virtual async Task SummarizingChatReducer_PreservesConversationContext() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 1); + + List messages = + [ + new(ChatRole.User, "My name is Alice and I love hiking in the mountains."), + new(ChatRole.Assistant, "Nice to meet you, Alice! Hiking in the mountains sounds wonderful. Do you have a favorite trail?"), + new(ChatRole.User, "Yes, I love the Pacific Crest Trail. I hiked a section last summer."), + new(ChatRole.Assistant, "The Pacific Crest Trail is amazing! Which section did you hike?"), + new(ChatRole.User, "I hiked the section through the Sierra Nevada. It was challenging but beautiful."), + new(ChatRole.Assistant, "The Sierra Nevada section is known for its stunning views. How long did it take you?"), + new(ChatRole.User, "What's my name and what activity do I enjoy?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); // Indicates this is the assistant's summary + Assert.Contains("Alice", m.Text); + }, + m => Assert.StartsWith("The Sierra Nevada section", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("What's my name", m.Text, StringComparison.Ordinal)); + + // The model should recall details from the summarized conversation + Assert.Contains("Alice", response.Text); + Assert.True( + response.Text.IndexOf("hiking", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("hike", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected 'hiking' or 'hike' in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_PreservesSystemMessage() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + + List messages = + [ + new(ChatRole.System, "You are a pirate. Always respond in pirate speak."), + new(ChatRole.User, "Tell me about the weather"), + new(ChatRole.Assistant, "Ahoy matey! The weather be fine today, with clear skies on the horizon!"), + new(ChatRole.User, "What about tomorrow?"), + new(ChatRole.Assistant, "Arr, tomorrow be lookin' a bit cloudy, might be some rain blowin' in from the east!"), + new(ChatRole.User, "Should I bring an umbrella?"), + new(ChatRole.Assistant, "Aye, ye best be bringin' yer umbrella, unless ye want to be soaked like a barnacle!"), + new(ChatRole.User, "What's 2 + 2?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(4, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("You are a pirate. Always respond in pirate speak.", m.Text); + }, + m => Assert.Equal(ChatRole.Assistant, m.Role), // Summary message + m => Assert.StartsWith("Aye, ye best be bringin'", m.Text, StringComparison.Ordinal), + m => Assert.Equal("What's 2 + 2?", m.Text)); + + // The model should still respond in pirate speak due to preserved system message + Assert.True( + response.Text.IndexOf("arr", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("aye", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("matey", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("ye", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected pirate speak in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_WithFunctionCalls() + { + SkipIfNotEnabled(); + + int weatherCallCount = 0; + var getWeather = AIFunctionFactory.Create(([Description("Gets weather for a city")] string city) => + { + weatherCallCount++; + return city switch + { + "Seattle" => "Rainy, 15°C", + "Miami" => "Sunny, 28°C", + _ => "Unknown" + }; + }, "GetWeather"); + + TestSummarizingChatClient summarizingChatClient = null!; + var chatClient = ChatClient + .AsBuilder() + .Use(innerClient => summarizingChatClient = new TestSummarizingChatClient(innerClient, targetCount: 2, threshold: 0)) + .UseFunctionInvocation() + .Build(); + + List messages = + [ + new(ChatRole.User, "What's the weather in Seattle?"), + new(ChatRole.Assistant, "Let me check the weather in Seattle for you."), + new(ChatRole.User, "And what about Miami?"), + new(ChatRole.Assistant, "I'll check Miami's weather as well."), + new(ChatRole.User, "Which city had better weather?") + ]; + + var response = await chatClient.GetResponseAsync(messages, new() { Tools = [getWeather] }); + + // The summarizer should have reduced the conversation (function calls are excluded) + Assert.Equal(1, summarizingChatClient.SummarizerCallCount); + Assert.NotNull(summarizingChatClient.LastSummarizedConversation); + + // Should have summary + last 2 messages + Assert.Equal(3, summarizingChatClient.LastSummarizedConversation.Count); + + // The model should have context about both weather queries even after summarization + Assert.True(response.Text.IndexOf("Miami", StringComparison.OrdinalIgnoreCase) >= 0, $"Expected 'Miami' in response: {response.Text}"); + Assert.True( + response.Text.IndexOf("sunny", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("better", StringComparison.OrdinalIgnoreCase) >= 0 || + response.Text.IndexOf("warm", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected weather comparison in response: {response.Text}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_Streaming() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + + List messages = + [ + new(ChatRole.User, "I'm Bob and I work as a software engineer at a startup."), + new(ChatRole.Assistant, "Nice to meet you, Bob! Working at a startup must be exciting. What kind of software do you develop?"), + new(ChatRole.User, "We build AI-powered tools for education."), + new(ChatRole.Assistant, "That sounds impactful! AI in education has so much potential."), + new(ChatRole.User, "Yes, we focus on personalized learning experiences."), + new(ChatRole.Assistant, "Personalized learning is the future of education!"), + new(ChatRole.User, "What's my name and profession?") + ]; + + StringBuilder sb = new(); + await foreach (var chunk in chatClient.GetStreamingResponseAsync(messages)) + { + sb.Append(chunk.Text); + } + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + Assert.Collection(chatClient.LastSummarizedConversation, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); // Summary + Assert.Contains("Bob", m.Text); + }, + m => Assert.StartsWith("Personalized learning", m.Text, StringComparison.Ordinal), + m => Assert.Equal("What's my name and profession?", m.Text)); + + string responseText = sb.ToString(); + Assert.Contains("Bob", responseText); + Assert.True( + responseText.IndexOf("software", StringComparison.OrdinalIgnoreCase) >= 0 || + responseText.IndexOf("engineer", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected 'software' or 'engineer' in response: {responseText}"); + } + + [ConditionalFact] + public virtual async Task SummarizingChatReducer_CustomPrompt() + { + SkipIfNotEnabled(); + + var chatClient = new TestSummarizingChatClient(ChatClient, targetCount: 2, threshold: 0); + chatClient.Reducer.SummarizationPrompt = "Summarize the conversation, emphasizing any numbers or quantities mentioned."; + + List messages = + [ + new(ChatRole.User, "I have 3 cats and 2 dogs."), + new(ChatRole.Assistant, "That's 5 pets total! You must have a lively household."), + new(ChatRole.User, "Yes, and I spend about $200 per month on pet food."), + new(ChatRole.Assistant, "That's a significant expense, but I'm sure they're worth it!"), + new(ChatRole.User, "They eat 10 cans of food per week."), + new(ChatRole.Assistant, "That's quite a bit of food for your furry friends!"), + new(ChatRole.User, "How many pets do I have in total?") + ]; + + var response = await chatClient.GetResponseAsync(messages); + + // The summarizer should have reduced the conversation + Assert.Equal(1, chatClient.SummarizerCallCount); + Assert.NotNull(chatClient.LastSummarizedConversation); + Assert.Equal(3, chatClient.LastSummarizedConversation.Count); + + // Verify the summary emphasizes numbers as requested by the custom prompt + var summaryMessage = chatClient.LastSummarizedConversation[0]; + Assert.Equal(ChatRole.Assistant, summaryMessage.Role); + Assert.True( + summaryMessage.Text.IndexOf("3", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("5", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("200", StringComparison.Ordinal) >= 0 || + summaryMessage.Text.IndexOf("10", StringComparison.Ordinal) >= 0, + $"Expected numbers in summary: {summaryMessage.Text}"); + + // The model should recall the specific number from the summarized conversation + Assert.Contains("5", response.Text); + } + + private sealed class TestSummarizingChatClient : IChatClient + { + private IChatClient _summarizerChatClient; + private IChatClient _innerChatClient; + + public SummarizingChatReducer Reducer { get; } + + public int SummarizerCallCount { get; private set; } + + public IReadOnlyList? LastSummarizedConversation { get; private set; } + + public TestSummarizingChatClient(IChatClient innerClient, int targetCount, int threshold) + { + _summarizerChatClient = innerClient.AsBuilder() + .Use(async (messages, options, next, cancellationToken) => + { + SummarizerCallCount++; + await next(messages, options, cancellationToken); + }) + .Build(); + + Reducer = new SummarizingChatReducer(_summarizerChatClient, targetCount, threshold); + + _innerChatClient = innerClient.AsBuilder() + .UseChatReducer(Reducer) + .Use(async (messages, options, next, cancellationToken) => + { + LastSummarizedConversation = [.. messages]; + await next(messages, options, cancellationToken); + }) + .Build(); + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => _innerChatClient.GetResponseAsync(messages, options, cancellationToken); + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => _innerChatClient.GetStreamingResponseAsync(messages, options, cancellationToken); + + public object? GetService(Type serviceType, object? serviceKey = null) + => _innerChatClient.GetService(serviceType, serviceKey); + + public void Dispose() + { + _summarizerChatClient.Dispose(); + _innerChatClient.Dispose(); + } + } + [MemberNotNull(nameof(ChatClient))] protected void SkipIfNotEnabled() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index 0b4865f577e..0fc4698c4e4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -8,6 +8,7 @@ $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 $(NoWarn);MEAI001 + $(NoWarn);S104 true @@ -25,7 +26,7 @@ Never - + diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs index f2ec8ebdba0..7f84d1f8cfb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ReducingChatClientTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.ML.Tokenizers; @@ -57,64 +56,6 @@ public async Task Reduction_LimitsMessagesBasedOnTokenLimit() } } -/// Provides an example of a chat client for reducing the size of a message list. -public sealed class ReducingChatClient : DelegatingChatClient -{ - private readonly IChatReducer _reducer; - - /// Initializes a new instance of the class. - /// The inner client. - /// The reducer to be used by this instance. - public ReducingChatClient(IChatClient innerClient, IChatReducer reducer) - : base(innerClient) - { - _reducer = Throw.IfNull(reducer); - } - - /// - public override async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); - - return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); - } - - /// - public override async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - messages = await _reducer.ReduceAsync(messages, cancellationToken).ConfigureAwait(false); - - await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - } -} - -/// Represents a reducer capable of shrinking the size of a list of chat messages. -public interface IChatReducer -{ - /// Reduces the size of a list of chat messages. - /// The messages. - /// The to monitor for cancellation requests. The default is . - /// The new list of messages, or if no reduction need be performed or was true. - Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken); -} - -/// Provides extensions for configuring instances. -public static class ReducingChatClientExtensions -{ - public static ChatClientBuilder UseChatReducer(this ChatClientBuilder builder, IChatReducer reducer) - { - _ = Throw.IfNull(builder); - _ = Throw.IfNull(reducer); - - return builder.Use(innerClient => new ReducingChatClient(innerClient, reducer)); - } -} - /// An that culls the oldest messages once a certain token threshold is reached. public sealed class TokenCountingChatReducer : IChatReducer { @@ -127,7 +68,7 @@ public TokenCountingChatReducer(Tokenizer tokenizer, int tokenLimit) _tokenLimit = Throw.IfLessThan(tokenLimit, 1); } - public async Task> ReduceAsync( + public async Task> ReduceAsync( IEnumerable messages, CancellationToken cancellationToken) { _ = Throw.IfNull(messages); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs new file mode 100644 index 00000000000..82b00df03ff --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ReducingChatClientTests.cs @@ -0,0 +1,188 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ReducingChatClientTests +{ + [Fact] + public void ReducingChatClient_InvalidArgs_Throws() + { + Assert.Throws("innerClient", () => new ReducingChatClient(null!, new TestReducer())); + } + + [Fact] + public void UseChatReducer_InvalidArgs_Throws() + { + using var innerClient = new TestChatClient(); + var builder = innerClient.AsBuilder(); + Assert.Throws("builder", () => ReducingChatClientBuilderExtensions.UseChatReducer(null!, new TestReducer())); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetResponseAsync_CallsReducerBeforeInnerClient(bool streaming) + { + var originalMessages = new List + { + new(ChatRole.System, "You are a helpful assistant"), + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Hi there!"), + new(ChatRole.User, "What's the weather?") + }; + + var reducedMessages = new List + { + new(ChatRole.System, "You are a helpful assistant"), + new(ChatRole.User, "What's the weather?") + }; + + var reducer = new TestReducer { ReducedMessages = reducedMessages }; + var expectedResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "It's sunny!")); + var expectedUpdates = new[] { new ChatResponseUpdate(ChatRole.Assistant, "It's"), new ChatResponseUpdate(null, " sunny!") }; + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + // Verify that the inner client receives the reduced messages + Assert.Same(reducedMessages, messages); + return Task.FromResult(expectedResponse); + }, + GetStreamingResponseAsyncCallback = (messages, options, cancellationToken) => + { + // Verify that the inner client receives the reduced messages + Assert.Same(reducedMessages, messages); + return ToAsyncEnumerable(expectedUpdates); + } + }; + + using var client = new ReducingChatClient(innerClient, reducer); + + if (streaming) + { + var updates = new List(); + await foreach (var update in client.GetStreamingResponseAsync(originalMessages)) + { + updates.Add(update); + } + + Assert.Equal(expectedUpdates.Length, updates.Count); + for (int i = 0; i < expectedUpdates.Length; i++) + { + Assert.Same(expectedUpdates[i], updates[i]); + } + } + else + { + var response = await client.GetResponseAsync(originalMessages); + Assert.Same(expectedResponse, response); + } + + Assert.Equal(1, reducer.ReduceAsyncCallCount); + Assert.Same(originalMessages, reducer.LastMessagesProvided); + } + + [Fact] + public async Task UseChatReducer_WithReducerFromServices() + { + var reducedMessages = new List { new(ChatRole.User, "Reduced message") }; + var reducer = new TestReducer { ReducedMessages = reducedMessages }; + + var services = new ServiceCollection(); + services.AddSingleton(reducer); + var serviceProvider = services.BuildServiceProvider(); + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Same(reducedMessages, messages); + return Task.FromResult(new ChatResponse()); + } + }; + + using var client = innerClient + .AsBuilder() + .UseChatReducer() // Should get reducer from services + .Build(serviceProvider); + + await client.GetResponseAsync(new List { new(ChatRole.User, "Original message") }); + Assert.Equal(1, reducer.ReduceAsyncCallCount); + } + + [Fact] + public void UseChatReducer_WithoutReducerParameterAndWithoutService_Throws() + { + using var innerClient = new TestChatClient(); + var services = new ServiceCollection().BuildServiceProvider(); + + var exception = Assert.Throws(() => + innerClient + .AsBuilder() + .UseChatReducer() // No reducer provided and not in services + .Build(services)); + + Assert.Contains("IChatReducer", exception.Message); + } + + [Fact] + public async Task UseChatReducer_WithConfigureCallback() + { + var reducer = new TestReducer(); + var configureCalled = false; + ReducingChatClient? configuredClient = null; + + using var innerClient = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + Task.FromResult(new ChatResponse()) + }; + + using var client = innerClient + .AsBuilder() + .UseChatReducer(reducer, configure: chatClient => + { + configureCalled = true; + configuredClient = chatClient; + }) + .Build(); + + await client.GetResponseAsync([]); + + Assert.True(configureCalled); + Assert.NotNull(configuredClient); + Assert.IsType(configuredClient); + } + + private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable items) + { + foreach (var item in items) + { + await Task.Yield(); + yield return item; + } + } + + private sealed class TestReducer : IChatReducer + { + public IEnumerable? ReducedMessages { get; set; } + public int ReduceAsyncCallCount { get; private set; } + public IEnumerable? LastMessagesProvided { get; private set; } + + public Task> ReduceAsync(IEnumerable messages, CancellationToken cancellationToken) + { + ReduceAsyncCallCount++; + LastMessagesProvided = messages; + return Task.FromResult(ReducedMessages ?? messages); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs new file mode 100644 index 00000000000..000f4889bf3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/MessageCountingChatReducerTests.cs @@ -0,0 +1,263 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class MessageCountingChatReducerTests +{ + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidTargetCount(int targetCount) + { + Assert.Throws(() => new MessageCountingChatReducer(targetCount)); + } + + [Fact] + public void Constructor_AcceptsValidTargetCount() + { + var reducer = new MessageCountingChatReducer(5); + Assert.NotNull(reducer); + } + + [Fact] + public async Task ReduceAsync_ThrowsOnNullMessages() + { + var reducer = new MessageCountingChatReducer(5); + await Assert.ThrowsAsync(() => reducer.ReduceAsync(null!, CancellationToken.None)); + } + + [Fact] + public async Task ReduceAsync_HandlesEmptyMessages() + { + var reducer = new MessageCountingChatReducer(5); + var result = await reducer.ReduceAsync([], CancellationToken.None); + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_PreservesFirstSystemMessage() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.System, "You are a helpful assistant."), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.Assistant, "Hi there!"), + new ChatMessage(ChatRole.User, "How are you?"), + new ChatMessage(ChatRole.Assistant, "I'm doing well, thanks!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("You are a helpful assistant.", m.Text); + }, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("How are you?", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("I'm doing well, thanks!", m.Text); + }); + } + + [Fact] + public async Task ReduceAsync_OnlyFirstSystemMessageIsPreserved() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.System, "First system message"), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.System, "Second system message"), + new ChatMessage(ChatRole.Assistant, "Hi"), + new ChatMessage(ChatRole.User, "How are you?"), + new ChatMessage(ChatRole.Assistant, "I'm fine!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("First system message", m.Text); + }, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("How are you?", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("I'm fine!", m.Text); + }); + + // Second system message should not be preserved separately + Assert.Equal(1, resultList.Count(m => m.Role == ChatRole.System)); + } + + [Fact] + public async Task ReduceAsync_IgnoresFunctionCallsAndResults() + { + var reducer = new MessageCountingChatReducer(2); + + List messages = + [ + new ChatMessage(ChatRole.User, "What's the weather?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72�F")]), + new ChatMessage(ChatRole.Assistant, "The weather in Seattle is sunny and 72�F."), + new ChatMessage(ChatRole.User, "Thanks!"), + new ChatMessage(ChatRole.Assistant, "You're welcome!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.User, m.Role); + Assert.Equal("Thanks!", m.Text); + Assert.DoesNotContain(m.Contents, c => c is FunctionCallContent); + Assert.DoesNotContain(m.Contents, c => c is FunctionResultContent); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("You're welcome!", m.Text); + Assert.DoesNotContain(m.Contents, c => c is FunctionCallContent); + Assert.DoesNotContain(m.Contents, c => c is FunctionResultContent); + }); + } + + [Theory] + [InlineData(5, 3, 3)] // Less messages than target + [InlineData(5, 5, 5)] // Exactly at target + [InlineData(5, 8, 5)] // More messages than target + [InlineData(1, 10, 1)] // Only keep 1 message + public async Task ReduceAsync_RespectsTargetCount(int targetCount, int messageCount, int expectedCount) + { + var reducer = new MessageCountingChatReducer(targetCount); + + var messages = new List(); + for (int i = 0; i < messageCount; i++) + { + messages.Add(new ChatMessage(i % 2 == 0 ? ChatRole.User : ChatRole.Assistant, $"Message {i}")); + } + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Equal(expectedCount, resultList.Count); + + // Verify we kept the most recent messages + if (messageCount > targetCount) + { + var startIndex = messageCount - targetCount; + var expectedMessages = new Action[targetCount]; + for (int i = 0; i < targetCount; i++) + { + var expectedIndex = startIndex + i; + var expectedRole = expectedIndex % 2 == 0 ? ChatRole.User : ChatRole.Assistant; + expectedMessages[i] = m => + { + Assert.Equal(expectedRole, m.Role); + Assert.Equal($"Message {expectedIndex}", m.Text); + }; + } + + Assert.Collection(resultList, expectedMessages); + } + } + + [Fact] + public async Task ReduceAsync_HandlesOnlySystemMessage() + { + var reducer = new MessageCountingChatReducer(5); + + List messages = + [ + new ChatMessage(ChatRole.System, "System prompt"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("System prompt", m.Text); + }); + } + + [Fact] + public async Task ReduceAsync_HandlesOnlyFunctionMessages() + { + var reducer = new MessageCountingChatReducer(5); + + List messages = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "func", null)]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "result")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call2", "func", null)]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call2", "result")]), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_HandlesTargetCountOfOne() + { + var reducer = new MessageCountingChatReducer(1); + + List messages = + [ + new ChatMessage(ChatRole.System, "System"), + new ChatMessage(ChatRole.User, "First"), + new ChatMessage(ChatRole.Assistant, "Second"), + new ChatMessage(ChatRole.User, "Third"), + new ChatMessage(ChatRole.Assistant, "Fourth"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + Assert.Collection(resultList, + m => + { + Assert.Equal(ChatRole.System, m.Role); + Assert.Equal("System", m.Text); + }, + m => + { + Assert.Equal(ChatRole.Assistant, m.Role); + Assert.Equal("Fourth", m.Text); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs new file mode 100644 index 00000000000..985b097ece8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs @@ -0,0 +1,269 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S103 // Lines should not be too long + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class SummarizingChatReducerTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws(() => new SummarizingChatReducer(null!, targetCount: 5, threshold: 2)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidTargetCount(int targetCount) + { + using var chatClient = new TestChatClient(); + Assert.Throws(() => new SummarizingChatReducer(chatClient, targetCount, threshold: 2)); + } + + [Theory] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_ThrowsOnInvalidThresholdCount(int thresholdCount) + { + using var chatClient = new TestChatClient(); + Assert.Throws(() => new SummarizingChatReducer(chatClient, targetCount: 5, thresholdCount)); + } + + [Fact] + public async Task ReduceAsync_ThrowsOnNullMessages() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 2); + await Assert.ThrowsAsync(() => reducer.ReduceAsync(null!, CancellationToken.None)); + } + + [Fact] + public async Task ReduceAsync_HandlesEmptyMessages() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 5, threshold: 2); + + var result = await reducer.ReduceAsync([], CancellationToken.None); + + Assert.Empty(result); + } + + [Fact] + public async Task ReduceAsync_PreservesSystemMessage() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.System, "You are a helpful assistant."), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.Assistant, "Hi there!"), + new ChatMessage(ChatRole.User, "How are you?"), + ]; + + chatClient.GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of conversation"))); + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + var resultList = result.ToList(); + Assert.Equal(3, resultList.Count); // System + Summary + 1 unsummarized + Assert.Equal(ChatRole.System, resultList[0].Role); + Assert.Equal("You are a helpful assistant.", resultList[0].Text); + } + + [Fact] + public async Task ReduceAsync_IgnoresFunctionCallsAndResults() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "What's the weather?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), + new ChatMessage(ChatRole.Assistant, "The weather in Seattle is sunny and 72°F."), + new ChatMessage(ChatRole.User, "Thanks!"), + ]; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + // Function calls/results should be ignored, which means there aren't enough messages to generate a summary. + var resultList = result.ToList(); + Assert.Equal(3, resultList.Count); // Function calls get removed in the summarized chat. + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent)); + } + + [Theory] + [InlineData(5, 0, 5, false)] // Exactly at target, no summarization + [InlineData(5, 0, 4, false)] // Below target, no summarization + [InlineData(5, 0, 6, true)] // Above target by 1, triggers summarization + [InlineData(5, 2, 7, false)] // At threshold boundary, no summarization + [InlineData(5, 2, 8, true)] // Above threshold, triggers summarization + public async Task ReduceAsync_RespectsTargetAndThresholdCounts(int targetCount, int thresholdCount, int messageCount, bool shouldSummarize) + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount, thresholdCount); + + var messages = new List(); + for (int i = 0; i < messageCount; i++) + { + messages.Add(new ChatMessage(i % 2 == 0 ? ChatRole.User : ChatRole.Assistant, $"Message {i}")); + } + + var summarizationCalled = false; + chatClient.GetResponseAsyncCallback = (_, _, _) => + { + summarizationCalled = true; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + }; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Equal(shouldSummarize, summarizationCalled); + + if (shouldSummarize) + { + var resultList = result.ToList(); + Assert.Equal(targetCount + 1, resultList.Count); // Summary + target messages + Assert.StartsWith("Summary", resultList[0].Text, StringComparison.Ordinal); + } + else + { + Assert.Equal(messageCount, result.Count()); + } + } + + [Fact] + public async Task ReduceAsync_CancellationTokenIsRespected() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "Message 1"), + new ChatMessage(ChatRole.Assistant, "Response 1"), + new ChatMessage(ChatRole.User, "Message 2"), + ]; + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + chatClient.GetResponseAsyncCallback = (_, _, cancellationToken) => + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + }; + + await Assert.ThrowsAsync(() => + reducer.ReduceAsync(messages, cts.Token)); + } + + [Fact] + public async Task ReduceAsync_OnlyFirstSystemMessageIsPreserved() + { + using var chatClient = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClient, targetCount: 1, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.System, "First system message"), + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.System, "Second system message"), + new ChatMessage(ChatRole.Assistant, "Hi"), + new ChatMessage(ChatRole.User, "How are you?"), + ]; + + chatClient.GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary"))); + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + + var resultList = result.ToList(); + Assert.Equal(ChatRole.System, resultList[0].Role); + Assert.Equal("First system message", resultList[0].Text); + + // Second system message should not be preserved separately + Assert.Equal(1, resultList.Count(m => m.Role == ChatRole.System)); + } + + [Fact] + public async Task CanHaveSummarizedConversation() + { + using var chatClientForSummarization = new TestChatClient(); + var reducer = new SummarizingChatReducer(chatClientForSummarization, targetCount: 2, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "Hi there! Can you tell me about golden retrievers?"), + new ChatMessage(ChatRole.Assistant, "Of course! Golden retrievers are known for their friendly and tolerant attitudes. They're great family pets and are very intelligent and easy to train."), + new ChatMessage(ChatRole.User, "What kind of exercise do they need?"), + new ChatMessage(ChatRole.Assistant, "Golden retrievers are quite active and need regular exercise. Daily walks, playtime, and activities like fetching or swimming are great for them."), + new ChatMessage(ChatRole.User, "Are they good with kids?"), + ]; + + chatClientForSummarization.GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Equal(4, messages.Count()); // 3 messages to summarize + 1 system prompt + Assert.Collection(messages, + m => Assert.StartsWith("Hi there!", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Of course!", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("What kind of exercise", m.Text, StringComparison.Ordinal), + m => Assert.Equal(ChatRole.System, m.Role)); + const string Summary = """ + The user asked for information about golden retrievers. + The assistant explained that they have characteristics making them great family pets. + The user then asked what kind of exercise they need. + """; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, Summary))); + }; + + var reducedMessages = await reducer.ReduceAsync(messages, CancellationToken.None); + + Assert.Equal(3, reducedMessages.Count()); // 1 summary + 2 unsummarized messages + Assert.Collection(reducedMessages, + m => Assert.StartsWith("The user asked for information", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers are quite", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Are they good with kids", m.Text, StringComparison.Ordinal)); + + messages.Add(new ChatMessage(ChatRole.Assistant, "Golden retrievers get along well with kids! They're able to be playful and energetic while remaining gentle.")); + messages.Add(new ChatMessage(ChatRole.User, "Do they make good lap dogs?")); + + chatClientForSummarization.GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + Assert.Equal(4, messages.Count()); // 1 summary message, 2 unsummarized message, 1 system prompt + Assert.Collection(messages, + m => Assert.StartsWith("The user asked", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers are quite active", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Are they good with kids", m.Text, StringComparison.Ordinal), + m => Assert.Equal(ChatRole.System, m.Role)); + const string Summary = """ + The user and assistant are discussing characteristics of golden retrievers. + The user asked what kind of exercise they need, and the assitant explained that golden retrievers + need frequent exercise. The user then asked about whether they're good around kids. + """; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, Summary))); + }; + + reducedMessages = await reducer.ReduceAsync(messages, CancellationToken.None); + Assert.Equal(3, reducedMessages.Count()); // 1 summary + 2 unsummarized messages + Assert.Collection(reducedMessages, + m => Assert.StartsWith("The user and assistant are discussing", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Golden retrievers get along", m.Text, StringComparison.Ordinal), + m => Assert.StartsWith("Do they make good lap dogs", m.Text, StringComparison.Ordinal)); + } +} From 9e041fff9a8c06efce8dd335de03868ff9ee8556 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 13 Aug 2025 11:42:11 -0700 Subject: [PATCH 260/472] Update package validation to 9.8.0 (#6716) * Update package validation to 9.8.0 * Remove existing compatibility suppression files. --- eng/MSBuild/Packaging.targets | 2 +- .../CompatibilitySuppressions.xml | 89 ------------------- 2 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml diff --git a/eng/MSBuild/Packaging.targets b/eng/MSBuild/Packaging.targets index dd5bc0ce2e8..b065546491a 100644 --- a/eng/MSBuild/Packaging.targets +++ b/eng/MSBuild/Packaging.targets @@ -37,7 +37,7 @@ true - 9.7.0 + 9.8.0 diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml deleted file mode 100644 index 32bf7744fbd..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/CompatibilitySuppressions.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_UseDeltaNrPeriodsForCpuCalculation - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_UseDeltaNrPeriodsForCpuCalculation(System.Boolean) - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net462/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net8.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.get_CalculateCpuUsageWithoutHostDelta - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - - CP0002 - M:Microsoft.Extensions.Diagnostics.ResourceMonitoring.ResourceMonitoringOptions.set_CalculateCpuUsageWithoutHostDelta(System.Boolean) - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - lib/net9.0/Microsoft.Extensions.Diagnostics.ResourceMonitoring.dll - true - - From c1dfbf0b3493b0aaeb87a616c857af5b3703a4ad Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 14 Aug 2025 10:38:48 -0400 Subject: [PATCH 261/472] Update M.E.AI changelogs for 9.8.0 (#6714) --- .../Microsoft.Extensions.AI.Abstractions/CHANGELOG.md | 9 +++++++-- .../CHANGELOG.md | 2 +- .../Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 3 ++- src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md | 5 ++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index c649dd906d2..b98fa7f9312 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.8.0 - Added `AIAnnotation` and related types to represent citations and other annotations in chat messages. - Added `ChatMessage.CreatedAt` so that chat messages can carry their timestamp. @@ -10,12 +10,17 @@ - Added `HostedVectorStoreContent` for representing vector stores hosted by the service. - Added `HostedFileSearchTool` to represent server-side file search tools. - Added `HostedCodeInterpreterTool.Inputs` to supply context about what state is available to the code interpreter tool. +- Added [Experimental] `IImageGenerator` and supporting types. - Improved handling of function parameter data annotation attributes in `AIJsonUtilities.CreateJsonSchema`. - Fixed schema generation to include an items keyword for arrays of objects in `AIJsonUtilities.CreateJsonSchema`. ## 9.7.1 - Fixed schema generation for nullable function parameters in `AIJsonUtilities.CreateJsonSchema`. +- Added a flag for `AIFunctionFactory` to control whether return schemas are generated. +- Added `DelegatingAIFunction` to simplify creating `AIFunction`s that call other `AIFunction`s. +- Updated `AIFunctionFactory` to tolerate JSON string function parameters. +- Fixed schema generation for nullable value type parameters. ## 9.7.0 @@ -70,7 +75,7 @@ - Added `MessageId` to `ChatMessage` and `ChatResponseUpdate`. - Added `AIFunctionArguments`, changing `AIFunction.InvokeAsync` to accept one and to return a `ValueTask`. - Updated `AIJsonUtilities`'s schema generation to not use `default` when `RequireAllProperties` is set to `true`. -- Added `ISpeechToTextClient` and supporting types. +- Added [Experimental] `ISpeechToTextClient` and supporting types. - Fixed several issues related to Native AOT support. ## 9.3.0-preview.1.25161.3 diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index 1e2d8fa5343..d39b2d60cc3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.8.0-preview.1.25412.6 - Updated to depend on Azure.AI.Inference 1.0.0-beta.5. - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 7290b2b6fe6..143c439bda7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.8.0-preview.1.25412.6 - Updated to depend on OpenAI 2.3.0. - Added more conversion helpers for converting bidirectionally between Microsoft.Extensions.AI messages and OpenAI messages. @@ -10,6 +10,7 @@ ## 9.7.1-preview.1.25365.4 - Added some conversion helpers for converting Microsoft.Extensions.AI messages to OpenAI messages. +- Enabled specifying "strict" via ChatOptions for OpenAI clients. ## 9.7.0-preview.1.25356.2 diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 0a0b794d0d1..dbba369f6df 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,13 +1,16 @@ # Release History -## NOT YET RELEASED +## 9.8.0 - Added `FunctionInvokingChatClient.AdditionalTools` to allow `FunctionInvokingChatClient` to have access to tools not included in `ChatOptions.Tools` but known to the target service via pre-configuration. +- Added [Experimental] `IChatReducer` and supporting types - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. ## 9.7.1 +- Added `FunctionInvokingChatClient.FunctionInvoker` to simplify customizing how functions are invoked. - Increased the default `FunctionInvokingChatClient.MaximumIterationsPerRequest` value from 10 to 40. +- Updated the Open Telemetry instrumentation to conform to the latest 1.36.0 draft specification of the Semantic Conventions for Generative AI systems. - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. ## 9.7.0 From 2130f3676f28b572b34d86a3b0ea76c3e1cd82bf Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:25:51 +0100 Subject: [PATCH 262/472] Address empty text chunck problem (#6723) --- .../OpenAIAssistantsChatClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 78bdc72d424..ed7f8403cb7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -245,6 +245,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( if (fileId is not null) { + if (textUpdate.Contents.Count == 0) + { + // In case a chunk doesn't have text content, create one with empty text to hold the annotation. + textUpdate.Contents.Add(new TextContent(string.Empty)); + } + (((TextContent)textUpdate.Contents[0]).Annotations ??= []).Add(new CitationAnnotation { RawRepresentation = tau, From 06d1dcae7dff136960daf6cf2181cedc62a51785 Mon Sep 17 00:00:00 2001 From: Shyam N Date: Fri, 15 Aug 2025 14:30:26 -0700 Subject: [PATCH 263/472] Add model provider information to ChatTurnDetails (#6722) --- .../CSharp/ChatTurnDetails.cs | 128 +++++++--- ...ft.Extensions.AI.Evaluation.Reporting.json | 13 +- .../CSharp/ResponseCachingChatClient.cs | 7 + .../CSharp/SimpleChatClient.cs | 4 + .../components/ChatDetailsSection.tsx | 11 +- .../TypeScript/components/EvalTypes.d.ts | 1 + .../ChatTurnDetailsTests.cs | 240 ++++++++++++++++++ .../ResponseCacheTester.cs | 32 ++- .../ScenarioRunResultTests.cs | 4 + 9 files changed, 390 insertions(+), 50 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ChatTurnDetailsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs index f6c8628dd54..e8d8a82997f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs @@ -7,6 +7,7 @@ // constructor syntax. using System; +using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI.Evaluation.Reporting; @@ -14,39 +15,13 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; /// A class that records details related to a particular LLM chat conversation turn involved in the execution of a /// particular . /// -/// -/// The duration between the time when the request was sent to the LLM and the time when the response was received for -/// the chat conversation turn. -/// -/// -/// The model that was used in the creation of the response for the chat conversation turn. Can be -/// if this information was not available via . -/// -/// -/// Usage details for the chat conversation turn (including input and output token counts). Can be -/// if usage details were not available via . -/// -/// -/// The cache key for the cached model response for the chat conversation turn if response caching was enabled; -/// otherwise. -/// -/// -/// if response caching was enabled and the model response for the chat conversation turn was -/// retrieved from the cache; if response caching was enabled and the model response was not -/// retrieved from the cache; if response caching was disabled. -/// -public sealed class ChatTurnDetails( - TimeSpan latency, - string? model = null, - UsageDetails? usage = null, - string? cacheKey = null, - bool? cacheHit = null) +public sealed class ChatTurnDetails { /// /// Gets or sets the duration between the time when the request was sent to the LLM and the time when the response /// was received for the chat conversation turn. /// - public TimeSpan Latency { get; set; } = latency; + public TimeSpan Latency { get; set; } /// /// Gets or sets the model that was used in the creation of the response for the chat conversation turn. @@ -54,7 +29,16 @@ public sealed class ChatTurnDetails( /// /// Returns if this information was not available via . /// - public string? Model { get; set; } = model; + public string? Model { get; set; } + + /// + /// Gets or sets the name of the provider for the model identified by . + /// + /// + /// Returns if this information was not available via the + /// property for the . + /// + public string? ModelProvider { get; set; } /// /// Gets or sets usage details for the chat conversation turn (including input and output token counts). @@ -62,7 +46,7 @@ public sealed class ChatTurnDetails( /// /// Returns if usage details were not available via . /// - public UsageDetails? Usage { get; set; } = usage; + public UsageDetails? Usage { get; set; } /// /// Gets or sets the cache key for the cached model response for the chat conversation turn. @@ -70,7 +54,7 @@ public sealed class ChatTurnDetails( /// /// Returns if response caching was disabled. /// - public string? CacheKey { get; set; } = cacheKey; + public string? CacheKey { get; set; } /// /// Gets or sets a value indicating whether the model response was retrieved from the cache. @@ -78,5 +62,85 @@ public sealed class ChatTurnDetails( /// /// Returns if response caching was disabled. /// - public bool? CacheHit { get; set; } = cacheHit; + public bool? CacheHit { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The duration between the time when the request was sent to the LLM and the time when the response was received + /// for the chat conversation turn. + /// + /// + /// The model that was used in the creation of the response for the chat conversation turn. Can be + /// if this information was not available via . + /// + /// + /// Usage details for the chat conversation turn (including input and output token counts). Can be + /// if usage details were not available via . + /// + /// + /// The cache key for the cached model response for the chat conversation turn if response caching was enabled; + /// otherwise. + /// + /// + /// if response caching was enabled and the model response for the chat conversation turn + /// was retrieved from the cache; if response caching was enabled and the model response + /// was not retrieved from the cache; if response caching was disabled. + /// + public ChatTurnDetails( + TimeSpan latency, + string? model = null, + UsageDetails? usage = null, + string? cacheKey = null, + bool? cacheHit = null) + : this(latency, model, modelProvider: null, usage, cacheKey, cacheHit) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The duration between the time when the request was sent to the LLM and the time when the response was received + /// for the chat conversation turn. + /// + /// + /// The model that was used in the creation of the response for the chat conversation turn. Can be + /// if this information was not available via . + /// + /// + /// The name of the provider for the model identified by . Can be + /// if this information was not available via the + /// property for the . + /// + /// + /// Usage details for the chat conversation turn (including input and output token counts). Can be + /// if usage details were not available via . + /// + /// + /// The cache key for the cached model response for the chat conversation turn if response caching was enabled; + /// otherwise. + /// + /// + /// if response caching was enabled and the model response for the chat conversation turn + /// was retrieved from the cache; if response caching was enabled and the model response + /// was not retrieved from the cache; if response caching was disabled. + /// + [JsonConstructor] + public ChatTurnDetails( + TimeSpan latency, + string? model, + string? modelProvider, + UsageDetails? usage = null, + string? cacheKey = null, + bool? cacheHit = null) + { + Latency = latency; + Model = model; + ModelProvider = modelProvider; + Usage = usage; + CacheKey = cacheKey; + CacheHit = cacheHit; + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json index 165fbd37d82..c5b0186f0bd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.AI.Evaluation.Reporting, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.AI.Evaluation.Reporting, Version=9.9.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ChatDetails", @@ -40,15 +40,16 @@ ] }, { - // After generating the baseline, manually edit this file to remove primary constructor portion - // This is needed until ICSharpCode.Decompiler adds support for primary constructors - // See: https://github.com/icsharpcode/ILSpy/issues/829 "Type": "sealed class Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails", "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.ChatTurnDetails(System.TimeSpan latency, string? model = null, Microsoft.Extensions.AI.UsageDetails? usage = null, string? cacheKey = null, bool? cacheHit = null);", "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.ChatTurnDetails(System.TimeSpan latency, string? model, string? modelProvider, Microsoft.Extensions.AI.UsageDetails? usage = null, string? cacheKey = null, bool? cacheHit = null);", + "Stage": "Stable" } ], "Properties": [ @@ -68,6 +69,10 @@ "Member": "string? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.Model { get; set; }", "Stage": "Stable" }, + { + "Member": "string? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.ModelProvider { get; set; }", + "Stage": "Stable" + }, { "Member": "Microsoft.Extensions.AI.UsageDetails? Microsoft.Extensions.AI.Evaluation.Reporting.ChatTurnDetails.Usage { get; set; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs index bc49d76e1be..7b53c229226 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs @@ -14,6 +14,7 @@ internal sealed class ResponseCachingChatClient : DistributedCachingChatClient { private readonly ChatDetails _chatDetails; private readonly ConcurrentDictionary _stopWatches; + private readonly ChatClientMetadata? _metadata; internal ResponseCachingChatClient( IChatClient originalChatClient, @@ -23,8 +24,10 @@ internal ResponseCachingChatClient( : base(originalChatClient, cache) { CacheKeyAdditionalValues = [.. cachingKeys]; + _chatDetails = chatDetails; _stopWatches = new ConcurrentDictionary(); + _metadata = this.GetService(); } protected override async Task ReadCacheAsync(string key, CancellationToken cancellationToken) @@ -45,6 +48,7 @@ internal ResponseCachingChatClient( new ChatTurnDetails( latency: stopwatch.Elapsed, model: response.ModelId, + modelProvider: _metadata?.ProviderName, usage: response.Usage, cacheKey: key, cacheHit: true)); @@ -75,6 +79,7 @@ internal ResponseCachingChatClient( new ChatTurnDetails( latency: stopwatch.Elapsed, model: response.ModelId, + modelProvider: _metadata?.ProviderName, usage: response.Usage, cacheKey: key, cacheHit: true)); @@ -95,6 +100,7 @@ protected override async Task WriteCacheAsync(string key, ChatResponse value, Ca new ChatTurnDetails( latency: stopwatch.Elapsed, model: value.ModelId, + modelProvider: _metadata?.ProviderName, usage: value.Usage, cacheKey: key, cacheHit: false)); @@ -117,6 +123,7 @@ protected override async Task WriteCacheStreamingAsync( new ChatTurnDetails( latency: stopwatch.Elapsed, model: response.ModelId, + modelProvider: _metadata?.ProviderName, usage: response.Usage, cacheKey: key, cacheHit: false)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs index 8ef344ab982..20b886eb720 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs @@ -12,11 +12,13 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; internal sealed class SimpleChatClient : DelegatingChatClient { private readonly ChatDetails _chatDetails; + private readonly ChatClientMetadata? _metadata; internal SimpleChatClient(IChatClient originalChatClient, ChatDetails chatDetails) : base(originalChatClient) { _chatDetails = chatDetails; + _metadata = this.GetService(); } public async override Task GetResponseAsync( @@ -41,6 +43,7 @@ public async override Task GetResponseAsync( new ChatTurnDetails( latency: stopwatch.Elapsed, model: response.ModelId, + modelProvider: _metadata?.ProviderName, usage: response.Usage)); } } @@ -78,6 +81,7 @@ public override async IAsyncEnumerable GetStreamingResponseA new ChatTurnDetails( latency: stopwatch.Elapsed, model: response.ModelId, + modelProvider: _metadata?.ProviderName, usage: response.Usage)); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ChatDetailsSection.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ChatDetailsSection.tsx index b05dcca1eae..545f181220d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ChatDetailsSection.tsx +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/ChatDetailsSection.tsx @@ -15,7 +15,8 @@ export const ChatDetailsSection = ({ chatDetails }: { chatDetails: ChatDetails; const hasCacheKey = chatDetails.turnDetails.some(turn => turn.cacheKey !== undefined); const hasCacheStatus = chatDetails.turnDetails.some(turn => turn.cacheHit !== undefined); - const hasModelInfo = chatDetails.turnDetails.some(turn => turn.model !== undefined); + const hasModel = chatDetails.turnDetails.some(turn => turn.model !== undefined); + const hasModelProvider = chatDetails.turnDetails.some(turn => turn.modelProvider !== undefined); const hasInputTokens = chatDetails.turnDetails.some(turn => turn.usage?.inputTokenCount !== undefined); const hasOutputTokens = chatDetails.turnDetails.some(turn => turn.usage?.outputTokenCount !== undefined); const hasTotalTokens = chatDetails.turnDetails.some(turn => turn.usage?.totalTokenCount !== undefined); @@ -42,13 +43,14 @@ export const ChatDetailsSection = ({ chatDetails }: { chatDetails: ChatDetails; {isExpanded && (
    - +
    {hasCacheKey && Cache Key} {hasCacheStatus && Cache Status} Latency (s) - {hasModelInfo && Model Used} + {hasModel && Model} + {hasModelProvider && Model Provider} {hasInputTokens && Input Tokens} {hasOutputTokens && Output Tokens} {hasTotalTokens && Total Tokens} @@ -92,7 +94,8 @@ export const ChatDetailsSection = ({ chatDetails }: { chatDetails: ChatDetails; )} {turn.latency.toFixed(2)} - {hasModelInfo && {turn.model || '-'}} + {hasModel && {turn.model || '-'}} + {hasModelProvider && {turn.modelProvider || '-'}} {hasInputTokens && {turn.usage?.inputTokenCount || '-'}} {hasOutputTokens && {turn.usage?.outputTokenCount || '-'}} {hasTotalTokens && {turn.usage?.totalTokenCount || '-'}} diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts index 2b6d84b6086..4d52836975c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/EvalTypes.d.ts @@ -39,6 +39,7 @@ type ChatDetails = { type ChatTurnDetails = { latency: number; model?: string; + modelProvider?: string; usage?: UsageDetails; cacheKey?: string; cacheHit?: boolean; diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ChatTurnDetailsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ChatTurnDetailsTests.cs new file mode 100644 index 00000000000..b97334a1057 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ChatTurnDetailsTests.cs @@ -0,0 +1,240 @@ +// 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.Extensions.AI.Evaluation.Reporting.JsonSerialization; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Reporting.Tests; + +public class ChatTurnDetailsTests +{ + [Fact] + public void DeserializeWithLatencyOnly() + { + string json = + """ + { + "latency": 5 + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(5), details!.Latency); + Assert.Null(details.Model); + Assert.Null(details.ModelProvider); + Assert.Null(details.Usage); + Assert.Null(details.CacheKey); + Assert.Null(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Null(deserializedDetails.Model); + Assert.Null(deserializedDetails.ModelProvider); + Assert.Null(deserializedDetails.Usage); + Assert.Null(deserializedDetails.CacheKey); + Assert.Null(deserializedDetails.CacheHit); + } + + [Fact] + public void DeserializeWithLatencyAndModel() + { + string json = + """ + { + "latency": 5, + "model": "gpt-4" + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(5), details!.Latency); + Assert.Equal("gpt-4", details.Model); + Assert.Null(details.ModelProvider); + Assert.Null(details.Usage); + Assert.Null(details.CacheKey); + Assert.Null(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Equal(details.Model, deserializedDetails!.Model); + Assert.Null(deserializedDetails.ModelProvider); + Assert.Null(deserializedDetails.Usage); + Assert.Null(deserializedDetails.CacheKey); + Assert.Null(deserializedDetails.CacheHit); + } + + [Fact] + public void DeserializeWithLatencyModelAndModelProvider() + { + string json = + """ + { + "latency": 5, + "model": "gpt-4", + "modelProvider": "azure.openai" + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(5), details!.Latency); + Assert.Equal("gpt-4", details.Model); + Assert.Equal("azure.openai", details.ModelProvider); + Assert.Null(details.Usage); + Assert.Null(details.CacheKey); + Assert.Null(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Equal(details.Model, deserializedDetails!.Model); + Assert.Equal(details.ModelProvider, deserializedDetails!.ModelProvider); + Assert.Null(deserializedDetails.Usage); + Assert.Null(deserializedDetails.CacheKey); + Assert.Null(deserializedDetails.CacheHit); + } + + [Fact] + public void DeserializeWithoutModelAndModelProvider() + { + string json = + """ + { + "latency": 1, + "usage": { "inputTokenCount": 10, "outputTokenCount": 20, "totalTokenCount": 30 }, + "cacheKey": "cache-key", + "cacheHit": true + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(1), details!.Latency); + Assert.Null(details.Model); + Assert.Null(details.ModelProvider); + Assert.NotNull(details.Usage); + Assert.Equal(10, details.Usage!.InputTokenCount); + Assert.Equal(20, details.Usage.OutputTokenCount); + Assert.Equal(30, details.Usage.TotalTokenCount); + Assert.Equal("cache-key", details.CacheKey); + Assert.True(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Null(deserializedDetails.Model); + Assert.Null(deserializedDetails.ModelProvider); + Assert.Equal(details.Usage!.InputTokenCount, deserializedDetails.Usage!.InputTokenCount); + Assert.Equal(details.Usage.OutputTokenCount, deserializedDetails.Usage.OutputTokenCount); + Assert.Equal(details.Usage.TotalTokenCount, deserializedDetails.Usage.TotalTokenCount); + Assert.Equal(details.CacheKey, deserializedDetails.CacheKey); + Assert.Equal(details.CacheHit, deserializedDetails.CacheHit); + } + + [Fact] + public void DeserializeWithoutModelProvider() + { + string json = + """ + { + "latency": 1, + "model": "gpt-4", + "usage": { "inputTokenCount": 10, "outputTokenCount": 20, "totalTokenCount": 30 }, + "cacheKey": "cache-key", + "cacheHit": true + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(1), details!.Latency); + Assert.Equal("gpt-4", details.Model); + Assert.Null(details.ModelProvider); + Assert.NotNull(details.Usage); + Assert.Equal(10, details.Usage!.InputTokenCount); + Assert.Equal(20, details.Usage.OutputTokenCount); + Assert.Equal(30, details.Usage.TotalTokenCount); + Assert.Equal("cache-key", details.CacheKey); + Assert.True(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Equal(details.Model, deserializedDetails.Model); + Assert.Null(deserializedDetails.ModelProvider); + Assert.Equal(details.Usage!.InputTokenCount, deserializedDetails.Usage!.InputTokenCount); + Assert.Equal(details.Usage.OutputTokenCount, deserializedDetails.Usage.OutputTokenCount); + Assert.Equal(details.Usage.TotalTokenCount, deserializedDetails.Usage.TotalTokenCount); + Assert.Equal(details.CacheKey, deserializedDetails.CacheKey); + Assert.Equal(details.CacheHit, deserializedDetails.CacheHit); + } + + [Fact] + public void DeserializeWithModelProvider() + { + string json = + """ + { + "latency": 2, + "model": "gpt-4", + "modelProvider": "azure.openai", + "usage": { "inputTokenCount": 5, "outputTokenCount": 7, "totalTokenCount": 12 }, + "cacheKey": "cache-key-2", + "cacheHit": false + } + """; + + JsonSerializerOptions options = JsonUtilities.Default.Options; + ChatTurnDetails? details = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(details); + Assert.Equal(TimeSpan.FromSeconds(2), details!.Latency); + Assert.Equal("gpt-4", details.Model); + Assert.Equal("azure.openai", details.ModelProvider); + Assert.NotNull(details.Usage); + Assert.Equal(5, details.Usage!.InputTokenCount); + Assert.Equal(7, details.Usage.OutputTokenCount); + Assert.Equal(12, details.Usage.TotalTokenCount); + Assert.Equal("cache-key-2", details.CacheKey); + Assert.False(details.CacheHit); + + string roundTripJson = JsonSerializer.Serialize(details, options); + ChatTurnDetails? deserializedDetails = JsonSerializer.Deserialize(roundTripJson, options); + + Assert.NotNull(deserializedDetails); + Assert.Equal(details.Latency, deserializedDetails!.Latency); + Assert.Equal(details.Model, deserializedDetails.Model); + Assert.Equal(details.ModelProvider, deserializedDetails.ModelProvider); + Assert.Equal(details.Usage!.InputTokenCount, deserializedDetails.Usage!.InputTokenCount); + Assert.Equal(details.Usage.OutputTokenCount, deserializedDetails.Usage.OutputTokenCount); + Assert.Equal(details.Usage.TotalTokenCount, deserializedDetails.Usage.TotalTokenCount); + Assert.Equal(details.CacheKey, deserializedDetails.CacheKey); + Assert.Equal(details.CacheHit, deserializedDetails.CacheHit); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs index b69014e631b..1f74f66136c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ResponseCacheTester.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +#pragma warning disable S1128 // Unused "using" should be removed using System.Linq; +#pragma warning restore S1128 using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; @@ -45,10 +47,12 @@ public async Task AddUncachedEntry() Assert.Null(cache.Get(_keyB)); await cache.SetAsync(_keyA, _responseA); - Assert.True(_responseA.SequenceEqual(await cache.GetAsync(_keyA) ?? [])); + byte[] cached = await cache.GetAsync(_keyA) ?? []; + Assert.True(_responseA.SequenceEqual(cached)); cache.Set(_keyB, _responseB); - Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); + cached = cache.Get(_keyB) ?? []; + Assert.True(_responseB.SequenceEqual(cached)); } [ConditionalFact] @@ -63,10 +67,12 @@ public async Task RemoveCachedEntry() Assert.NotNull(cache); await cache.SetAsync(_keyA, _responseA); - Assert.True(_responseA.SequenceEqual(await cache.GetAsync(_keyA) ?? [])); + byte[] cached = await cache.GetAsync(_keyA) ?? []; + Assert.True(_responseA.SequenceEqual(cached)); cache.Set(_keyB, _responseB); - Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); + cached = cache.Get(_keyB) ?? []; + Assert.True(_responseB.SequenceEqual(cached)); await cache.RemoveAsync(_keyA); Assert.Null(await cache.GetAsync(_keyA)); @@ -90,10 +96,12 @@ public async Task CacheEntryExpiration() Assert.NotNull(cache); await cache.SetAsync(_keyA, _responseA); - Assert.True(_responseA.SequenceEqual(await cache.GetAsync(_keyA) ?? [])); + byte[] cached = await cache.GetAsync(_keyA) ?? []; + Assert.True(_responseA.SequenceEqual(cached)); cache.Set(_keyB, _responseB); - Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); + cached = cache.Get(_keyB) ?? []; + Assert.True(_responseB.SequenceEqual(cached)); now = DateTime.UtcNow + Defaults.DefaultTimeToLiveForCacheEntries; @@ -138,10 +146,12 @@ public async Task DeleteExpiredEntries() Assert.NotNull(cache); await cache.SetAsync(_keyA, _responseA); - Assert.True(_responseA.SequenceEqual(await cache.GetAsync(_keyA) ?? [])); + byte[] cached = await cache.GetAsync(_keyA) ?? []; + Assert.True(_responseA.SequenceEqual(cached)); cache.Set(_keyB, _responseB); - Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); + cached = cache.Get(_keyB) ?? []; + Assert.True(_responseB.SequenceEqual(cached)); now = DateTime.UtcNow + Defaults.DefaultTimeToLiveForCacheEntries; @@ -168,10 +178,12 @@ public async Task ResetCache() Assert.NotNull(cache); await cache.SetAsync(_keyA, _responseA); - Assert.True(_responseA.SequenceEqual(await cache.GetAsync(_keyA) ?? [])); + byte[] cached = await cache.GetAsync(_keyA) ?? []; + Assert.True(_responseA.SequenceEqual(cached)); cache.Set(_keyB, _responseB); - Assert.True(_responseB.SequenceEqual(cache.Get(_keyB) ?? [])); + cached = cache.Get(_keyB) ?? []; + Assert.True(_responseB.SequenceEqual(cached)); await provider.ResetAsync(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ScenarioRunResultTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ScenarioRunResultTests.cs index f44b7187c2c..84324183091 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ScenarioRunResultTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/ScenarioRunResultTests.cs @@ -64,6 +64,7 @@ public void SerializeScenarioRunResult() new ChatTurnDetails( latency: TimeSpan.FromSeconds(1), model: "gpt-4o", + modelProvider: "openai", usage: new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20, TotalTokenCount = 30 }, cacheKey: Guid.NewGuid().ToString(), cacheHit: true); @@ -155,6 +156,7 @@ public void SerializeDatasetCompact() new ChatTurnDetails( latency: TimeSpan.FromSeconds(1), model: "gpt-4o", + modelProvider: "openai", usage: new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20, TotalTokenCount = 30 }, cacheKey: Guid.NewGuid().ToString(), cacheHit: true); @@ -392,6 +394,8 @@ private class ChatTurnDetailsComparer : IEqualityComparer #pragma warning disable S1067 // Expressions should not be too complex public bool Equals(ChatTurnDetails? x, ChatTurnDetails? y) => x?.Latency == y?.Latency && + x?.Model == y?.Model && + x?.ModelProvider == y?.ModelProvider && x?.Usage?.InputTokenCount == y?.Usage?.InputTokenCount && x?.Usage?.OutputTokenCount == y?.Usage?.OutputTokenCount && x?.Usage?.TotalTokenCount == y?.Usage?.TotalTokenCount && From 92cce3081c5a972210c0667ef3698079f9f9324f Mon Sep 17 00:00:00 2001 From: Yernar <47194841+ykumashev@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:21:14 +0200 Subject: [PATCH 264/472] Consider using Environment.CpuUsage in ResourceMonitoring (#6696) --- .../Windows/WindowsSnapshotProvider.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index e238a59aff9..c6f0e7519f5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +#if !NET9_0_OR_GREATER using System.Diagnostics; +#endif using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; @@ -95,18 +97,31 @@ internal WindowsSnapshotProvider( public Snapshot GetSnapshot() { +#if NET9_0_OR_GREATER + var cpuUsage = Environment.CpuUsage; + return new Snapshot( + totalTimeSinceStart: TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), + kernelTimeSinceStart: cpuUsage.PrivilegedTime, + userTimeSinceStart: cpuUsage.UserTime, + memoryUsageInBytes: (ulong)Environment.WorkingSet); +#else using var process = Process.GetCurrentProcess(); - - return new Snapshot(totalTimeSinceStart: TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), + return new Snapshot( + totalTimeSinceStart: TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), kernelTimeSinceStart: process.PrivilegedProcessorTime, userTimeSinceStart: process.UserProcessorTime, memoryUsageInBytes: (ulong)Environment.WorkingSet); +#endif } internal static long GetCpuTicks() { +#if NET9_0_OR_GREATER + return Environment.CpuUsage.TotalTime.Ticks; +#else using var process = Process.GetCurrentProcess(); return process.TotalProcessorTime.Ticks; +#endif } internal static int GetCpuUnits() => Environment.ProcessorCount; From 736edc6c21e82fb4cb96dac41bb8e720e7274c5c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 18 Aug 2025 20:43:59 -0400 Subject: [PATCH 265/472] Fix GetResponseAsync to look at the last message only (#6721) * Fix GetResponseAsync to look at the last message only It was looking at all messages in the whole response rather than just the last, which is where we expect the JSON to be. * update changelog --- .../Microsoft.Extensions.AI/CHANGELOG.md | 4 ++ .../ChatCompletion/ChatResponse{T}.cs | 2 +- ...atClientStructuredOutputExtensionsTests.cs | 44 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index dbba369f6df..213e3b8a60d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## NOT YET RELEASED + +- Fixed `GetResponseAsync` to only look at the contents of the last message in the response. + ## 9.8.0 - Added `FunctionInvokingChatClient.AdditionalTools` to allow `FunctionInvokingChatClient` to have access to tools not included in `ChatOptions.Tools` but known to the target service via pre-configuration. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs index 3756b255cc8..57c90307b67 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs @@ -127,7 +127,7 @@ public bool TryGetResult([NotNullWhen(true)] out T? result) return _deserializedResult; } - var json = Text; + var json = Messages.Count > 0 ? Messages[Messages.Count - 1].Text : string.Empty; if (string.IsNullOrEmpty(json)) { failureReason = FailureReason.ResultDidNotContainJson; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs index edd22edc41e..dcb372d0571 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs @@ -195,6 +195,50 @@ public async Task WrapsNonObjectValuesInDataProperty() Assert.Equal(123, response.Result); } + [Fact] + public async Task OnlyUsesLastMessage() + { + var expectedResult = new Envelope { data = 123 }; + var expectedResponse = new ChatResponse( + [ + new ChatMessage(ChatRole.Assistant, + [ + new TextContent("I'm going to invoke a function to get the data."), + new FunctionCallContent("callid123", "get_data"), + ]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callid123", "result")]), + new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, JsonContext2.Default.Options)) + ]); + + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, cancellationToken) => + { + var responseFormat = Assert.IsType(options!.ResponseFormat); + Assert.Equal(""" + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "data": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "data" + ] + } + """, responseFormat.Schema.ToString()); + return Task.FromResult(expectedResponse); + }, + }; + + var response = await client.GetResponseAsync("Hello"); + Assert.Equal(123, response.Result); + } + [Fact] public async Task FailureUsage_InvalidJson() { From 77859cb87c33bcda7187871a683c5779557cb2ef Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:02:02 +0200 Subject: [PATCH 266/472] Update dependencies from https://github.com/dotnet/arcade build 20250815.3 (#6728) Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Templating , Microsoft.DotNet.Helix.Sdk From Version 9.0.0-beta.25407.2 -> To Version 9.0.0-beta.25415.3 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- eng/common/SetupNugetSources.ps1 | 4 ++-- eng/common/SetupNugetSources.sh | 4 ++-- eng/common/core-templates/job/job.yml | 8 ++++---- eng/common/core-templates/job/onelocbuild.yml | 6 +++--- .../core-templates/job/publish-build-assets.yml | 10 ++++++---- .../core-templates/job/source-index-stage1.yml | 2 +- eng/common/core-templates/jobs/codeql-build.yml | 2 +- eng/common/core-templates/jobs/jobs.yml | 2 ++ .../core-templates/post-build/post-build.yml | 8 ++++---- .../post-build/setup-maestro-vars.yml | 2 +- .../steps/enable-internal-sources.yml | 12 ++++++------ eng/common/core-templates/steps/generate-sbom.yml | 2 +- eng/common/core-templates/steps/publish-logs.yml | 14 +++++++------- eng/common/core-templates/steps/source-build.yml | 4 ++-- eng/common/template-guidance.md | 2 +- eng/common/templates-official/job/job.yml | 2 +- .../templates-official/variables/sdl-variables.yml | 2 +- eng/common/templates/job/job.yml | 4 ++-- global.json | 4 ++-- 21 files changed, 56 insertions(+), 52 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1128ae0c403..89144ccd7fd 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - e29823691315ed6b3acff20d5bdf3b0be7628283 + d87d66c43d0660e5c8e84e667c5c8a8140bce888 - + https://github.com/dotnet/arcade - e29823691315ed6b3acff20d5bdf3b0be7628283 + d87d66c43d0660e5c8e84e667c5c8a8140bce888 - + https://github.com/dotnet/arcade - e29823691315ed6b3acff20d5bdf3b0be7628283 + d87d66c43d0660e5c8e84e667c5c8a8140bce888 diff --git a/eng/Versions.props b/eng/Versions.props index 888c2800987..0435109a781 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -83,7 +83,7 @@ 9.0.8 - 9.0.0-beta.25407.2 + 9.0.0-beta.25415.3 diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index 5db4ad71ee2..792b60b49d4 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -10,8 +10,8 @@ # displayName: Setup Private Feeds Credentials # condition: eq(variables['Agent.OS'], 'Windows_NT') # inputs: -# filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 -# arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token +# filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 +# arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token # env: # Token: $(dn-bot-dnceng-artifact-feeds-rw) # diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index 4604b61b032..facb415ca6f 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -11,8 +11,8 @@ # - task: Bash@3 # displayName: Setup Internal Feeds # inputs: -# filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh -# arguments: $(Build.SourcesDirectory)/NuGet.config +# filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.sh +# arguments: $(System.DefaultWorkingDirectory)/NuGet.config # condition: ne(variables['Agent.OS'], 'Windows_NT') # - task: NuGetAuthenticate@1 # diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index 8947ea3f059..8da43d3b583 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -166,7 +166,7 @@ jobs: inputs: languages: ${{ coalesce(parameters.richCodeNavigationLanguage, 'csharp') }} environment: ${{ coalesce(parameters.richCodeNavigationEnvironment, 'internal') }} - richNavLogOutputDirectory: $(Build.SourcesDirectory)/artifacts/bin + richNavLogOutputDirectory: $(System.DefaultWorkingDirectory)/artifacts/bin uploadRichNavArtifacts: ${{ coalesce(parameters.richCodeNavigationUploadArtifacts, false) }} continueOnError: true @@ -189,7 +189,7 @@ jobs: inputs: testResultsFormat: 'xUnit' testResultsFiles: '*.xml' - searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit mergeTestResults: ${{ parameters.mergeTestResults }} continueOnError: true @@ -200,7 +200,7 @@ jobs: inputs: testResultsFormat: 'VSTest' testResultsFiles: '*.trx' - searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx mergeTestResults: ${{ parameters.mergeTestResults }} continueOnError: true @@ -244,7 +244,7 @@ jobs: - task: CopyFiles@2 displayName: Gather buildconfiguration for build retry inputs: - SourceFolder: '$(Build.SourcesDirectory)/eng/common/BuildConfiguration' + SourceFolder: '$(System.DefaultWorkingDirectory)/eng/common/BuildConfiguration' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/eng/common/BuildConfiguration' continueOnError: true diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index 00feec8ebbc..edefa789d36 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -8,7 +8,7 @@ parameters: CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) - SourcesDirectory: $(Build.SourcesDirectory) + SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true AutoCompletePr: false ReusePr: true @@ -68,7 +68,7 @@ jobs: - ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}: - task: Powershell@2 inputs: - filePath: $(Build.SourcesDirectory)/eng/common/generate-locproject.ps1 + filePath: $(System.DefaultWorkingDirectory)/eng/common/generate-locproject.ps1 arguments: $(_GenerateLocProjectArguments) displayName: Generate LocProject.json condition: ${{ parameters.condition }} @@ -115,7 +115,7 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish LocProject.json - pathToPublish: '$(Build.SourcesDirectory)/eng/Localize/' + pathToPublish: '$(System.DefaultWorkingDirectory)/eng/Localize/' publishLocation: Container artifactName: Loc condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 3d3356e3196..b103b7ee168 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -32,6 +32,8 @@ parameters: is1ESPipeline: '' + repositoryAlias: self + jobs: - job: Asset_Registry_Publish @@ -72,7 +74,7 @@ jobs: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - checkout: self + - checkout: ${{ parameters.repositoryAlias }} fetchDepth: 3 clean: true @@ -93,7 +95,7 @@ jobs: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath - scriptPath: $(Build.SourcesDirectory)/eng/common/sdk-task.ps1 + scriptPath: $(System.DefaultWorkingDirectory)/eng/common/sdk-task.ps1 arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' /p:MaestroApiEndpoint=https://maestro.dot.net @@ -113,7 +115,7 @@ jobs: Add-Content -Path $filePath -Value "$(DefaultChannels)" Add-Content -Path $filePath -Value $(IsStableBuild) - $symbolExclusionfile = "$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt" + $symbolExclusionfile = "$(System.DefaultWorkingDirectory)/eng/SymbolPublishingExclusionsFile.txt" if (Test-Path -Path $symbolExclusionfile) { Write-Host "SymbolExclusionFile exists" @@ -142,7 +144,7 @@ jobs: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath - scriptPath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion 3 diff --git a/eng/common/core-templates/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml index 8b833332b3e..662b9fcce15 100644 --- a/eng/common/core-templates/job/source-index-stage1.yml +++ b/eng/common/core-templates/job/source-index-stage1.yml @@ -66,7 +66,7 @@ jobs: - script: ${{ parameters.sourceIndexBuildCommand }} displayName: Build Repository - - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(Build.SourcesDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output + - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output displayName: Process Binlog into indexable sln - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: diff --git a/eng/common/core-templates/jobs/codeql-build.yml b/eng/common/core-templates/jobs/codeql-build.yml index f2144252cc6..4571a7864df 100644 --- a/eng/common/core-templates/jobs/codeql-build.yml +++ b/eng/common/core-templates/jobs/codeql-build.yml @@ -25,7 +25,7 @@ jobs: - name: DefaultGuardianVersion value: 0.109.0 - name: GuardianPackagesConfigFile - value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config + value: $(System.DefaultWorkingDirectory)\eng\common\sdl\packages.config - name: GuardianVersion value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }} diff --git a/eng/common/core-templates/jobs/jobs.yml b/eng/common/core-templates/jobs/jobs.yml index ea69be4341c..3129670b338 100644 --- a/eng/common/core-templates/jobs/jobs.yml +++ b/eng/common/core-templates/jobs/jobs.yml @@ -43,6 +43,7 @@ parameters: artifacts: {} is1ESPipeline: '' + repositoryAlias: self # Internal resources (telemetry, microbuild) can only be accessed from non-public projects, # and some (Microbuild) should only be applied to non-PR cases for internal builds. @@ -117,3 +118,4 @@ jobs: enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} + repositoryAlias: ${{ parameters.repositoryAlias }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index a8c0bd3b921..2ee8bbfff54 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -149,7 +149,7 @@ stages: - task: PowerShell@2 displayName: Validate inputs: - filePath: $(Build.SourcesDirectory)/eng/common/post-build/nuget-validation.ps1 + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ - job: @@ -206,7 +206,7 @@ stages: filePath: eng\common\sdk-task.ps1 arguments: -task SigningValidation -restore -msbuildEngine vs /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' - /p:SignCheckExclusionsFile='$(Build.SourcesDirectory)/eng/SignCheckExclusionsFile.txt' + /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' ${{ parameters.signingValidationAdditionalParameters }} - template: /eng/common/core-templates/steps/publish-logs.yml @@ -256,7 +256,7 @@ stages: - task: PowerShell@2 displayName: Validate inputs: - filePath: $(Build.SourcesDirectory)/eng/common/post-build/sourcelink-validation.ps1 + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Extract/ -GHRepoName $(Build.Repository.Name) @@ -311,7 +311,7 @@ stages: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath - scriptPath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} diff --git a/eng/common/core-templates/post-build/setup-maestro-vars.yml b/eng/common/core-templates/post-build/setup-maestro-vars.yml index f7602980dbe..a7abd58c4bb 100644 --- a/eng/common/core-templates/post-build/setup-maestro-vars.yml +++ b/eng/common/core-templates/post-build/setup-maestro-vars.yml @@ -36,7 +36,7 @@ steps: $AzureDevOpsBuildId = $Env:Build_BuildId } else { - . $(Build.SourcesDirectory)\eng\common\tools.ps1 + . $(System.DefaultWorkingDirectory)\eng\common\tools.ps1 $darc = Get-Darc $buildInfo = & $darc get-build ` --id ${{ parameters.BARBuildId }} ` diff --git a/eng/common/core-templates/steps/enable-internal-sources.yml b/eng/common/core-templates/steps/enable-internal-sources.yml index 64f881bffc3..4085512b690 100644 --- a/eng/common/core-templates/steps/enable-internal-sources.yml +++ b/eng/common/core-templates/steps/enable-internal-sources.yml @@ -17,8 +17,8 @@ steps: - task: PowerShell@2 displayName: Setup Internal Feeds inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 - arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token env: Token: ${{ parameters.legacyCredential }} # If running on dnceng (internal project), just use the default behavior for NuGetAuthenticate. @@ -29,8 +29,8 @@ steps: - task: PowerShell@2 displayName: Setup Internal Feeds inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 - arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config + filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config - ${{ else }}: - template: /eng/common/templates/steps/get-federated-access-token.yml parameters: @@ -39,8 +39,8 @@ steps: - task: PowerShell@2 displayName: Setup Internal Feeds inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 - arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $(dnceng-artifacts-feeds-read-access-token) + filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $(dnceng-artifacts-feeds-read-access-token) # This is required in certain scenarios to install the ADO credential provider. # It installed by default in some msbuild invocations (e.g. VS msbuild), but needs to be installed for others # (e.g. dotnet msbuild). diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index 56a09009482..7f5b84c4cb8 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -6,7 +6,7 @@ parameters: PackageVersion: 9.0.0 - BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom IgnoreDirectories: '' diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 80788c52319..0623ac6e112 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -12,22 +12,22 @@ steps: inputs: targetType: inline script: | - New-Item -ItemType Directory $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ - Move-Item -Path $(Build.SourcesDirectory)/artifacts/log/Debug/* $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + New-Item -ItemType Directory $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + Move-Item -Path $(System.DefaultWorkingDirectory)/artifacts/log/Debug/* $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ continueOnError: true condition: always() - task: PowerShell@2 displayName: Redact Logs inputs: - filePath: $(Build.SourcesDirectory)/eng/common/post-build/redact-logs.ps1 + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/redact-logs.ps1 # For now this needs to have explicit list of all sensitive data. Taken from eng/publishing/v3/publish.yml - # Sensitive data can as well be added to $(Build.SourcesDirectory)/eng/BinlogSecretsRedactionFile.txt' + # Sensitive data can as well be added to $(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' # If the file exists - sensitive data for redaction will be sourced from it # (single entry per line, lines starting with '# ' are considered comments and skipped) - arguments: -InputPath '$(Build.SourcesDirectory)/PostBuildLogs' + arguments: -InputPath '$(System.DefaultWorkingDirectory)/PostBuildLogs' -BinlogToolVersion ${{parameters.BinlogToolVersion}} - -TokensFilePath '$(Build.SourcesDirectory)/eng/BinlogSecretsRedactionFile.txt' + -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' '$(publishing-dnceng-devdiv-code-r-build-re)' '$(MaestroAccessToken)' '$(dn-bot-all-orgs-artifact-feeds-rw)' @@ -42,7 +42,7 @@ steps: - task: CopyFiles@2 displayName: Gather post build logs inputs: - SourceFolder: '$(Build.SourcesDirectory)/PostBuildLogs' + SourceFolder: '$(System.DefaultWorkingDirectory)/PostBuildLogs' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/PostBuildLogs' diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index 37133b55b75..730f7ab2b67 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -97,7 +97,7 @@ steps: - task: CopyFiles@2 displayName: Prepare BuildLogs staging directory inputs: - SourceFolder: '$(Build.SourcesDirectory)' + SourceFolder: '$(System.DefaultWorkingDirectory)' Contents: | **/*.log **/*.binlog @@ -126,5 +126,5 @@ steps: parameters: displayName: Component Detection (Exclude upstream cache) is1ESPipeline: ${{ parameters.is1ESPipeline }} - componentGovernanceIgnoreDirectories: '$(Build.SourcesDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' + componentGovernanceIgnoreDirectories: '$(System.DefaultWorkingDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' disableComponentGovernance: ${{ eq(variables['System.TeamProject'], 'public') }} diff --git a/eng/common/template-guidance.md b/eng/common/template-guidance.md index 98bbc1ded0b..4bf4cf41bd7 100644 --- a/eng/common/template-guidance.md +++ b/eng/common/template-guidance.md @@ -50,7 +50,7 @@ extends: - task: CopyFiles@2 displayName: Gather build output inputs: - SourceFolder: '$(Build.SourcesDirectory)/artifacts/marvel' + SourceFolder: '$(System.DefaultWorkingDirectory)/artifacts/marvel' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/marvel' ``` diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml index 817555505aa..81ea7a261f2 100644 --- a/eng/common/templates-official/job/job.yml +++ b/eng/common/templates-official/job/job.yml @@ -3,7 +3,7 @@ parameters: enableSbom: true runAsPublic: false PackageVersion: 9.0.0 - BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' jobs: - template: /eng/common/core-templates/job/job.yml diff --git a/eng/common/templates-official/variables/sdl-variables.yml b/eng/common/templates-official/variables/sdl-variables.yml index dbdd66d4a4b..f1311bbb1b3 100644 --- a/eng/common/templates-official/variables/sdl-variables.yml +++ b/eng/common/templates-official/variables/sdl-variables.yml @@ -4,4 +4,4 @@ variables: - name: DefaultGuardianVersion value: 0.109.0 - name: GuardianPackagesConfigFile - value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config \ No newline at end of file + value: $(System.DefaultWorkingDirectory)\eng\common\sdl\packages.config \ No newline at end of file diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index d1aeb92fcea..5bdd3dd85fd 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -6,7 +6,7 @@ parameters: enableSbom: true runAsPublic: false PackageVersion: 9.0.0 - BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' jobs: - template: /eng/common/core-templates/job/job.yml @@ -75,7 +75,7 @@ jobs: parameters: is1ESPipeline: false args: - targetPath: '$(Build.SourcesDirectory)\eng\common\BuildConfiguration' + targetPath: '$(System.DefaultWorkingDirectory)\eng\common\BuildConfiguration' artifactName: 'BuildConfiguration' displayName: 'Publish build retry configuration' continueOnError: true diff --git a/global.json b/global.json index 8e8ecb1b1da..8a2286f473a 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25407.2", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25407.2" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25415.3", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25415.3" } } From 43bde7f6e8b6e8f66af1dbf5690d9aa6ee6df809 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:56:02 +0300 Subject: [PATCH 267/472] Fix server.address telemetry to use host only per OpenTelemetry semantic conventions (#6742) * Initial plan * Fix server.address telemetry to use host only per OpenTelemetry semantic conventions Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- .../ChatCompletion/OpenTelemetryChatClient.cs | 2 +- .../Embeddings/OpenTelemetryEmbeddingGenerator.cs | 2 +- .../ChatCompletion/OpenTelemetryChatClientTests.cs | 2 +- .../Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index a759aeea248..232475b428c 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -61,7 +61,7 @@ public OpenTelemetryChatClient(IChatClient innerClient, ILogger? logger = null, { _defaultModelId = metadata.DefaultModelId; _system = metadata.ProviderName; - _serverAddress = metadata.ProviderUri?.GetLeftPart(UriPartial.Path); + _serverAddress = metadata.ProviderUri?.Host; _serverPort = metadata.ProviderUri?.Port ?? 0; } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 87af9e52717..280e2a2ead3 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -59,7 +59,7 @@ public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator i _defaultModelId = metadata.DefaultModelId; _defaultModelDimensions = metadata.DefaultModelDimensions; _modelProvider = metadata.ProviderName; - _endpointAddress = metadata.ProviderUri?.GetLeftPart(UriPartial.Path); + _endpointAddress = metadata.ProviderUri?.Host; _endpointPort = metadata.ProviderUri?.Port ?? 0; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index b81a480e207..1cd849e3c14 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -151,7 +151,7 @@ async static IAsyncEnumerable CallbackAsync( Assert.NotNull(activity.Id); Assert.NotEmpty(activity.Id); - Assert.Equal("http://localhost:12345/something", activity.GetTagItem("server.address")); + Assert.Equal("localhost", activity.GetTagItem("server.address")); Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); Assert.Equal("chat replacementmodel", activity.DisplayName); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs index 418d11544a3..053f52a9d57 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs @@ -78,7 +78,7 @@ public async Task ExpectedInformationLogged_Async(string? perRequestModelId, boo Assert.NotNull(activity.Id); Assert.NotEmpty(activity.Id); - Assert.Equal("http://localhost:12345/something", activity.GetTagItem("server.address")); + Assert.Equal("localhost", activity.GetTagItem("server.address")); Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); Assert.Equal($"embeddings {expectedModelName}", activity.DisplayName); From da94b6045fef85f78a5eee183579221cee684e51 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 25 Aug 2025 09:04:51 -0400 Subject: [PATCH 268/472] Move `IChatReducer` to MEAI.Abstractions (#6738) --- .../ChatReduction/IChatReducer.cs | 0 .../ChatReduction/IChatReducer_Forwarder.cs | 7 +++++++ 2 files changed, 7 insertions(+) rename src/Libraries/{Microsoft.Extensions.AI => Microsoft.Extensions.AI.Abstractions}/ChatReduction/IChatReducer.cs (100%) create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer_Forwarder.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatReduction/IChatReducer.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatReduction/IChatReducer.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer_Forwarder.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer_Forwarder.cs new file mode 100644 index 00000000000..8a61f0c83be --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/IChatReducer_Forwarder.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; + +[assembly: TypeForwardedTo(typeof(IChatReducer))] From 81b24582150092198d026850e781d892ea07e32b Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Mon, 25 Aug 2025 15:45:17 -0700 Subject: [PATCH 269/472] Build ServiceDiscovery library and tests against netstandard2.0 (#10470) * Build ServiceDiscovery library and tests against .NET Framework This enables scenarios to use the library on .NET Framework. This is mostly done by using C# 14 new extension syntax so very little code changes were made but rather the APIs that didn't exist are added into FrameworkExtensions classes to light them up on .NET Core builds. * Add Dependency flow and condition new dependencies to only apply in netfx build. * Fix build break * conditionally inclue targets file * fix merge break * add netstandard2.0 * remove net462 * up the version for 9.0.8 in version.details.xml --------- Co-authored-by: Jose Perez Rodriguez --- ...sions.ServiceDiscovery.Abstractions.csproj | 10 +- ...crosoft.Extensions.ServiceDiscovery.csproj | 14 ++- ...iceDiscoveryHttpClientBuilderExtensions.cs | 12 ++- src/Shared/FxPolyfills/ArgumentException.cs | 28 ++++++ .../FxPolyfills/ArgumentNullException.cs | 24 +++++ .../CallerArgumentExpressionAttribute.cs | 10 ++ .../FxPolyfills/ConcurrentDictionary.cs | 43 +++++++++ .../FxPolyfills/ExceptionDispatchInfo.cs | 18 ++++ src/Shared/FxPolyfills/FxPolyfills.targets | 23 +++++ src/Shared/FxPolyfills/IPEndPoint.cs | 59 ++++++++++++ src/Shared/FxPolyfills/Interlocked.cs | 94 +++++++++++++++++++ src/Shared/FxPolyfills/IsExternalInit.cs | 8 ++ src/Shared/FxPolyfills/KeyValuePair.cs | 24 +++++ .../FxPolyfills/ObjectDisposedException.cs | 20 ++++ src/Shared/FxPolyfills/OperatingSystem.cs | 14 +++ src/Shared/FxPolyfills/String.cs | 12 +++ src/Shared/FxPolyfills/Task.TimeProvider.cs | 15 +++ src/Shared/FxPolyfills/Task.cs | 54 +++++++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 11 ++- .../ServiceEndpointResolverTests.cs | 2 + 20 files changed, 484 insertions(+), 11 deletions(-) create mode 100644 src/Shared/FxPolyfills/ArgumentException.cs create mode 100644 src/Shared/FxPolyfills/ArgumentNullException.cs create mode 100644 src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs create mode 100644 src/Shared/FxPolyfills/ConcurrentDictionary.cs create mode 100644 src/Shared/FxPolyfills/ExceptionDispatchInfo.cs create mode 100644 src/Shared/FxPolyfills/FxPolyfills.targets create mode 100644 src/Shared/FxPolyfills/IPEndPoint.cs create mode 100644 src/Shared/FxPolyfills/Interlocked.cs create mode 100644 src/Shared/FxPolyfills/IsExternalInit.cs create mode 100644 src/Shared/FxPolyfills/KeyValuePair.cs create mode 100644 src/Shared/FxPolyfills/ObjectDisposedException.cs create mode 100644 src/Shared/FxPolyfills/OperatingSystem.cs create mode 100644 src/Shared/FxPolyfills/String.cs create mode 100644 src/Shared/FxPolyfills/Task.TimeProvider.cs create mode 100644 src/Shared/FxPolyfills/Task.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 6f412779689..061e85b44d3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,9 +1,9 @@ - $(DefaultTargetFramework) + $(DefaultTargetFramework);netstandard2.0 true - true + true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery @@ -19,4 +19,10 @@ + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 59c1c31fee9..72f8f4f9051 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,21 +1,27 @@ - $(DefaultTargetFramework) + netstandard2.0;$(DefaultTargetFramework) true - true + true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) - - + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index 7d5b94c10c5..d2890ae8c8d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -1,12 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Http; +#if NET +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +#endif + namespace Microsoft.Extensions.DependencyInjection; /// @@ -34,13 +37,15 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt return new ResolvingHttpDelegatingHandler(registry, options); }); +#if NET // Configure the HttpClient to disable gRPC load balancing. // This is done on all HttpClient instances but only impacts gRPC clients. AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); - +#endif return httpClientBuilder; } +#if NET private static void AddDisableGrpcLoadBalancingFilter(IServiceCollection services, string? name) { // A filter is used because it will always run last. This is important because the disable @@ -86,4 +91,5 @@ public Action Configure(Action throw new ArgumentNullException(paramName); +} diff --git a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000000..6d82b4a25c9 --- /dev/null +++ b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute +{ + public string ParameterName => parameterName; +} diff --git a/src/Shared/FxPolyfills/ConcurrentDictionary.cs b/src/Shared/FxPolyfills/ConcurrentDictionary.cs new file mode 100644 index 00000000000..92e4a2195df --- /dev/null +++ b/src/Shared/FxPolyfills/ConcurrentDictionary.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Concurrent; + +internal static partial class FxPolyfillConcurrentDictionary +{ + extension(ConcurrentDictionary dictionary) + { + public TValue GetOrAdd(TKey key, Func valueFactory) + { + if (dictionary.TryGetValue(key, out var existing)) + { + return existing; + } + + return dictionary.GetOrAdd(key, valueFactory(key)); + } + + public TValue GetOrAdd(TKey key, Func valueFactory, TState state) + { + if (dictionary.TryGetValue(key, out var existing)) + { + return existing; + } + + return dictionary.GetOrAdd(key, valueFactory(key, state)); + } + + public void TryRemove(TKey key) + { + dictionary.TryRemove(key, out _); + } + + public void TryRemove(KeyValuePair pair) + { + if (dictionary.TryRemove(pair.Key, out var existing) && !EqualityComparer.Default.Equals(existing, pair.Value)) + { + dictionary.TryAdd(pair.Key, pair.Value); + } + } + } +} diff --git a/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs b/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs new file mode 100644 index 00000000000..81cee7cba9a --- /dev/null +++ b/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.ExceptionServices; + +internal static partial class FxPolyfillExceptionDispatchInfo +{ + extension(ExceptionDispatchInfo) + { + [DoesNotReturn] + public static void Throw(Exception ex) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + } +} diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets new file mode 100644 index 00000000000..ea9db526b69 --- /dev/null +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -0,0 +1,23 @@ + + + $(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + diff --git a/src/Shared/FxPolyfills/IPEndPoint.cs b/src/Shared/FxPolyfills/IPEndPoint.cs new file mode 100644 index 00000000000..8571b675bb5 --- /dev/null +++ b/src/Shared/FxPolyfills/IPEndPoint.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace System.Net; + +internal static partial class FxPolyfillIPEndPoint +{ + extension(IPEndPoint) + { + public static IPEndPoint Parse(string endpoint) + { + if (TryParse(endpoint.AsSpan(), out var result)) + { + return result; + } + + throw new FormatException("The endpoint format is invalid."); + } + + public static bool TryParse(ReadOnlySpan s, out IPEndPoint? result) + { + const int MaxPort = 0x0000FFFF; + + int addressLength = s.Length; // If there's no port then send the entire string to the address parser + int lastColonPos = s.LastIndexOf(':'); + + // Look to see if this is an IPv6 address with a port. + if (lastColonPos > 0) + { + if (s[lastColonPos - 1] == ']') + { + addressLength = lastColonPos; + } + // Look to see if this is IPv4 with a port (IPv6 will have another colon) + else if (s.Slice(0, lastColonPos).LastIndexOf(':') == -1) + { + addressLength = lastColonPos; + } + } + + if (IPAddress.TryParse(s.Slice(0, addressLength).ToString(), out IPAddress? address)) + { + uint port = 0; + if (addressLength == s.Length || + (uint.TryParse(s.Slice(addressLength + 1).ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort)) + + { + result = new IPEndPoint(address, (int)port); + return true; + } + } + + result = null; + return false; + } + } +} diff --git a/src/Shared/FxPolyfills/Interlocked.cs b/src/Shared/FxPolyfills/Interlocked.cs new file mode 100644 index 00000000000..6177e411c35 --- /dev/null +++ b/src/Shared/FxPolyfills/Interlocked.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Threading; + +internal static partial class FxPolyfillInterlocked +{ + extension(Interlocked) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Decrement(ref uint location) => + (uint)Interlocked.Add(ref Unsafe.As(ref location), -1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Decrement(ref ulong location) => + (ulong)Interlocked.Add(ref Unsafe.As(ref location), -1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Increment(ref uint location) => + Add(ref location, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Increment(ref ulong location) => + Add(ref location, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Add(ref uint location1, uint value) => + (uint)Interlocked.Add(ref Unsafe.As(ref location1), (int)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Add(ref ulong location1, ulong value) => + (ulong)Interlocked.Add(ref Unsafe.As(ref location1), (long)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Or(ref long location1, long value) + { + long current = location1; + while (true) + { + long newValue = current | value; + long oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong And(ref ulong location1, ulong value) => + (ulong)Interlocked.And(ref Unsafe.As(ref location1), (long)value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint And(ref uint location1, uint value) => + (uint)Interlocked.And(ref Unsafe.As(ref location1), (int)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int And(ref int location1, int value) + { + int current = location1; + while (true) + { + int newValue = current & value; + int oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long And(ref long location1, long value) + { + long current = location1; + while (true) + { + long newValue = current & value; + long oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Or(ref ulong location1, ulong value) => + (ulong)Or(ref Unsafe.As(ref location1), (long)value); + } +} diff --git a/src/Shared/FxPolyfills/IsExternalInit.cs b/src/Shared/FxPolyfills/IsExternalInit.cs new file mode 100644 index 00000000000..f2bac777b13 --- /dev/null +++ b/src/Shared/FxPolyfills/IsExternalInit.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ +} diff --git a/src/Shared/FxPolyfills/KeyValuePair.cs b/src/Shared/FxPolyfills/KeyValuePair.cs new file mode 100644 index 00000000000..64c79606bf4 --- /dev/null +++ b/src/Shared/FxPolyfills/KeyValuePair.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Generic; + +internal static partial class FxPolyfillKeyValuePair +{ + extension(KeyValuePair pair) + { + public void Deconstruct(out TKey key, out TValue value) + { + key = pair.Key; + value = pair.Value; + } + } +} + +internal static class KeyValuePair +{ + public static KeyValuePair Create(TKey key, TValue value) + { + return new KeyValuePair(key, value); + } +} diff --git a/src/Shared/FxPolyfills/ObjectDisposedException.cs b/src/Shared/FxPolyfills/ObjectDisposedException.cs new file mode 100644 index 00000000000..85ec090dd6c --- /dev/null +++ b/src/Shared/FxPolyfills/ObjectDisposedException.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System; + +internal static partial class FxPolyfillObjectDisposedException +{ + extension(ObjectDisposedException) + { + public static void ThrowIf([DoesNotReturnIf(true)] bool condition, object instance) + { + if (condition) + { + throw new ObjectDisposedException(instance?.GetType().FullName); + } + } + } +} diff --git a/src/Shared/FxPolyfills/OperatingSystem.cs b/src/Shared/FxPolyfills/OperatingSystem.cs new file mode 100644 index 00000000000..4c88e7909aa --- /dev/null +++ b/src/Shared/FxPolyfills/OperatingSystem.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +internal static class FrameworkExtensions +{ + extension(OperatingSystem) + { + public static bool IsLinux() => false; + public static bool IsWindows() => true; + public static bool IsMacOS() => false; + } +} diff --git a/src/Shared/FxPolyfills/String.cs b/src/Shared/FxPolyfills/String.cs new file mode 100644 index 00000000000..92df065be7e --- /dev/null +++ b/src/Shared/FxPolyfills/String.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +internal static partial class FxPolyfillString +{ + extension(string s) + { + public bool StartsWith(char c) => s is [{ } first, ..] && first == c; + } +} diff --git a/src/Shared/FxPolyfills/Task.TimeProvider.cs b/src/Shared/FxPolyfills/Task.TimeProvider.cs new file mode 100644 index 00000000000..7e3a9c85bf0 --- /dev/null +++ b/src/Shared/FxPolyfills/Task.TimeProvider.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks; + +internal static partial class FxPolyfillTask +{ + extension(Task task) + { + public Task WaitAsync(CancellationToken token) + { + return task.WaitAsync(Timeout.InfiniteTimeSpan, TimeProvider.System, token); + } + } +} diff --git a/src/Shared/FxPolyfills/Task.cs b/src/Shared/FxPolyfills/Task.cs new file mode 100644 index 00000000000..0035cde4b6f --- /dev/null +++ b/src/Shared/FxPolyfills/Task.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks; + +internal enum ConfigureAwaitOptions +{ + None, + ContinueOnCapturedContext, + ForceYielding, + SuppressThrowing, +} + +internal static partial class FxPolyfillTask +{ + extension(Task task) + { + public async Task ConfigureAwait(ConfigureAwaitOptions options) + { + if (options == ConfigureAwaitOptions.None) + { + await task.ConfigureAwait(false); + } + else if (options == ConfigureAwaitOptions.ContinueOnCapturedContext) + { + await task.ConfigureAwait(true); + } + else if (options == ConfigureAwaitOptions.ForceYielding) + { + await Task.Yield(); + await task.ConfigureAwait(false); + } + else if (options == ConfigureAwaitOptions.SuppressThrowing) + { + try + { + await task.ConfigureAwait(false); + } + catch + { + } + } + else + { + throw new InvalidOperationException(); + } + } + } +} + +internal sealed class TaskCompletionSource(TaskCreationOptions options) : TaskCompletionSource(options) +{ + public void SetResult() => SetResult(true); +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 269081ae5d7..20147d5d465 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,11 +1,19 @@ - $(DefaultTargetFramework) + $(DefaultTargetFramework) + $(TargetFrameworks);net472 enable enable + + + + + + + @@ -15,5 +23,4 @@ - diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index 0e08c07271e..c91f07c9300 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -66,7 +66,9 @@ public async Task AddServiceDiscovery_NoProviders_Throws() private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { +#pragma warning disable CS0436 // Type conflicts with imported type public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) +#pragma warning restore CS0436 // Type conflicts with imported type { bool result; (result, resolver) = createResolverDelegate(query); From ec3b31d717a05bfa5ea1ea084ff33d15430a6cdf Mon Sep 17 00:00:00 2001 From: Shyam N Date: Wed, 27 Aug 2025 19:54:27 -0700 Subject: [PATCH 270/472] Introduce some helpers that perform heuristic matches to detect well known model hosts (#6751) --- ...ft.Extensions.AI.Evaluation.Console.csproj | 1 + .../CSharp/ChatTurnDetails.cs | 10 +- ....Extensions.AI.Evaluation.Reporting.csproj | 4 + .../CSharp/ResponseCachingChatClient.cs | 29 ++- .../CSharp/SimpleChatClient.cs | 15 +- .../ContentSafetyChatClient.cs | 30 ++- ...oft.Extensions.AI.Evaluation.Safety.csproj | 1 + .../Utilities/ModelInfo.cs | 131 ++++++++++ .../AgentQualityEvaluatorTests.cs | 6 +- .../QualityEvaluatorTests.cs | 6 +- .../SafetyEvaluatorTests.cs | 21 +- .../ModelInfoTests.cs | 226 ++++++++++++++++++ 12 files changed, 436 insertions(+), 44 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/ModelInfo.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/ModelInfoTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj index 835d7f7c1e6..e0dbc06d9df 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Console/Microsoft.Extensions.AI.Evaluation.Console.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs index e8d8a82997f..f9c93995224 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs @@ -35,8 +35,8 @@ public sealed class ChatTurnDetails /// Gets or sets the name of the provider for the model identified by . /// /// - /// Returns if this information was not available via the - /// property for the . + /// Can be if this information was not available via the + /// for the . /// public string? ModelProvider { get; set; } @@ -110,9 +110,9 @@ public ChatTurnDetails( /// if this information was not available via . /// /// - /// The name of the provider for the model identified by . Can be - /// if this information was not available via the - /// property for the . + /// The name of the provider for the model identified by . Can be + /// if this information was not available via the for the + /// . /// /// /// Usage details for the chat conversation turn (including input and output token counts). Can be diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj index 8a9ef12f2bd..77a492d154f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj @@ -23,6 +23,10 @@ n/a + + + + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs index 7b53c229226..c983cb87a03 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Utilities; using Microsoft.Extensions.Caching.Distributed; namespace Microsoft.Extensions.AI.Evaluation.Reporting; @@ -44,11 +45,14 @@ internal ResponseCachingChatClient( { stopwatch.Stop(); + string? model = response.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: response.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: response.Usage, cacheKey: key, cacheHit: true)); @@ -75,11 +79,14 @@ internal ResponseCachingChatClient( stopwatch.Stop(); ChatResponse response = updates.ToChatResponse(); + string? model = response.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: response.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: response.Usage, cacheKey: key, cacheHit: true)); @@ -96,11 +103,14 @@ protected override async Task WriteCacheAsync(string key, ChatResponse value, Ca { stopwatch.Stop(); + string? model = value.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: value.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: value.Usage, cacheKey: key, cacheHit: false)); @@ -119,11 +129,14 @@ protected override async Task WriteCacheStreamingAsync( stopwatch.Stop(); ChatResponse response = value.ToChatResponse(); + string? model = response.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: response.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: response.Usage, cacheKey: key, cacheHit: false)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs index 20b886eb720..875d6a6c26a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/SimpleChatClient.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Utilities; namespace Microsoft.Extensions.AI.Evaluation.Reporting; @@ -39,11 +40,14 @@ public async override Task GetResponseAsync( if (response is not null) { + string? model = response.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: response.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: response.Usage)); } } @@ -77,11 +81,14 @@ public override async IAsyncEnumerable GetStreamingResponseA if (updates is not null) { ChatResponse response = updates.ToChatResponse(); + string? model = response.ModelId; + string? modelProvider = ModelInfo.GetModelProvider(model, _metadata); + _chatDetails.AddTurnDetails( new ChatTurnDetails( latency: stopwatch.Elapsed, - model: response.ModelId, - modelProvider: _metadata?.ProviderName, + model, + modelProvider, usage: response.Usage)); } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs index 60d87cf700d..1669bb7df6b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs @@ -14,15 +14,13 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Evaluation.Utilities; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Evaluation.Safety; internal sealed class ContentSafetyChatClient : IChatClient { - private const string ProviderName = "azure.ai.foundry"; - private const string ModelId = $"{ProviderName}.evaluation"; - private readonly ContentSafetyService _service; private readonly IChatClient? _originalChatClient; private readonly ChatClientMetadata _metadata; @@ -35,15 +33,23 @@ public ContentSafetyChatClient( _originalChatClient = originalChatClient; ChatClientMetadata? originalMetadata = _originalChatClient?.GetService(); - - string providerName = ProviderName; - if (originalMetadata?.ProviderName is string originalProviderName && - !string.IsNullOrWhiteSpace(originalProviderName)) + if (originalMetadata is null) { - providerName = $"{providerName}; {originalProviderName}"; + _metadata = + new ChatClientMetadata( + providerName: ModelInfo.KnownModelProviders.AzureAIFoundry, + defaultModelId: ModelInfo.KnownModels.AzureAIFoundryEvaluation); + } + else + { + // If we are wrapping an existing client, prefer its metadata. Preserving the metadata of the inner client + // (when available) ensures that the contained information remains available for requests that are + // delegated to the inner client and serviced by an LLM endpoint. For requests that are not delegated, the + // ChatResponse.ModelId for the produced response would be sufficient to identify that the model used was + // the finetuned model provided by the Azure AI Foundry Evaluation service (even though the outer client's + // metadata will not reflect this). + _metadata = originalMetadata; } - - _metadata = new ChatClientMetadata(providerName, defaultModelId: ModelId); } public async Task GetResponseAsync( @@ -65,7 +71,7 @@ await _service.AnnotateAsync( return new ChatResponse(new ChatMessage(ChatRole.Assistant, annotationResult)) { - ModelId = ModelId + ModelId = ModelInfo.KnownModels.AzureAIFoundryEvaluation }; } else @@ -98,7 +104,7 @@ await _service.AnnotateAsync( yield return new ChatResponseUpdate(ChatRole.Assistant, annotationResult) { - ModelId = ModelId + ModelId = ModelInfo.KnownModels.AzureAIFoundryEvaluation }; } else diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj index cc458103480..b1ac49dce28 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/Microsoft.Extensions.AI.Evaluation.Safety.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/ModelInfo.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/ModelInfo.cs new file mode 100644 index 00000000000..b5c74212401 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/Utilities/ModelInfo.cs @@ -0,0 +1,131 @@ +// 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.RegularExpressions; + +namespace Microsoft.Extensions.AI.Evaluation.Utilities; + +internal static class ModelInfo +{ + internal static class KnownModels + { + internal const string AzureAIFoundryEvaluation = "azure.ai.foundry.evaluation"; + } + + internal static class KnownModelProviders + { + internal const string AzureAIFoundry = "azure.ai.foundry"; + } + + internal static class KnownModelHostMonikers + { + internal const string LocalMachine = "local"; + internal const string AzureAIFoundry = "azure.ai.foundry"; + internal const string AzureOpenAI = "azure.openai"; + internal const string AzureML = "azure.ml"; + internal const string GitHubModels = "github.models"; + internal const string Azure = "azure"; + internal const string GitHub = "github"; + internal const string Microsoft = "microsoft"; + } + + private const string LocalMachineHost = "localhost"; + + private static Regex LocalMachineHostMonikerRegex { get; } = + new Regex($"\\({Regex.Escape(KnownModelHostMonikers.LocalMachine)}\\)$"); + + // NOTE: Order more specific patterns first. + private static (string hostPattern, string hostMoniker)[] KnownHostMonikers { get; } = + [ + ("services.ai.azure.", KnownModelHostMonikers.AzureAIFoundry), + ("openai.azure.", KnownModelHostMonikers.AzureOpenAI), + ("ml.azure.", KnownModelHostMonikers.AzureML), + ("models.github.ai", KnownModelHostMonikers.GitHubModels), + ("models.inference.ai.azure.", KnownModelHostMonikers.GitHubModels), + (".azure.", KnownModelHostMonikers.Azure), + (".github.", KnownModelHostMonikers.GitHub), + (".microsoft.", KnownModelHostMonikers.Microsoft) + ]; + + private static Regex KnownHostMonikersRegex { get; } = + new Regex( + $"\\((" + + $"{Regex.Escape(KnownModelHostMonikers.AzureAIFoundry)}|" + + $"{Regex.Escape(KnownModelHostMonikers.AzureOpenAI)}|" + + $"{Regex.Escape(KnownModelHostMonikers.AzureML)}|" + + $"{Regex.Escape(KnownModelHostMonikers.GitHubModels)}|" + + $"{Regex.Escape(KnownModelHostMonikers.Azure)}|" + + $"{Regex.Escape(KnownModelHostMonikers.GitHub)}|" + + $"{Regex.Escape(KnownModelHostMonikers.Microsoft)}" + + $")\\)$"); + + /// + /// Returns a string with format {provider} ({host}) where {provider} is the name of the model + /// provider (available via - for example, openai) and + /// {host} is a moniker that identifies the hosting service (for example, azure.openai or + /// github.models). If the hosting service is not recognized, only the name of the model provider is + /// returned. + /// + /// + /// The that identifies the model that produced a particular response. + /// + /// + /// The for the that was used to communicate with the + /// model. + /// + internal static string? GetModelProvider(string? model, ChatClientMetadata? metadata) + { +#pragma warning disable S2219 // Runtime type checking should be simplified + if (model is KnownModels.AzureAIFoundryEvaluation) +#pragma warning restore S2219 + { + // We know that the model provider and the host are both Azure AI Foundry in this case. + return $"{KnownModelProviders.AzureAIFoundry} ({KnownModelHostMonikers.AzureAIFoundry})"; + } + + if (metadata is null) + { + return null; + } + + string? provider = metadata.ProviderName; + string? host = metadata.ProviderUri?.Host; + + if (!string.IsNullOrWhiteSpace(host)) + { + if (string.Equals(host, LocalMachineHost, StringComparison.OrdinalIgnoreCase)) + { + return $"{provider} ({KnownModelHostMonikers.LocalMachine})"; + } + + foreach (var (hostPattern, hostMoniker) in KnownHostMonikers) + { +#if NET + if (host.Contains(hostPattern, StringComparison.OrdinalIgnoreCase)) +#else + if (host!.IndexOf(hostPattern, StringComparison.OrdinalIgnoreCase) >= 0) +#endif + { + return $"{provider} ({hostMoniker})"; + } + } + } + + return provider; + } + + /// + /// Returns if the specified indicates that the model is + /// hosted by a well-known (Microsoft-owned) service; otherwise. + /// + internal static bool IsModelHostWellKnown(string? modelProvider) + => !string.IsNullOrWhiteSpace(modelProvider) && KnownHostMonikersRegex.IsMatch(modelProvider); + + /// + /// Returns if the specified indicates that the model is + /// hosted locally (using ollama, for example); otherwise. + /// + internal static bool IsModelHostedLocally(string? modelProvider) + => !string.IsNullOrWhiteSpace(modelProvider) && LocalMachineHostMonikerRegex.IsMatch(modelProvider); +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs index 134d6a50f32..238b805e867 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/AgentQualityEvaluatorTests.cs @@ -54,8 +54,8 @@ static AgentQualityEvaluatorTests() string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; string projectName = $"Project: Integration Tests"; string testClass = $"Test Class: {nameof(AgentQualityEvaluatorTests)}"; - string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; string temperature = $"Temperature: {_chatOptionsWithTools.Temperature}"; string usesContext = $"Feature: Context"; @@ -69,7 +69,7 @@ static AgentQualityEvaluatorTests() evaluators: [taskAdherenceEvaluator, intentResolutionEvaluator], chatConfiguration: chatConfigurationWithToolCalling, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature]); + tags: [version, date, projectName, testClass, model, provider, temperature]); _needsContextReportingConfiguration = DiskBasedReportingConfiguration.Create( @@ -77,7 +77,7 @@ static AgentQualityEvaluatorTests() evaluators: [toolCallAccuracyEvaluator, taskAdherenceEvaluator, intentResolutionEvaluator], chatConfiguration: chatConfigurationWithToolCalling, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); + tags: [version, date, projectName, testClass, model, provider, temperature, usesContext]); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs index ecec3ad51e5..c867fca4926 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/QualityEvaluatorTests.cs @@ -43,8 +43,8 @@ static QualityEvaluatorTests() string date = $"Date: {DateTime.UtcNow:dddd, dd MMMM yyyy}"; string projectName = $"Project: Integration Tests"; string testClass = $"Test Class: {nameof(QualityEvaluatorTests)}"; - string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; string temperature = $"Temperature: {_chatOptions.Temperature}"; string usesContext = $"Feature: Context"; @@ -60,7 +60,7 @@ static QualityEvaluatorTests() evaluators: [rtcEvaluator, coherenceEvaluator, fluencyEvaluator, relevanceEvaluator], chatConfiguration: chatConfiguration, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature,]); + tags: [version, date, projectName, testClass, model, provider, temperature]); IEvaluator groundednessEvaluator = new GroundednessEvaluator(); IEvaluator equivalenceEvaluator = new EquivalenceEvaluator(); @@ -73,7 +73,7 @@ static QualityEvaluatorTests() evaluators: [groundednessEvaluator, equivalenceEvaluator, completenessEvaluator, retrievalEvaluator], chatConfiguration: chatConfiguration, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); + tags: [version, date, projectName, testClass, model, provider, temperature, usesContext]); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs index c0f955b8416..68b4a9d8ce0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/SafetyEvaluatorTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.AI.Evaluation.Reporting.Storage; using Microsoft.Extensions.AI.Evaluation.Safety; using Microsoft.Extensions.AI.Evaluation.Tests; +using Microsoft.Extensions.AI.Evaluation.Utilities; using Microsoft.TestUtilities; using Xunit; @@ -58,8 +59,10 @@ static SafetyEvaluatorTests() ChatClientMetadata? clientMetadata = contentSafetyChatConfiguration.ChatClient.GetService(); - string provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; - string model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + const string Model = $"Model: {ModelInfo.KnownModels.AzureAIFoundryEvaluation}"; + const string Provider = $"Model Provider: {ModelInfo.KnownModelProviders.AzureAIFoundry}"; + string model2 = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + string provider2 = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; IEvaluator hateAndUnfairnessEvaluator = new HateAndUnfairnessEvaluator(); IEvaluator selfHarmEvaluator = new SelfHarmEvaluator(); @@ -82,7 +85,7 @@ static SafetyEvaluatorTests() indirectAttackEvaluator], chatConfiguration: contentSafetyChatConfiguration, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); + tags: [version, date, projectName, testClass, Model, Provider, model2, provider2, temperature, usesContext]); ChatConfiguration contentSafetyChatConfigurationWithoutLLM = contentSafetyServiceConfiguration.ToChatConfiguration(); @@ -97,7 +100,7 @@ static SafetyEvaluatorTests() indirectAttackEvaluator], chatConfiguration: contentSafetyChatConfigurationWithoutLLM, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature]); + tags: [version, date, projectName, testClass, Model, Provider, model2, provider2, temperature]); IEvaluator codeVulnerabilityEvaluator = new CodeVulnerabilityEvaluator(); @@ -107,7 +110,7 @@ static SafetyEvaluatorTests() evaluators: [codeVulnerabilityEvaluator], chatConfiguration: contentSafetyChatConfigurationWithoutLLM, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature]); + tags: [version, date, projectName, testClass, Model, Provider, model2, provider2, temperature]); IEvaluator fluencyEvaluator = new FluencyEvaluator(); IEvaluator contentHarmEvaluator = new ContentHarmEvaluator(); @@ -118,7 +121,7 @@ static SafetyEvaluatorTests() evaluators: [fluencyEvaluator, contentHarmEvaluator], chatConfiguration: contentSafetyChatConfiguration, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature]); + tags: [version, date, projectName, testClass, Model, Provider, model2, provider2, temperature]); var hubBasedContentSafetyServiceConfiguration = new ContentSafetyServiceConfiguration( @@ -132,8 +135,8 @@ static SafetyEvaluatorTests() clientMetadata = hubBasedContentSafetyChatConfiguration.ChatClient.GetService(); - provider = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; - model = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + model2 = $"Model: {clientMetadata?.DefaultModelId ?? "Unknown"}"; + provider2 = $"Model Provider: {clientMetadata?.ProviderName ?? "Unknown"}"; _hubBasedContentSafetyReportingConfiguration = DiskBasedReportingConfiguration.Create( @@ -147,7 +150,7 @@ static SafetyEvaluatorTests() indirectAttackEvaluator], chatConfiguration: hubBasedContentSafetyChatConfiguration, executionName: Constants.Version, - tags: [version, date, projectName, testClass, provider, model, temperature, usesContext]); + tags: [version, date, projectName, testClass, Model, Provider, model2, provider2, temperature, usesContext]); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/ModelInfoTests.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/ModelInfoTests.cs new file mode 100644 index 00000000000..31d7ceb80c6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Tests/ModelInfoTests.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +extern alias Evaluation; + +using System; +using Evaluation::Microsoft.Extensions.AI.Evaluation.Utilities; +using Xunit; + +namespace Microsoft.Extensions.AI.Evaluation.Tests; + +public class ModelInfoTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + [InlineData("openai")] + public void GetModelProvider_NoProviderUriAndModelSpecified_ReturnsProviderNameOnly(string? providerName) + { + var metadata = new ChatClientMetadata(providerName, providerUri: null); + + string? result = ModelInfo.GetModelProvider(model: null, metadata); + + Assert.Equal(providerName, result); + } + + [Theory] + [InlineData(null, "https://localhost:11434", " (local)")] + [InlineData(null, "https://test.services.ai.azure.com/", " (azure.ai.foundry)")] + [InlineData(null, "https://myapp.openai.azure.com/v1/chat", " (azure.openai)")] + [InlineData(null, "https://myapp.ml.azure.com/", " (azure.ml)")] + [InlineData(null, "https://models.inference.ai.azure.com/v1", " (github.models)")] + [InlineData(null, "https://models.github.ai", " (github.models)")] + [InlineData("", "https://custom.azure.com", " (azure)")] + [InlineData(" ", "https://models.github.com/openai", " (github)")] + [InlineData("\t", "https://services.microsoft.com/models", "\t (microsoft)")] + [InlineData(null, "https://localhost.com:11434/models", null)] + [InlineData(null, "https://github.com/models", null)] + [InlineData("", "https://azure.com/models", "")] + [InlineData("\t", "https://microsoft.com/models", "\t")] + [InlineData(null, "https://example.com/models", null)] + public void GetModelProvider_NoProviderNameAndModelSpecified_ReturnsHostMonikerOnly( + string? providerName, + string providerUri, + string? expected) + { + Uri? uri = providerUri != null ? new Uri(providerUri) : null; + var metadata = new ChatClientMetadata(providerName, providerUri: uri); + + string? result = ModelInfo.GetModelProvider(model: null, metadata); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + [InlineData(" ", null)] + [InlineData("\t", null)] + [InlineData("unknown", null)] + [InlineData("azure.ai.foundry.evaluation", "azure.ai.foundry (azure.ai.foundry)")] + [InlineData(" azure.ai.foundry.evaluation", null)] + [InlineData("azure.ai.foundry.evaluation\t", null)] + [InlineData("azure.ai.foundry . evaluation", null)] + [InlineData("(azure.ai.foundry.evaluation)", null)] + [InlineData("azure.AI.FOUNDRY.evaluation", null)] + [InlineData("ai.foundry.evaluation", null)] + public void GetModelProvider_NoMetadataSpecified_ReturnsExpectedFormat( + string? model, + string? expected) + { + string? result = ModelInfo.GetModelProvider(model, metadata: null); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(null, null, null, null)] + [InlineData("azure.ai.foundry.evaluation", null, null, "azure.ai.foundry (azure.ai.foundry)")] + [InlineData(" azure.ai.foundry.evaluation", null, null, null)] + [InlineData("azure.ai.foundry.evaluation\t", null, null, null)] + [InlineData("(azure.ai.foundry.evaluation)", null, null, null)] + [InlineData("azure.ai.foundry.evaluation", null, "https://myapp.openai.azure.com/", "azure.ai.foundry (azure.ai.foundry)")] + [InlineData("azure.ai.foundry.evaluation", "openai", null, "azure.ai.foundry (azure.ai.foundry)")] + [InlineData("azure.ai.foundry.evaluation", "azure", "https://services.ai.azure.com/", "azure.ai.foundry (azure.ai.foundry)")] + [InlineData("azure.AI.FOUNDRY.evaluation", "custom", null, "custom")] + [InlineData("ai.foundry.evaluation", "custom", "https://myapp.openai.azure.com/", "custom (azure.openai)")] + [InlineData(null, "custom", "https://services.ai.azure.com/", "custom (azure.ai.foundry)")] + [InlineData("", null, "https://myapp.openai.azure.com/", " (azure.openai)")] + [InlineData(" ", null, "https://myapp.openai.azure.com/v1", " (azure.openai)")] + [InlineData("\t", null, "https://myapp.OpenAI.Azure.com/v1/chat", " (azure.openai)")] + [InlineData("unknown", null, "https://myapp.OpenAI.Azure.com/v1/chat", " (azure.openai)")] + public void GetModelProvider_ModelSpecified_ReturnsExpectedFormat( + string? model, + string? providerName, + string? providerUri, + string? expected) + { + Uri? uri = providerUri != null ? new Uri(providerUri) : null; + var metadata = new ChatClientMetadata(providerName, providerUri: uri, defaultModelId: "ignored"); + + string? result = ModelInfo.GetModelProvider(model, metadata); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("llama", "https://localhost:11434", "llama (local)")] + [InlineData("llama", "https://LocalHost", "llama (local)")] + [InlineData("llama", "https://localhost:1234/models/llama", "llama (local)")] + [InlineData("openai", "https://services.ai.azure.com/", "openai (azure.ai.foundry)")] + [InlineData("azure", "https://test.services.ai.azure.com/endpoint", "azure (azure.ai.foundry)")] + [InlineData("openai", "https://myapp.openai.azure.com/", "openai (azure.openai)")] + [InlineData("azure", "https://test.openai.azure.com/v1/chat", "azure (azure.openai)")] + [InlineData("ml", "https://myapp.ml.azure.com/", "ml (azure.ml)")] + [InlineData("azure", "https://myapp.inference.ml.azure.com/v1", "azure (azure.ml)")] + [InlineData("github", "https://models.github.ai/", "github (github.models)")] + [InlineData("openai", "https://models.github.ai/v1", "openai (github.models)")] + [InlineData("github", "https://models.inference.ai.azure.com/", "github (github.models)")] + [InlineData("openai", "https://models.inference.ai.azure.com/v1", "openai (github.models)")] + [InlineData("custom", "https://test.azure.com/", "custom (azure)")] + [InlineData("provider", "https://api.github.com/", "provider (github)")] + [InlineData("service", "https://api.microsoft.com/", "service (microsoft)")] + [InlineData("openai", "https://api.openai.com/", "openai")] + [InlineData("anthropic.claude", "https://api.anthropic.com/", "anthropic.claude")] + [InlineData("custom", "https://example.com/", "custom")] + [InlineData("custom", "https://localhost.com:11434/", "custom")] + [InlineData("custom", "https://host:11434", "custom")] + [InlineData("custom", "https://127.0.0.0:11434", "custom")] + [InlineData("provider", "https://unknown-host.com/", "provider")] + [InlineData("OPENAI provider", "https://SERVICES.AI.AZURE.COM/", "OPENAI provider (azure.ai.foundry)")] + [InlineData("Azure-model-provider", "https://Test.OpenAI.Azure.Com/", "Azure-model-provider (azure.openai)")] + public void GetModelProvider_ReturnsProviderWithHostMoniker( + string providerName, + string providerUri, + string expected) + { + var metadata = new ChatClientMetadata(providerName, new Uri(providerUri)); + + string? result = ModelInfo.GetModelProvider(model: "some-model", metadata); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("https://myapp.openai.azure.services.ai.azure.com/", "azure.ai.foundry")] + [InlineData("https://myapp.services.ai.azure.openai.azure.com/", "azure.ai.foundry")] + [InlineData("https://myapp.microsoft.services.ai.azure.com/", "azure.ai.foundry")] + [InlineData("https://inference.openai.azure.ml.azure.com/", "azure.openai")] + [InlineData("https://inference.ml.azure.openai.azure.com/", "azure.openai")] + [InlineData("https://myapp.azure.models.github.ai/", "github.models")] + [InlineData("https://test.azure.microsoft.com/", "azure")] + [InlineData("https://test.microsoft.github.com/", "github")] + public void GetModelProvider_MultipleHostPatternMatches_ReturnsExpectedHostMoniker( + string providerUri, + string expectedHostMoniker) + { + var metadata = new ChatClientMetadata(providerName: "some-provider", new Uri(providerUri)); + + string? result = ModelInfo.GetModelProvider(model: "some-model", metadata); + + Assert.Equal($"some-provider ({expectedHostMoniker})", result); + } + + [Theory] + [InlineData(null, false, false)] + [InlineData("", false, false)] + [InlineData(" ", false, false)] + [InlineData("\t", false, false)] + [InlineData("llama (local)", false, true)] + [InlineData("openai (azure.ai.foundry)", true, false)] + [InlineData("azure (azure.openai)", true, false)] + [InlineData("azure (azure.ml)", true, false)] + [InlineData("github (github.models)", true, false)] + [InlineData("(azure.ai.foundry)", true, false)] + [InlineData("provider (azure)", true, false)] + [InlineData("service (github)", true, false)] + [InlineData("custom (microsoft)", true, false)] + [InlineData(" (azure.ai.foundry)", true, false)] + [InlineData(" (azure.openai)", true, false)] + [InlineData("\t(github.models)", true, false)] + [InlineData("\t (local)", false, true)] + [InlineData("(azure) ", false, false)] + [InlineData("(github) ", false, false)] + [InlineData("(microsoft)\t", false, false)] + [InlineData(" (local)\t", false, false)] + [InlineData("( azure.ml)", false, false)] + [InlineData("(local\t)", false, false)] + [InlineData("(azure .ml)", false, false)] + [InlineData("(azure. ml)", false, false)] + [InlineData("(LOCAL)", false, false)] + [InlineData("ml [azure.ml]", false, false)] + [InlineData("{azure.ml}", false, false)] + [InlineData("openai (AZURE.OPENAI)", false, false)] + [InlineData("prefix provider (azure.openai)", true, false)] + [InlineData("local", false, false)] + [InlineData("openai", false, false)] + [InlineData("azure.ai.foundry", false, false)] + [InlineData("azure.openai", false, false)] + [InlineData("azure.ml", false, false)] + [InlineData("github.models", false, false)] + [InlineData("azure", false, false)] + [InlineData("github", false, false)] + [InlineData("microsoft", false, false)] + [InlineData("(custom-host)", false, false)] + [InlineData("provider (unknown)", false, false)] + [InlineData("provider (", false, false)] + [InlineData("provider )", false, false)] + [InlineData("provider (azure.ai.foundry) extra", false, false)] + [InlineData("(microsoft)\tcustom (other)", false, false)] + [InlineData("provider (azure.ai.foundry", false, false)] + [InlineData("provider azure.ai.foundry)", false, false)] + public void ModelHostMonikerClassificationWorks( + string? modelProvider, + bool expectedIsModelHostWellKnown, + bool expectedIsModelHostedLocally) + { + bool isModelHostWellKnown = ModelInfo.IsModelHostWellKnown(modelProvider); + Assert.Equal(expectedIsModelHostWellKnown, isModelHostWellKnown); + + bool isModelHostedLocally = ModelInfo.IsModelHostedLocally(modelProvider); + Assert.Equal(expectedIsModelHostedLocally, isModelHostedLocally); + } +} From 4b40c8e7b168d488f1960e87890b48484143279d Mon Sep 17 00:00:00 2001 From: William Godbe Date: Fri, 29 Aug 2025 10:19:15 -0700 Subject: [PATCH 271/472] Add unofficial .yml (#6756) * Add unofficial .yml * get rid of bad condition --- azure-pipelines-unofficial.yml | 258 +++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 azure-pipelines-unofficial.yml diff --git a/azure-pipelines-unofficial.yml b/azure-pipelines-unofficial.yml new file mode 100644 index 00000000000..4bfc6d9e5fd --- /dev/null +++ b/azure-pipelines-unofficial.yml @@ -0,0 +1,258 @@ +trigger: none + +pr: none + +variables: + - name: _TeamName + value: dotnet-r9 + - name: NativeToolsOnMachine + value: true + - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE + value: true + + - name: SkipQualityGates + value: false + + - name: runAsPublic + value: ${{ eq(variables['System.TeamProject'], 'public') }} + + - name: _BuildConfig + value: Release + - name: isOfficialBuild + value: ${{ and(ne(variables['runAsPublic'], 'true'), notin(variables['Build.Reason'], 'PullRequest')) }} + - name: Build.Arcade.ArtifactsPath + value: $(Build.SourcesDirectory)/artifacts/ + - name: Build.Arcade.LogsPath + value: $(Build.Arcade.ArtifactsPath)log/$(_BuildConfig)/ + - name: Build.Arcade.TestResultsPath + value: $(Build.Arcade.ArtifactsPath)TestResults/$(_BuildConfig)/ + - name: Build.Arcade.VSIXOutputPath + value: $(Build.Arcade.ArtifactsPath)VSIX + + - ${{ if or(startswith(variables['Build.SourceBranch'], 'refs/heads/release/'), startswith(variables['Build.SourceBranch'], 'refs/heads/internal/release/'), startswith(variables['Build.SourceBranch'], 'refs/heads/validation/'), eq(variables['Build.Reason'], 'Manual')) }}: + - name: PostBuildSign + value: false + - ${{ else }}: + - name: PostBuildSign + value: true + + - name: _PublishArgs + value: >- + /p:DotNetPublishUsingPipelines=true + - name: _OfficialBuildIdArgs + value: /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + # needed for signing + - name: _SignType + value: test + - name: _SignArgs + value: /p:DotNetSignType=$(_SignType) /p:TeamName=$(_TeamName) /p:Sign=$(_Sign) /p:DotNetPublishUsingPipelines=true + - name: _Sign + value: true + + - name: enableSourceIndex + value: false + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + sdl: + sourceAnalysisPool: + name: NetCore1ESPool-Internal + image: windows.vs2022preview.amd64 + os: windows + customBuildTags: + - ES365AIMigrationTooling + + stages: + - stage: build + displayName: Build + variables: + - template: /eng/common/templates-official/variables/pool-providers.yml@self + jobs: + - template: /eng/common/templates-official/jobs/jobs.yml@self + parameters: + enableMicrobuild: true + enableTelemetry: true + enableSourceIndex: ${{ variables['enableSourceIndex'] }} + runAsPublic: ${{ variables['runAsPublic'] }} + # Publish build logs + enablePublishBuildArtifacts: false + # Publish test logs + enablePublishTestResults: true + # Publish NuGet packages using v3 + # https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md#basic-onboarding-scenario-for-new-repositories-to-the-current-publishing-version-v3 + enablePublishUsingPipelines: false + enablePublishBuildAssets: false + workspace: + clean: all + + jobs: + + # ---------------------------------------------------------------- + # This job build and run tests on Windows + # ---------------------------------------------------------------- + - job: Windows + timeoutInMinutes: 180 + testResultsFormat: VSTest + pool: + name: NetCore1ESPool-Internal + image: windows.vs2022preview.amd64 + os: windows + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.cmd -ci -NativeToolsOnMachine + + templateContext: + outputs: + - output: pipelineArtifact + displayName: 'Publish Azure DevOps extension artifacts' + condition: succeeded() + targetPath: '$(Build.Arcade.VSIXOutputPath)' + artifactName: 'VSIXArtifacts' + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: 1 + + steps: + - template: /eng/pipelines/templates/BuildAndTest.yml + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipQualityGates: ${{ eq(variables['SkipQualityGates'], 'true') }} + isWindows: true + warnAsError: 0 + + # ---------------------------------------------------------------- + # This job build and run tests on Ubuntu + # ---------------------------------------------------------------- + - job: Ubuntu + timeoutInMinutes: 180 + testResultsFormat: VSTest + pool: + name: NetCore1ESPool-Internal + image: 1es-mariner-2 + os: linux + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.sh --ci + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: 1 + + steps: + - template: /eng/pipelines/templates/BuildAndTest.yml + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipQualityGates: ${{ eq(variables['SkipQualityGates'], 'true') }} + isWindows: false + warnAsError: 0 + + # ---------------------------------------------------------------- + # This stage performs quality gates enforcements + # ---------------------------------------------------------------- + - stage: codecoverage + displayName: CodeCoverage + dependsOn: + - build + condition: and(succeeded('build'), ne(variables['SkipQualityGates'], 'true')) + variables: + - template: /eng/common/templates-official/variables/pool-providers.yml@self + jobs: + - template: /eng/common/templates-official/jobs/jobs.yml@self + parameters: + enableMicrobuild: true + enableTelemetry: true + runAsPublic: ${{ variables['runAsPublic'] }} + workspace: + clean: all + + # ---------------------------------------------------------------- + # This stage downloads the code coverage reports from the build jobs, + # merges those and validates the combined test coverage. + # ---------------------------------------------------------------- + jobs: + - job: CodeCoverageReport + timeoutInMinutes: 180 + + pool: + name: NetCore1ESPool-Internal + image: 1es-mariner-2 + os: linux + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: 1 + + steps: + - script: $(Build.SourcesDirectory)/build.sh --ci --restore + displayName: Init toolset + + - template: /eng/pipelines/templates/VerifyCoverageReport.yml + + + # ---------------------------------------------------------------- + # This stage only performs a build treating warnings as errors + # to detect any kind of code style violations + # ---------------------------------------------------------------- + - stage: correctness + displayName: Correctness + dependsOn: [] + variables: + - template: /eng/common/templates-official/variables/pool-providers.yml@self + jobs: + - template: /eng/common/templates-official/jobs/jobs.yml@self + parameters: + enableMicrobuild: true + enableTelemetry: true + runAsPublic: ${{ variables['runAsPublic'] }} + workspace: + clean: all + + jobs: + - job: WarningsCheck + timeoutInMinutes: 180 + + pool: + name: NetCore1ESPool-Internal + image: 1es-mariner-2 + os: linux + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.sh --ci + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: 1 + + steps: + - template: '\eng\pipelines\templates\BuildAndTest.yml' + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipTests: true + skipQualityGates: true + isWindows: false From efe30a38adfa89d7b6b81df41ea30af0996269ae Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 29 Aug 2025 12:57:34 -0600 Subject: [PATCH 272/472] Add suppressions for Credential Scanner (#6758) Get repo clean, verified locally --- .config/CredScanSuppressions.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.config/CredScanSuppressions.json b/.config/CredScanSuppressions.json index 9e26dfeeb6e..3bbc3c4e0b2 100644 --- a/.config/CredScanSuppressions.json +++ b/.config/CredScanSuppressions.json @@ -1 +1,9 @@ -{} \ No newline at end of file +{ + "tool": "Credential Scanner", + "suppressions": [ + { + "file": "\\test\\Libraries\\Microsoft.Extensions.Compliance.Redaction.Tests\\HmacRedactorTest.cs", + "_justification": "Tests" + } + ] +} From 0b4c4d53f728a0d4ea12c099bba3f3c1218dc15a Mon Sep 17 00:00:00 2001 From: Dmytro Bohdanov <41544793+rainsxng@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:54:20 +0200 Subject: [PATCH 273/472] Log and redact query string params (#6521) * parse query string params * add proper redaction * linter fix * additional tests, check for the null params and false logging flag * add experimental attribute, lint * suppress warnings for 4.6.2 * Proper error suppression * fix linter * PR fixes * add support for net462, wip * remove extra logging flag, update tests * remove query parameters count field, reset QP in the LogRecord * update interfaces * fix tests and pr comments * use classification tag, trying to fix TryAdd with conditional compilation * update the logging, additional tests * formatting, experimental attribute * fix PR comments * fix formatting * use array instead of the list for the query parameters * use shared object pool to store the result, mem alloc improvements * fix formatting * use url.full tag for query parameters * update tests to check the redacted value * remove LINQ and store FullURI as a string * update prefixes * explicit pool type and small refactor * fix warnings * update naming tag names * remove double data redaction * update redaction logic * fix warnings * fix warnings * change naming * always emit the url.full tag, update the tests * fix warnings and PR comments * fix build warnings * use url.query tag to store query parameters * update test assertions * Update src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs Co-authored-by: Iliar Turdushev --------- Co-authored-by: Iliar Turdushev --- .../Logging/HttpClientLoggingTagNames.cs | 16 +- .../Logging/Internal/HttpHeadersReader.cs | 3 + .../Logging/Internal/HttpHeadersRedactor.cs | 5 + .../Logging/Internal/HttpRequestReader.cs | 113 +++++- .../Logging/Internal/IHttpHeadersReader.cs | 9 + .../Logging/Internal/IHttpHeadersRedactor.cs | 8 + .../Logging/Internal/Log.cs | 9 +- .../Logging/Internal/LogRecord.cs | 7 + .../Logging/LoggingOptions.cs | 16 + .../Logging/HttpRequestReaderTest.cs | 326 +++++++++++++++++- 10 files changed, 485 insertions(+), 27 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/HttpClientLoggingTagNames.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/HttpClientLoggingTagNames.cs index 803eb03d082..d4cbc839551 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/HttpClientLoggingTagNames.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/HttpClientLoggingTagNames.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Http.Logging; @@ -51,6 +53,12 @@ public static class HttpClientLoggingTagNames /// public const string ResponseHeaderPrefix = "http.response.header."; + /// + /// URL query parameters prefix. + /// + [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] + public const string UrlQuery = "url.query"; + /// /// HTTP Status Code. /// @@ -61,8 +69,7 @@ public static class HttpClientLoggingTagNames /// /// A read-only of all tag names. public static IReadOnlyList TagNames { get; } = - Array.AsReadOnly(new[] - { + Array.AsReadOnly([ Duration, Host, Method, @@ -71,6 +78,7 @@ public static class HttpClientLoggingTagNames RequestHeaderPrefix, ResponseBody, ResponseHeaderPrefix, - StatusCode - }); + StatusCode, + UrlQuery + ]); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersReader.cs index 8e6bcedc4e7..1679d2c764b 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersReader.cs @@ -70,6 +70,9 @@ public void ReadResponseHeaders(HttpResponseMessage response, List _redactor.Redact(value, classification); + private void ReadHeaders(HttpHeaders headers, FrozenDictionary headersToLog, List> destination) { #if NET6_0_OR_GREATER diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersRedactor.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersRedactor.cs index 652636e53a6..a09f0eecf7c 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersRedactor.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpHeadersRedactor.cs @@ -29,6 +29,11 @@ public string Redact(IEnumerable headerValues, DataClassification classi _ => TelemetryConstants.Unknown }; + public string Redact(string value, DataClassification classification) + { + return _redactorProvider.GetRedactor(classification).Redact(value); + } + private string RedactIEnumerable(IEnumerable input, DataClassification classification) { var redactor = _redactorProvider.GetRedactor(classification); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs index a9eaf982ac4..78cb73bbddc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/HttpRequestReader.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Telemetry.Internal; using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Http.Logging.Internal; @@ -23,16 +24,18 @@ internal sealed class HttpRequestReader : IHttpRequestReader private readonly IHttpRouteParser _httpRouteParser; private readonly IHttpHeadersReader _httpHeadersReader; private readonly FrozenDictionary _defaultSensitiveParameters; + private readonly FrozenDictionary _queryParameterDataClasses; private readonly bool _logRequestBody; private readonly bool _logResponseBody; + private readonly bool _logRequestQueryParameters; private readonly bool _logRequestHeaders; private readonly bool _logResponseHeaders; private readonly HttpRouteParameterRedactionMode _routeParameterRedactionMode; - // These are not registered in DI as handler today is public and we would need to make all of those types public. + // These are not registered in DI as handler today is public, and we would need to make all of those types public. // They are not implemented as statics to simplify design and pass less arguments around. // Also wanted to encapsulate logic of reading each part of the request to simplify handler logic itself. private readonly HttpRequestBodyReader _httpRequestBodyReader; @@ -77,6 +80,7 @@ internal HttpRequestReader( _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager; _defaultSensitiveParameters = options.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal); + _queryParameterDataClasses = options.RequestQueryParametersDataClasses.ToFrozenDictionary(StringComparer.Ordinal); if (options.LogBody) { @@ -86,6 +90,7 @@ internal HttpRequestReader( _logRequestHeaders = options.RequestHeadersDataClasses.Count > 0; _logResponseHeaders = options.ResponseHeadersDataClasses.Count > 0; + _logRequestQueryParameters = options.RequestQueryParametersDataClasses.Count > 0; _httpRequestBodyReader = new HttpRequestBodyReader(options); _httpResponseBodyReader = new HttpResponseBodyReader(options); @@ -93,8 +98,27 @@ internal HttpRequestReader( _routeParameterRedactionMode = options.RequestPathParameterRedactionMode; } + public async Task ReadResponseAsync(LogRecord logRecord, HttpResponseMessage response, + List>? responseHeadersBuffer, + CancellationToken cancellationToken) + { + if (_logResponseHeaders) + { + _httpHeadersReader.ReadResponseHeaders(response, responseHeadersBuffer); + logRecord.ResponseHeaders = responseHeadersBuffer; + } + + if (_logResponseBody) + { + logRecord.ResponseBody = await _httpResponseBodyReader.ReadAsync(response, cancellationToken).ConfigureAwait(false); + } + + logRecord.StatusCode = (int)response.StatusCode; + } + public async Task ReadRequestAsync(LogRecord logRecord, HttpRequestMessage request, - List>? requestHeadersBuffer, CancellationToken cancellationToken) + List>? requestHeadersBuffer, CancellationToken + cancellationToken) { logRecord.Host = request.RequestUri?.Host ?? TelemetryConstants.Unknown; logRecord.Method = request.Method; @@ -111,24 +135,86 @@ public async Task ReadRequestAsync(LogRecord logRecord, HttpRequestMessage reque logRecord.RequestBody = await _httpRequestBodyReader.ReadAsync(request, cancellationToken) .ConfigureAwait(false); } + + if (_logRequestQueryParameters && !string.IsNullOrEmpty(request.RequestUri?.Query)) + { + logRecord.QueryString = ExtractAndRedactQueryParameters(request.RequestUri!.Query); + } + else + { + logRecord.QueryString = string.Empty; + } } - public async Task ReadResponseAsync(LogRecord logRecord, HttpResponseMessage response, - List>? responseHeadersBuffer, - CancellationToken cancellationToken) + private static string UnescapeDataString(ReadOnlySpan value) { - if (_logResponseHeaders) +#if NET9_0_OR_GREATER + return Uri.UnescapeDataString(value); +#else + return Uri.UnescapeDataString(value.ToString()); +#endif + } + + private string ExtractAndRedactQueryParameters(string query) + { + var stringBuilder = PoolFactory.SharedStringBuilderPool.Get(); + try { - _httpHeadersReader.ReadResponseHeaders(response, responseHeadersBuffer); - logRecord.ResponseHeaders = responseHeadersBuffer; - } + ReadOnlySpan querySpan = query.AsSpan(); + int length = querySpan.Length; + int start = 0; - if (_logResponseBody) + // Remove leading '?' + if (length > 0 && querySpan[0] == '?') + { + start = 1; + } + + while (start < length) + { + int amp = querySpan.Slice(start).IndexOf('&'); + int end = amp == -1 ? length : start + amp; + + int eq = querySpan.Slice(start, end - start).IndexOf('='); + if (eq >= 0) + { + var keySpan = querySpan.Slice(start, eq); + var valueSpan = querySpan.Slice(start + eq + 1, end - (start + eq + 1)); + + string key = UnescapeDataString(keySpan); + string value = UnescapeDataString(valueSpan); + + // Only process if the key is in the classification dictionary and value is not empty + if (!string.IsNullOrEmpty(value) && _queryParameterDataClasses.TryGetValue(key, out var classification)) + { + string redacted = _httpHeadersReader.RedactValue(value, classification); + + // Append to string builder directly with proper encoding + if (stringBuilder.Length > 0) + { + _ = stringBuilder.Append('&'); + } + + _ = stringBuilder.Append(Uri.EscapeDataString(key)) + .Append('=') + .Append(Uri.EscapeDataString(redacted)); + } + } + + if (amp == -1) + { + break; + } + + start = end + 1; + } + + return stringBuilder.ToString(); + } + finally { - logRecord.ResponseBody = await _httpResponseBodyReader.ReadAsync(response, cancellationToken).ConfigureAwait(false); + PoolFactory.SharedStringBuilderPool.Return(stringBuilder); } - - logRecord.StatusCode = (int)response.StatusCode; } private void GetRedactedPathAndParameters(HttpRequestMessage request, LogRecord logRecord) @@ -185,3 +271,4 @@ private void GetRedactedPathAndParameters(HttpRequestMessage request, LogRecord } } } + diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersReader.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersReader.cs index 7e21550929e..66da00b5c78 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersReader.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersReader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Http; +using Microsoft.Extensions.Compliance.Classification; namespace Microsoft.Extensions.Http.Logging.Internal; @@ -24,4 +25,12 @@ internal interface IHttpHeadersReader /// An instance of to read headers from. /// Destination to save read headers to. void ReadResponseHeaders(HttpResponseMessage response, List>? destination); + + /// + /// Redact values by using a . + /// + /// A value that needs to be redacted. + /// An instance of to redact a value. + /// Redacted value. + string RedactValue(string value, DataClassification classification); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersRedactor.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersRedactor.cs index 3ced3da01b9..3453beed4d0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersRedactor.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/IHttpHeadersRedactor.cs @@ -19,4 +19,12 @@ internal interface IHttpHeadersRedactor /// Data classification which is used to get an appropriate redactor to redact headers. /// Returns text and parameter segments of route. string Redact(IEnumerable headerValues, DataClassification classification); + + /// + /// Redacts HTTP header value which results into a . + /// + /// HTTP header value. + /// Data classification which is used to get an appropriate redactor to redact header. + /// Returns text and parameter segments of route. + string Redact(string headerValue, DataClassification classification); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs index 3ae22bf50bf..2999122d3e2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/Log.cs @@ -99,9 +99,10 @@ private static void OutgoingRequest( var statusCodePropertyCount = record.StatusCode.HasValue ? 1 : 0; var requestHeadersCount = record.RequestHeaders?.Count ?? 0; var responseHeadersCount = record.ResponseHeaders?.Count ?? 0; + var urlQueryPropertyCount = string.IsNullOrEmpty(record.QueryString) ? 0 : 1; var spaceToReserve = MinimalPropertyCount + statusCodePropertyCount + requestHeadersCount + responseHeadersCount + - record.PathParametersCount + (record.RequestBody is null ? 0 : 1) + (record.ResponseBody is null ? 0 : 1); + record.PathParametersCount + (record.RequestBody is null ? 0 : 1) + (record.ResponseBody is null ? 0 : 1) + urlQueryPropertyCount; var index = loggerMessageState.ReserveTagSpace(spaceToReserve); loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Method, record.Method); @@ -109,6 +110,11 @@ private static void OutgoingRequest( loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Path, record.Path); loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.Duration, record.Duration); + if (!string.IsNullOrEmpty(record.QueryString)) + { + loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.UrlQuery, record.QueryString); + } + if (record.StatusCode.HasValue) { loggerMessageState.TagArray[index++] = new(HttpClientLoggingTagNames.StatusCode, record.StatusCode.Value); @@ -148,7 +154,6 @@ private static void OutgoingRequest( loggerMessageState, exception, _originalFormatValueFmtFunc); - if (record.EnrichmentTags is null) { loggerMessageState.Clear(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs index 93238e5809c..09830eb6f93 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/Internal/LogRecord.cs @@ -1,6 +1,7 @@ // 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.Buffers; using System.Collections.Generic; using System.Net.Http; @@ -75,6 +76,11 @@ internal sealed class LogRecord : IResettable /// public int PathParametersCount { get; set; } + /// + /// Gets or sets formatted query parameters. + /// + public string? QueryString { get; set; } + public bool TryReset() { if (PathParameters != null) @@ -94,6 +100,7 @@ public bool TryReset() RequestHeaders = null; ResponseHeaders = null; PathParametersCount = 0; + QueryString = string.Empty; return true; } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs index fe2d096f7bc..0b5039272b5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Logging/LoggingOptions.cs @@ -36,6 +36,22 @@ public class LoggingOptions /// public bool LogRequestStart { get; set; } + /// + /// Gets or sets the set of HTTP request query parameters to log and their respective data classifications to use for redaction. + /// + /// + /// The default value is . + /// + /// + /// If empty, no HTTP request query parameters will be logged. + /// If the data class is , no redaction will be done. + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + [Required] + [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] + public IDictionary RequestQueryParametersDataClasses { get; set; } = new Dictionary(); + /// /// Gets or sets a value indicating whether the HTTP request and response body are logged. /// diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs index ba781c5d101..c2b5295d707 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Logging/HttpRequestReaderTest.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Http.Logging.Internal; using Microsoft.Extensions.Http.Logging.Test.Internal; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Telemetry.Internal; using Moq; using Xunit; @@ -54,6 +55,8 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() ResponseHeaders = [new("Header2", Redacted), new("Header3", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty, }; var options = new LoggingOptions @@ -80,7 +83,7 @@ public async Task ReadAsync_AllData_ReturnsLogRecord() using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo"), + RequestUri = new Uri("https://default-uri.com/foo"), Content = new StringContent(requestContent, Encoding.UTF8) }; @@ -120,6 +123,8 @@ public async Task ReadAsync_NoHost_ReturnsLogRecordWithoutHost() StatusCode = 200, RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty }; var options = new LoggingOptions @@ -180,6 +185,8 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty }; var opts = new LoggingOptions @@ -206,7 +213,7 @@ public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri("https://default-uri.com/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -251,7 +258,8 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, - PathParametersCount = 1 + PathParametersCount = 1, + QueryString = string.Empty }; var opts = new LoggingOptions @@ -281,7 +289,7 @@ public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute( using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri($"http://{RequestedHost}/foo/bar/123"), + RequestUri = new Uri($"https://{RequestedHost}/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -325,6 +333,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith Path = "/foo/bar/123", RequestHeaders = [new("Header1", Redacted)], RequestBody = requestContent, + QueryString = string.Empty }; var opts = new LoggingOptions @@ -353,7 +362,7 @@ public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWith using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri("https://default-uri.com/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -385,6 +394,8 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty }; var opts = new LoggingOptions @@ -411,7 +422,7 @@ public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_Returns using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri("https://default-uri.com/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -456,6 +467,8 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty }; var opts = new LoggingOptions @@ -482,7 +495,7 @@ public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri("https://default-uri.com/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -523,6 +536,8 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur ResponseHeaders = [new("Header2", Redacted)], RequestBody = requestContent, ResponseBody = responseContent, + + QueryString = string.Empty }; var opts = new LoggingOptions @@ -549,7 +564,7 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur using var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + RequestUri = new Uri("https://default-uri.com/foo/bar/123"), Content = new StringContent(requestContent, Encoding.UTF8), }; @@ -573,6 +588,301 @@ public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_Retur actualRecord.Should().BeEquivalentTo(expectedRecord); } + [Fact] + public async Task ReadAsync_SetsQueryParameters_WhenClassificationPresent() + { + var requestContent = _fixture.Create(); + var queryParamName = "userId"; + var queryParamValue = "12345"; + + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { queryParamName, FakeTaxonomy.PrivateData } + }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + LogBody = true + }; + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + await using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var uri = new Uri($"https://{RequestedHost}/api/resource?{queryParamName}={queryParamValue}"); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = uri, + Content = new StringContent(requestContent, Encoding.UTF8, "text/plain") + }; + + var logRecord = new LogRecord(); + var requestHeadersBuffer = new List>(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None); + logRecord.QueryString.Should().NotBeNullOrEmpty(); + logRecord.QueryString.Should().Contain("userId=REDACTED"); + } + + [Fact] + public async Task ReadAsync_SkipsQueryString_WhenClassificationEmpty() + { + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary() // No data classification + }; + + var mockHeadersRedactor = new Mock(); + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var uri = new Uri($"https://{RequestedHost}/api/resource?userId=12345"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + var logRecord = new LogRecord(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, new List>(), CancellationToken.None); + logRecord.QueryString.Should().BeNullOrEmpty(); + } + + [Fact] + public async Task ReadAsync_SetsEmptyQueryParameters_WhenNoMatchingClassification() + { + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { "otherParam", FakeTaxonomy.PrivateData } + } + }; + + var mockHeadersRedactor = new Mock(); + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var uri = new Uri($"https://{RequestedHost}/api/resource?userId=12345"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + var logRecord = new LogRecord(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, new List>(), CancellationToken.None); + + logRecord.QueryString.Should().BeNullOrEmpty(); + } + + [Fact] + public async Task ReadAsync_SetsMultipleQueryParameters_WhenMultipleClassifications() + { + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { "userId", FakeTaxonomy.PrivateData }, + { "token", FakeTaxonomy.PrivateData } + } + }; + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var uri = new Uri($"https://{RequestedHost}/api/resource?userId=12345&token=abc&other=not_logged"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + var logRecord = new LogRecord(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, new List>(), CancellationToken.None); + logRecord.QueryString.Should().NotBeNullOrEmpty(); + logRecord.QueryString.Should().Be("userId=REDACTED&token=REDACTED"); + } + + [Fact] + public async Task LogRequestStartAsync_LogsQueryParameters_TagArray() + { + // Arrange + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { "userId", FakeTaxonomy.PrivateData } + }, + LogRequestStart = true + }; + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + + var fakeLogger = new FakeLogger( + new FakeLogCollector( + Options.Options.Create( + new FakeLogCollectorOptions()))); + using var serviceProvider = GetServiceProvider(headersReader); + var enrichers = Enumerable.Empty(); + var httpRequestReader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var clientLogger = new HttpClientLogger( + fakeLogger, + httpRequestReader, + enrichers, + options); + + var uri = new Uri($"https://{RequestedHost}/api/resource?userId=12345"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + await clientLogger.LogRequestStartAsync(httpRequestMessage); + + // Assert + var logRecord = fakeLogger.Collector.GetSnapshot().First(); + var state = logRecord.GetStructuredState(); + + Assert.Contains( + state, + tag => tag.Key == "url.query" && (tag.Value!).Contains("userId=REDACTED")); + } + + [Fact] + public async Task ReadAsync_DoesntSetQueryString_WhenQueryValueEmpty() + { + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { "userId", FakeTaxonomy.PrivateData } + } + }; + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + var uri = new Uri($"https://{RequestedHost}/api/resource?userId="); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + + var logRecord = new LogRecord(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, new List>(), CancellationToken.None); + logRecord.QueryString.Should().BeNullOrEmpty(); + } + + [Fact] + public async Task ReadAsync_RedactsPathAndQueryParameters() + { + // Arrange + var requestContent = _fixture.Create(); + var queryParamName = "userId"; + var queryParamValue = "12345"; + var pathParamName = "orderId"; + var pathParamValue = "789"; + + var options = new LoggingOptions + { + RequestQueryParametersDataClasses = new Dictionary + { + { queryParamName, FakeTaxonomy.PrivateData } + }, + LogBody = true, + RequestPathLoggingMode = OutgoingPathLoggingMode.Formatted + }; + options.RouteParameterDataClasses.Add("routeId", FakeTaxonomy.PrivateData); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object); + using var serviceProvider = GetServiceProvider(headersReader); + + var reader = new HttpRequestReader( + serviceProvider, + options.ToOptionsMonitor(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + RequestMetadataContext); + + // The route template includes a path parameter + var routeTemplate = $"/api/orders/{{{pathParamName}}}/details"; + var uri = new Uri($"https://{RequestedHost}/api/orders/{pathParamValue}/details?{queryParamName}={queryParamValue}"); + + using var httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.Method = HttpMethod.Get; + httpRequestMessage.RequestUri = uri; + httpRequestMessage.Content = new StringContent(requestContent, Encoding.UTF8, "text/plain"); + + // Attach request metadata for the route template + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = routeTemplate + }); + + var logRecord = new LogRecord(); + var requestHeadersBuffer = new List>(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None); + + // Assert: path parameter is redacted in the path + logRecord.Path.Should().NotContain(pathParamValue); + logRecord.Path.Should().Contain(Redacted); + + logRecord.QueryString.Should().NotBeNullOrEmpty(); + logRecord.QueryString.Should().Contain($"{queryParamName}={Redacted}"); + logRecord.QueryString.Should().NotContain(queryParamValue); + } + private static ServiceProvider GetServiceProvider( HttpHeadersReader headersReader, string? serviceKey = null, From 18bfe72425b923ea8f0a87ad1bd39ff32848acd7 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:01:23 -0700 Subject: [PATCH 274/472] Update dependencies from https://github.com/dotnet/arcade build 20250828.3 (#6760) Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Templating , Microsoft.DotNet.Helix.Sdk From Version 9.0.0-beta.25415.3 -> To Version 9.0.0-beta.25428.3 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 2 +- eng/common/core-templates/job/source-build.yml | 4 ++++ eng/common/core-templates/jobs/source-build.yml | 5 +++++ eng/common/core-templates/steps/source-build.yml | 9 ++++++++- global.json | 4 ++-- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 89144ccd7fd..3070e2658d3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - d87d66c43d0660e5c8e84e667c5c8a8140bce888 + 5fe939db0a156be6f10e17c105b1842c0c8c8bdc - + https://github.com/dotnet/arcade - d87d66c43d0660e5c8e84e667c5c8a8140bce888 + 5fe939db0a156be6f10e17c105b1842c0c8c8bdc - + https://github.com/dotnet/arcade - d87d66c43d0660e5c8e84e667c5c8a8140bce888 + 5fe939db0a156be6f10e17c105b1842c0c8c8bdc diff --git a/eng/Versions.props b/eng/Versions.props index 0435109a781..f79956ce23f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -83,7 +83,7 @@ 9.0.8 - 9.0.0-beta.25415.3 + 9.0.0-beta.25428.3 diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index d47f09d58fd..5baedac1e03 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -33,6 +33,9 @@ parameters: # container and pool. platform: {} + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: '' # If set to true and running on a non-public project, @@ -93,3 +96,4 @@ jobs: parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} platform: ${{ parameters.platform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} diff --git a/eng/common/core-templates/jobs/source-build.yml b/eng/common/core-templates/jobs/source-build.yml index a10ccfbee6d..0b408a67bd5 100644 --- a/eng/common/core-templates/jobs/source-build.yml +++ b/eng/common/core-templates/jobs/source-build.yml @@ -21,6 +21,9 @@ parameters: # one job runs on 'defaultManagedPlatform'. platforms: [] + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: '' # If set to true and running on a non-public project, @@ -47,6 +50,7 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ platform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} - ${{ if eq(length(parameters.platforms), 0) }}: @@ -55,4 +59,5 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ parameters.defaultManagedPlatform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index 730f7ab2b67..0718e4ba902 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -11,6 +11,10 @@ parameters: # for details. The entire object is described in the 'job' template for simplicity, even though # the usage of the properties on this object is split between the 'job' and 'steps' templates. platform: {} + + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: false steps: @@ -126,5 +130,8 @@ steps: parameters: displayName: Component Detection (Exclude upstream cache) is1ESPipeline: ${{ parameters.is1ESPipeline }} - componentGovernanceIgnoreDirectories: '$(System.DefaultWorkingDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' + ${{ if eq(length(parameters.componentGovernanceIgnoreDirectories), 0) }}: + componentGovernanceIgnoreDirectories: '$(System.DefaultWorkingDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' + ${{ else }}: + componentGovernanceIgnoreDirectories: ${{ join(',', parameters.componentGovernanceIgnoreDirectories) }} disableComponentGovernance: ${{ eq(variables['System.TeamProject'], 'public') }} diff --git a/global.json b/global.json index 8a2286f473a..b722651bed4 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25415.3", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25415.3" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25428.3", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25428.3" } } From d1008fbbc6aa99d75cdff006bf72d622f75fba88 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 2 Sep 2025 10:04:18 -0700 Subject: [PATCH 275/472] Add net462 target to ServiceDiscovery (#11114) An add on from #10470 that added support for netstandard2.0, this adds an explicit net462 target which is part of the recommendation for multi-targeted libraries. --- ...rosoft.Extensions.ServiceDiscovery.Abstractions.csproj | 8 ++++---- .../Microsoft.Extensions.ServiceDiscovery.csproj | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 061e85b44d3..c32fb4c87e5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,9 +1,9 @@ - $(DefaultTargetFramework);netstandard2.0 + netstandard2.0;net462;$(DefaultTargetFramework) true - true + true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery @@ -19,10 +19,10 @@ - + - + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 72f8f4f9051..2556df195e6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,9 +1,9 @@ - netstandard2.0;$(DefaultTargetFramework) + netstandard2.0;net462;$(DefaultTargetFramework) true - true + true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) @@ -18,10 +18,10 @@ - + - + From e57e605061b4a129b0a6e1a7f54939058adccb6e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 2 Sep 2025 14:14:20 -0400 Subject: [PATCH 276/472] Split AIFunction into a base class (#6695) --- .../CHANGELOG.md | 4 + .../ChatCompletion/ChatToolMode.cs | 3 +- .../ChatCompletion/RequiredChatToolMode.cs | 8 +- .../Functions/AIFunction.cs | 51 ++---- .../Functions/AIFunctionDeclaration.cs | 58 ++++++ .../Functions/AIFunctionFactory.cs | 43 ++++- .../Functions/AIFunctionFactoryOptions.cs | 4 +- .../DelegatingAIFunctionDeclaration.cs | 48 +++++ .../Microsoft.Extensions.AI.Abstractions.json | 36 +++- .../Utilities/AIJsonSchemaTransformCache.cs | 16 +- .../AzureAIInferenceChatClient.cs | 4 +- .../CHANGELOG.md | 5 + .../AIToolExtensions.cs | 2 +- .../IntentResolutionEvaluator.cs | 2 +- .../IntentResolutionEvaluatorContext.cs | 8 +- .../TaskAdherenceEvaluator.cs | 2 +- .../TaskAdherenceEvaluatorContext.cs | 8 +- .../ToolCallAccuracyEvaluator.cs | 2 +- .../ToolCallAccuracyEvaluatorContext.cs | 8 +- .../CHANGELOG.md | 5 + ...crosoftExtensionsAIAssistantsExtensions.cs | 4 +- .../MicrosoftExtensionsAIChatExtensions.cs | 4 +- ...MicrosoftExtensionsAIRealtimeExtensions.cs | 4 +- ...icrosoftExtensionsAIResponsesExtensions.cs | 4 +- .../OpenAIAssistantsChatClient.cs | 4 +- .../OpenAIChatClient.cs | 4 +- .../OpenAIClientExtensions.cs | 4 +- .../OpenAIRealtimeConversationClient.cs | 2 +- .../OpenAIResponsesChatClient.cs | 4 +- .../Microsoft.Extensions.AI/CHANGELOG.md | 1 + .../FunctionInvokingChatClient.cs | 170 +++++++++++++----- .../Microsoft.Extensions.AI.json | 4 + .../FunctionInvokingChatClientTests.cs | 115 ++++++++++++ .../Functions/AIFunctionFactoryTest.cs | 20 +++ 34 files changed, 530 insertions(+), 131 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index b98fa7f9312..bd2643ec060 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## NOT YET RELEASED + +- Added non-invocable `AIFunctionDeclaration` (base class for `AIFunction`), `AIFunctionFactory.CreateDeclaration`, and `AIFunction.AsDeclarationOnly`. + ## 9.8.0 - Added `AIAnnotation` and related types to represent citations and other annotations in chat messages. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs index 05e1f28f476..73134a5d894 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs @@ -55,8 +55,7 @@ private protected ChatToolMode() /// /// Instantiates a indicating that tool usage is required, - /// and that the specified must be selected. The function name - /// must match an entry in . + /// and that the specified function name must be selected. /// /// The name of the required function. /// An instance of for the specified function name. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs index 91397e67602..59ce51e7ef3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs @@ -15,17 +15,17 @@ namespace Microsoft.Extensions.AI; public sealed class RequiredChatToolMode : ChatToolMode { /// - /// Gets the name of a specific that must be called. + /// Gets the name of a specific tool that must be called. /// /// - /// If the value is , any available function can be selected (but at least one must be). + /// If the value is , any available tool can be selected (but at least one must be). /// public string? RequiredFunctionName { get; } /// - /// Initializes a new instance of the class that requires a specific function to be called. + /// Initializes a new instance of the class that requires a specific tool to be called. /// - /// The name of the function that must be called. + /// The name of the tool that must be called. /// is empty or composed entirely of whitespace. /// /// can be . However, it's preferable to use diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 3910040d0a0..88a224ab1c1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -6,44 +6,17 @@ using System.Threading; using System.Threading.Tasks; +#pragma warning disable SA1202 // Elements should be ordered by access + namespace Microsoft.Extensions.AI; /// Represents a function that can be described to an AI service and invoked. -public abstract class AIFunction : AITool +public abstract class AIFunction : AIFunctionDeclaration { - /// 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 a JSON Schema describing the function's return value. - /// - /// A typically reflects a function that doesn't specify a return schema - /// or a function that returns , , or . - /// - public virtual JsonElement? ReturnJsonSchema => null; + /// Initializes a new instance of the class. + protected AIFunction() + { + } /// /// Gets the underlying that this might be wrapping. @@ -72,4 +45,14 @@ public abstract class AIFunction : AITool protected abstract ValueTask InvokeCoreAsync( AIFunctionArguments arguments, CancellationToken cancellationToken); + + /// Creates a representation of this that can't be invoked. + /// The created instance. + /// + /// derives from , layering on the ability to invoke the function in addition + /// to describing it. creates a new object that describes the function but that can't be invoked. + /// + public AIFunctionDeclaration AsDeclarationOnly() => new NonInvocableAIFunction(this); + + private sealed class NonInvocableAIFunction(AIFunction function) : DelegatingAIFunctionDeclaration(function); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs new file mode 100644 index 00000000000..74e1242023a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs @@ -0,0 +1,58 @@ +// 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 System.Threading.Tasks; + +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods + +namespace Microsoft.Extensions.AI; + +/// Represents a function that can be described to an AI service. +/// +/// is the base class for , which +/// adds the ability to invoke the function. Components may type test instances +/// for to determine whether they can be described as functions, +/// and may type test for to determine whether they can be invoked. +/// +public abstract class AIFunctionDeclaration : AITool +{ + /// Initializes a new instance of the class. + protected AIFunctionDeclaration() + { + } + + /// 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 a JSON Schema describing the function's return value. + /// + /// A typically reflects a function that doesn't specify a return schema + /// or a function that returns , , or . + /// + public virtual JsonElement? ReturnJsonSchema => null; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 0d7de9a341e..a613f6b693d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -49,7 +49,7 @@ public static partial class AIFunctionFactory /// /// By default, any parameters to are sourced from the 's dictionary /// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned 's - /// . There are a few exceptions to this: + /// . There are a few exceptions to this: /// /// /// @@ -131,7 +131,7 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio /// /// Any parameters to are sourced from the 's dictionary /// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned 's - /// . There are a few exceptions to this: + /// . There are a few exceptions to this: /// /// /// @@ -212,7 +212,7 @@ public static AIFunction Create(Delegate method, string? name = null, string? de /// /// By default, any parameters to are sourced from the 's dictionary /// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned 's - /// . There are a few exceptions to this: + /// . There are a few exceptions to this: /// /// /// @@ -304,7 +304,7 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// /// Any parameters to are sourced from the 's dictionary /// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned 's - /// . There are a few exceptions to this: + /// . There are a few exceptions to this: /// /// /// @@ -398,7 +398,7 @@ public static AIFunction Create(MethodInfo method, object? target, string? name /// /// By default, any parameters to are sourced from the 's dictionary /// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned 's - /// . There are a few exceptions to this: + /// . There are a few exceptions to this: /// /// /// @@ -467,6 +467,39 @@ public static AIFunction Create( AIFunctionFactoryOptions? options = null) => ReflectionAIFunction.Build(method, createInstanceFunc, options ?? _defaultOptions); + /// Creates an using the specified parameters as the implementation of its corresponding properties. + /// The name of the function. + /// A description of the function, suitable for use in describing the purpose to a model. + /// A JSON schema describing the function and its input parameters. + /// A JSON schema describing the function's return value. + /// The created that describes a function. + /// is . + /// + /// creates an that can be used to describe a function + /// but not invoke it. To create an invocable , use Create. A non-invocable + /// may also be created from an invocable using that function's method. + /// + public static AIFunctionDeclaration CreateDeclaration( + string name, + string? description, + JsonElement jsonSchema, + JsonElement? returnJsonSchema = null) => + new DefaultAIFunctionDeclaration( + Throw.IfNullOrEmpty(name), + description ?? string.Empty, + jsonSchema, + returnJsonSchema); + + private sealed class DefaultAIFunctionDeclaration( + string name, string description, JsonElement jsonSchema, JsonElement? returnJsonSchema) : + AIFunctionDeclaration + { + public override string Name => name; + public override string Description => description; + public override JsonElement JsonSchema => jsonSchema; + public override JsonElement? ReturnJsonSchema => returnJsonSchema; + } + private sealed class ReflectionAIFunction : AIFunction { public static ReflectionAIFunction Build(MethodInfo method, object? target, AIFunctionFactoryOptions options) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index ebfffc34908..2bb9841a65e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -107,14 +107,14 @@ public AIFunctionFactoryOptions() public Func>? MarshalResult { get; set; } /// - /// Gets or sets a value indicating whether a schema should be created for the function's result type, if possible, and included as . + /// Gets or sets a value indicating whether a schema should be created for the function's result type, if possible, and included as . /// /// /// /// The default value is . /// /// - /// When set to , results in the produced to always be . + /// When set to , results in the produced to always be . /// /// public bool ExcludeResultSchema { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs new file mode 100644 index 00000000000..874745610c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs @@ -0,0 +1,48 @@ +// 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 Microsoft.Shared.Diagnostics; + +#pragma warning disable SA1202 // Elements should be ordered by access + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an optional base class for an that passes through calls to another instance. +/// +internal class DelegatingAIFunctionDeclaration : AIFunctionDeclaration // could be made public in the future if there's demand +{ + /// + /// Initializes a new instance of the class as a wrapper around . + /// + /// The inner AI function to which all calls are delegated by default. + /// is . + protected DelegatingAIFunctionDeclaration(AIFunctionDeclaration innerFunction) + { + InnerFunction = Throw.IfNull(innerFunction); + } + + /// Gets the inner . + protected AIFunctionDeclaration InnerFunction { get; } + + /// + public override string Name => InnerFunction.Name; + + /// + public override string Description => InnerFunction.Description; + + /// + public override JsonElement JsonSchema => InnerFunction.JsonSchema; + + /// + public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema; + + /// + public override IReadOnlyDictionary AdditionalProperties => InnerFunction.AdditionalProperties; + + /// + public override string ToString() => InnerFunction.ToString(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 92a5b51d6f7..ca42fdacd20 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -172,7 +172,7 @@ ] }, { - "Type": "abstract class Microsoft.Extensions.AI.AIFunction : Microsoft.Extensions.AI.AITool", + "Type": "abstract class Microsoft.Extensions.AI.AIFunction : Microsoft.Extensions.AI.AIFunctionDeclaration", "Stage": "Stable", "Methods": [ { @@ -186,23 +186,39 @@ { "Member": "abstract System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.AIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration Microsoft.Extensions.AI.AIFunction.AsDeclarationOnly();", + "Stage": "Stable" } ], "Properties": [ { - "Member": "virtual System.Text.Json.JsonElement Microsoft.Extensions.AI.AIFunction.JsonSchema { get; }", + "Member": "virtual System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.AIFunction.JsonSerializerOptions { get; }", "Stage": "Stable" }, { - "Member": "virtual System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.AIFunction.JsonSerializerOptions { get; }", + "Member": "virtual System.Reflection.MethodInfo? Microsoft.Extensions.AI.AIFunction.UnderlyingMethod { get; }", "Stage": "Stable" - }, + } + ] + }, + { + "Type": "abstract class Microsoft.Extensions.AI.AIFunctionDeclaration : Microsoft.Extensions.AI.AITool", + "Stage": "Stable", + "Methods": [ { - "Member": "virtual System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIFunction.ReturnJsonSchema { get; }", + "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration.AIFunctionDeclaration();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "virtual System.Text.Json.JsonElement Microsoft.Extensions.AI.AIFunctionDeclaration.JsonSchema { get; }", "Stage": "Stable" }, { - "Member": "virtual System.Reflection.MethodInfo? Microsoft.Extensions.AI.AIFunction.UnderlyingMethod { get; }", + "Member": "virtual System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIFunctionDeclaration.ReturnJsonSchema { get; }", "Stage": "Stable" } ] @@ -306,6 +322,10 @@ { "Member": "static Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.AIFunctionFactory.Create(System.Reflection.MethodInfo method, System.Func createInstanceFunc, Microsoft.Extensions.AI.AIFunctionFactoryOptions? options = null);", "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.AIFunctionDeclaration Microsoft.Extensions.AI.AIFunctionFactory.CreateDeclaration(string name, string? description, System.Text.Json.JsonElement jsonSchema, System.Text.Json.JsonElement? returnJsonSchema = null);", + "Stage": "Stable" } ] }, @@ -513,6 +533,10 @@ "Member": "System.Text.Json.JsonElement Microsoft.Extensions.AI.AIJsonSchemaTransformCache.GetOrCreateTransformedSchema(Microsoft.Extensions.AI.AIFunction function);", "Stage": "Stable" }, + { + "Member": "System.Text.Json.JsonElement Microsoft.Extensions.AI.AIJsonSchemaTransformCache.GetOrCreateTransformedSchema(Microsoft.Extensions.AI.AIFunctionDeclaration function);", + "Stage": "Stable" + }, { "Member": "System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIJsonSchemaTransformCache.GetOrCreateTransformedSchema(Microsoft.Extensions.AI.ChatResponseFormatJson responseFormat);", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs index a1aaeff26ac..198fd33a9d4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.Json; using Microsoft.Shared.Diagnostics; @@ -23,10 +24,10 @@ namespace Microsoft.Extensions.AI; /// public sealed class AIJsonSchemaTransformCache { - private readonly ConditionalWeakTable _functionSchemaCache = new(); + private readonly ConditionalWeakTable _functionSchemaCache = new(); private readonly ConditionalWeakTable _responseFormatCache = new(); - private readonly ConditionalWeakTable.CreateValueCallback _functionSchemaCreateValueCallback; + private readonly ConditionalWeakTable.CreateValueCallback _functionSchemaCreateValueCallback; private readonly ConditionalWeakTable.CreateValueCallback _responseFormatCreateValueCallback; /// @@ -57,7 +58,16 @@ public AIJsonSchemaTransformCache(AIJsonSchemaTransformOptions transformOptions) /// /// The function whose JSON schema we want to transform. /// The transformed JSON schema corresponding to . - public JsonElement GetOrCreateTransformedSchema(AIFunction function) + [EditorBrowsable(EditorBrowsableState.Never)] // maintained for binary compat; functionality for AIFunction is satisfied by AIFunctionDeclaration overload + public JsonElement GetOrCreateTransformedSchema(AIFunction function) => + GetOrCreateTransformedSchema((AIFunctionDeclaration)function); + + /// + /// Gets or creates a transformed JSON schema for the specified instance. + /// + /// The function whose JSON schema we want to transform. + /// The transformed JSON schema corresponding to . + public JsonElement GetOrCreateTransformedSchema(AIFunctionDeclaration function) { _ = Throw.IfNull(function); return (JsonElement)_functionSchemaCache.GetValue(function, _functionSchemaCreateValueCallback); diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index 0fd8f4506db..b56604c027d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -343,7 +343,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon { foreach (AITool tool in tools) { - if (tool is AIFunction af) + if (tool is AIFunctionDeclaration af) { result.Tools.Add(ToAzureAIChatTool(af)); } @@ -410,7 +410,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon private static readonly BinaryData _falseString = BinaryData.FromString("false"); /// Converts an Extensions function to an AzureAI chat tool. - private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) + private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunctionDeclaration aiFunction) { // Map to an intermediate model so that redundant properties are skipped. var tool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(aiFunction), JsonContext.Default.AzureAIChatToolJson)!; diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index d39b2d60cc3..4ba77ddf8cc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## NOT YET RELEASED + +- Updated tool mapping to recognize any `AIFunctionDeclaration`. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + ## 9.8.0-preview.1.25412.6 - Updated to depend on Azure.AI.Inference 1.0.0-beta.5. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs index 3dbc8211416..dcba14f92c0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/AIToolExtensions.cs @@ -19,7 +19,7 @@ internal static string RenderAsJson( var toolDefinitionsJsonArray = new JsonArray(); - foreach (AIFunction function in toolDefinitions.OfType()) + foreach (AIFunctionDeclaration function in toolDefinitions.OfType()) { JsonNode functionJsonNode = new JsonObject diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs index 5960eb14aa0..5741adaff66 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs @@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs index c19cb5dcd71..c8f407a12d8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluatorContext.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// @@ -36,7 +36,7 @@ public sealed class IntentResolutionEvaluatorContext : EvaluationContext /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) @@ -55,7 +55,7 @@ public IntentResolutionEvaluatorContext(params AITool[] toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) @@ -81,7 +81,7 @@ public IntentResolutionEvaluatorContext(IEnumerable toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions that are supplied via + /// are defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs index fc97dcc0268..c9e189af365 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs @@ -24,7 +24,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs index c8e94d03b26..535306b5d4e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluatorContext.cs @@ -20,7 +20,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// @@ -37,7 +37,7 @@ public sealed class TaskAdherenceEvaluatorContext : EvaluationContext /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) @@ -56,7 +56,7 @@ public TaskAdherenceEvaluatorContext(params AITool[] toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) @@ -83,7 +83,7 @@ public TaskAdherenceEvaluatorContext(IEnumerable toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that are - /// defined as s. Any other definitions that are supplied via + /// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs index bed95eeb3a2..252b1254354 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs @@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs index 037d811e0f4..7b01b3f50eb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluatorContext.cs @@ -21,7 +21,7 @@ namespace Microsoft.Extensions.AI.Evaluation.Quality; /// /// /// Note that at the moment, only supports evaluating calls to tools that are -/// defined as s. Any other definitions that are supplied via +/// defined as s. Any other definitions that are supplied via /// will be ignored. /// /// @@ -38,7 +38,7 @@ public sealed class ToolCallAccuracyEvaluatorContext : EvaluationContext /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) @@ -57,7 +57,7 @@ public ToolCallAccuracyEvaluatorContext(params AITool[] toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions will be ignored. + /// are defined as s. Any other definitions will be ignored. /// /// public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) @@ -85,7 +85,7 @@ public ToolCallAccuracyEvaluatorContext(IEnumerable toolDefinitions) /// /// /// Note that at the moment, only supports evaluating calls to tools that - /// are defined as s. Any other definitions that are supplied via + /// are defined as s. Any other definitions that are supplied via /// will be ignored. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 143c439bda7..1c1c175ccd4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## NOT YET RELEASED + +- Updated tool mappings to recognize any `AIFunctionDeclaration`. +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + ## 9.8.0-preview.1.25412.6 - Updated to depend on OpenAI 2.3.0. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs index 900883c6d43..793c906bd9d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIAssistantsExtensions.cs @@ -10,10 +10,10 @@ namespace OpenAI.Assistants; /// Provides extension methods for working with content associated with OpenAI.Assistants. public static class MicrosoftExtensionsAIAssistantsExtensions { - /// Creates an OpenAI from an . + /// Creates an OpenAI from an . /// The function to convert. /// An OpenAI representing . /// is . - public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) => + public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunctionDeclaration function) => OpenAIAssistantsChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function)); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs index 0385d318842..113e91c3305 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -19,11 +19,11 @@ namespace OpenAI.Chat; /// Provides extension methods for working with content associated with OpenAI.Chat. public static class MicrosoftExtensionsAIChatExtensions { - /// Creates an OpenAI from an . + /// Creates an OpenAI from an . /// The function to convert. /// An OpenAI representing . /// is . - public static ChatTool AsOpenAIChatTool(this AIFunction function) => + public static ChatTool AsOpenAIChatTool(this AIFunctionDeclaration function) => OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function)); /// Creates a sequence of OpenAI instances from the specified input messages. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs index ad180e96b1e..903c6253dde 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs @@ -10,10 +10,10 @@ namespace OpenAI.Realtime; /// Provides extension methods for working with content associated with OpenAI.Realtime. public static class MicrosoftExtensionsAIRealtimeExtensions { - /// Creates an OpenAI from an . + /// Creates an OpenAI from an . /// The function to convert. /// An OpenAI representing . /// is . - public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) => + public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunctionDeclaration function) => OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 188f5df3e52..d6b290f431b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -14,11 +14,11 @@ namespace OpenAI.Responses; /// Provides extension methods for working with content associated with OpenAI.Responses. public static class MicrosoftExtensionsAIResponsesExtensions { - /// Creates an OpenAI from an . + /// Creates an OpenAI from an . /// The function to convert. /// An OpenAI representing . /// is . - public static ResponseTool AsOpenAIResponseTool(this AIFunction function) => + public static ResponseTool AsOpenAIResponseTool(this AIFunctionDeclaration function) => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function)); /// Creates a sequence of OpenAI instances from the specified input messages. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index ed7f8403cb7..01b78994f38 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -286,7 +286,7 @@ void IDisposable.Dispose() } /// Converts an Extensions function to an OpenAI assistants function tool. - internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null) + internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? @@ -348,7 +348,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( { switch (tool) { - case AIFunction aiFunction: + case AIFunctionDeclaration aiFunction: runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 7bffbc79d10..415aef5901e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -101,7 +101,7 @@ void IDisposable.Dispose() } /// Converts an Extensions function to an OpenAI chat tool. - internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? options = null) + internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? @@ -564,7 +564,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) { foreach (AITool tool in tools) { - if (tool is AIFunction af) + if (tool is AIFunctionDeclaration af) { result.Tools.Add(ToOpenAIChatTool(af, options)); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index a5739fdb4ac..19fa835851f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -177,8 +177,8 @@ public static IEmbeddingGenerator> AsIEmbeddingGenerato strictObj is bool strictValue ? strictValue : null; - /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. - internal static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, bool? strict) + /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. + internal static BinaryData ToOpenAIFunctionParameters(AIFunctionDeclaration aiFunction, bool? strict) { // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting. JsonElement jsonSchema = strict is true ? diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs index 7c944ac5edb..dbbabea026f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI; /// Provides helpers for interacting with OpenAI Realtime. internal sealed class OpenAIRealtimeConversationClient { - public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction, ChatOptions? options = null) + public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index afb03b518d9..e1cd031f8a8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -336,7 +336,7 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } - internal static ResponseTool ToResponseTool(AIFunction aiFunction, ChatOptions? options = null) + internal static ResponseTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? @@ -399,7 +399,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { switch (tool) { - case AIFunction aiFunction: + case AIFunctionDeclaration aiFunction: result.Tools.Add(ToResponseTool(aiFunction, options)); break; diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 213e3b8a60d..2c1a6179a69 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -2,6 +2,7 @@ ## NOT YET RELEASED +- Added `FunctionInvokingChatClient` support for non-invocable tools and `TerminateOnUnknownCalls` property. - Fixed `GetResponseAsync` to only look at the contents of the last message in the response. ## 9.8.0 diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 9c0506a2307..c585e007401 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -18,6 +18,7 @@ #pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test #pragma warning disable SA1202 // 'protected' members should come before 'private' members #pragma warning disable S107 // Methods should not have too many parameters +#pragma warning disable IDE0032 // Use auto property, suppressed until repo updates to C# 14 namespace Microsoft.Extensions.AI; @@ -215,6 +216,30 @@ public int MaximumConsecutiveErrorsPerRequest /// public IList? AdditionalTools { get; set; } + /// Gets or sets a value indicating whether a request to call an unknown function should terminate the function calling loop. + /// + /// + /// When , call requests to any tools that aren't available to the + /// will result in a response message automatically being created and returned to the inner client stating that the tool couldn't be + /// found; this can help in cases where a model hallucinates a function, but it's problematic if the model has been made aware + /// of the existence of tools outside of the normal mechanisms, and requests one of those. can be used + /// to help with that, but if instead the consumer wants to know about all function call requests that the client can't handle, + /// can be set to , and upon receiving a request to call a function + /// that the doesn't know about, it will terminate the function calling loop and return + /// the response, leaving the handling of the function call requests to the consumer of the client. + /// + /// + /// Note that s that the is aware of (e.g. because they're in + /// or ) but that aren't are not considered + /// unknown, just not invocable. Any requests to a non-invocable tool will also result in the function calling loop terminating, + /// regardless of . + /// + /// + /// This defaults to . + /// + /// + public bool TerminateOnUnknownCalls { get; set; } + /// Gets or sets a delegate used to invoke instances. /// /// By default, the protected method is called for each to be invoked, @@ -239,6 +264,7 @@ public override async Task GetResponseAsync( List originalMessages = [.. messages]; messages = originalMessages; + Dictionary? toolMap = null; // all available tools, indexed by name List? augmentedHistory = null; // the actual history of messages sent on turns other than the first ChatResponse? response = null; // the response from the inner client, which is possibly modified and then eventually returned List? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response @@ -260,14 +286,17 @@ public override async Task GetResponseAsync( // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents. bool requiresFunctionInvocation = - (options?.Tools is { Count: > 0 } || AdditionalTools is { Count: > 0 }) && iteration < MaximumIterationsPerRequest && CopyFunctionCalls(response.Messages, ref functionCallContents); - // In a common case where we make a request and there's no function calling work required, - // fast path out by just returning the original response. - if (iteration == 0 && !requiresFunctionInvocation) + if (requiresFunctionInvocation) + { + toolMap ??= CreateToolsDictionary(AdditionalTools, options?.Tools); + } + else if (iteration == 0) { + // In a common case where we make a request and there's no function calling work required, + // fast path out by just returning the original response. return response; } @@ -285,10 +314,10 @@ public override async Task GetResponseAsync( } } - // If there are no tools to call, or for any other reason we should stop, we're done. - // Break out of the loop and allow the handling at the end to configure the response - // with aggregated data from previous requests. - if (!requiresFunctionInvocation) + // If there's nothing more to do, break out of the loop and allow the handling at the + // end to configure the response with aggregated data from previous requests. + if (!requiresFunctionInvocation || + ShouldTerminateLoopBasedOnHandleableFunctions(functionCallContents, toolMap)) { break; } @@ -298,7 +327,7 @@ public override async Task GetResponseAsync( // Add the responses from the function calls into the augmented history and also into the tracked // list of response messages. - var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken); + var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken); responseMessages.AddRange(modeAndMessages.MessagesAdded); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; @@ -335,6 +364,7 @@ public override async IAsyncEnumerable GetStreamingResponseA List originalMessages = [.. messages]; messages = originalMessages; + Dictionary? toolMap = null; // all available tools, indexed by name List? augmentedHistory = null; // the actual history of messages sent on turns other than the first List? functionCallContents = null; // function call contents that need responding to in the current turn List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history @@ -375,10 +405,10 @@ public override async IAsyncEnumerable GetStreamingResponseA Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802 } - // If there are no tools to call, or for any other reason we should stop, return the response. - if (functionCallContents is not { Count: > 0 } || - (options?.Tools is not { Count: > 0 } && AdditionalTools is not { Count: > 0 }) || - iteration >= _maximumIterationsPerRequest) + // If there's nothing more to do, break out of the loop and allow the handling at the + // end to configure the response with aggregated data from previous requests. + if (iteration >= MaximumIterationsPerRequest || + ShouldTerminateLoopBasedOnHandleableFunctions(functionCallContents, toolMap ??= CreateToolsDictionary(AdditionalTools, options?.Tools))) { break; } @@ -391,7 +421,7 @@ public override async IAsyncEnumerable GetStreamingResponseA FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadConversationId); // Process all of the functions, adding their results into the history. - var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken); + var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken); responseMessages.AddRange(modeAndMessages.MessagesAdded); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; @@ -513,6 +543,31 @@ private static void FixupHistories( messages = augmentedHistory; } + /// Creates a dictionary mapping tool names to the corresponding tools. + /// + /// The lists of tools to combine into a single dictionary. Tools from later lists are preferred + /// over tools from earlier lists if they have the same name. + /// + private static Dictionary? CreateToolsDictionary(params ReadOnlySpan?> toolLists) + { + Dictionary? tools = null; + + foreach (var toolList in toolLists) + { + if (toolList?.Count is int count && count > 0) + { + tools ??= new(StringComparer.Ordinal); + for (int i = 0; i < count; i++) + { + AITool tool = toolList[i]; + tools[tool.Name] = tool; + } + } + } + + return tools; + } + /// Copies any from to . private static bool CopyFunctionCalls( IList messages, [NotNullWhen(true)] ref List? functionCalls) @@ -571,11 +626,59 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions? options, stri } } + /// Gets whether the function calling loop should exit based on the function call requests. + /// The call requests. + /// The map from tool names to tools. + private bool ShouldTerminateLoopBasedOnHandleableFunctions(List? functionCalls, Dictionary? toolMap) + { + if (functionCalls is not { Count: > 0 }) + { + // There are no functions to call, so there's no reason to keep going. + return true; + } + + if (toolMap is not { Count: > 0 }) + { + // There are functions to call but we have no tools, so we can't handle them. + // If we're configured to terminate on unknown call requests, do so now. + // Otherwise, ProcessFunctionCallsAsync will handle it by creating a NotFound response message. + return TerminateOnUnknownCalls; + } + + // At this point, we have both function call requests and some tools. + // Look up each function. + foreach (var fcc in functionCalls) + { + if (toolMap.TryGetValue(fcc.Name, out var tool)) + { + if (tool is not AIFunction) + { + // The tool was found but it's not invocable. Regardless of TerminateOnUnknownCallRequests, + // we need to break out of the loop so that callers can handle all the call requests. + return true; + } + } + else + { + // The tool couldn't be found. If we're configured to terminate on unknown call requests, + // break out of the loop now. Otherwise, ProcessFunctionCallsAsync will handle it by + // creating a NotFound response message. + if (TerminateOnUnknownCalls) + { + return true; + } + } + } + + return false; + } + /// /// Processes the function calls in the list. /// /// The current chat contents, inclusive of the function call contents being processed. /// The options used for the response being processed. + /// Map from tool name to tool. /// The function call contents representing the functions to be invoked. /// The iteration number of how many roundtrips have been made to the inner client. /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors. @@ -583,7 +686,8 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions? options, stri /// The to monitor for cancellation requests. /// A value indicating how the caller should proceed. private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync( - List messages, ChatOptions? options, List functionCallContents, int iteration, int consecutiveErrorCount, + List messages, ChatOptions? options, + Dictionary? toolMap, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken) { // We must add a response for every tool call, regardless of whether we successfully executed it or not. @@ -591,13 +695,13 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions? options, stri Debug.Assert(functionCallContents.Count > 0, "Expected at least one function call."); var shouldTerminate = false; - var captureCurrentIterationExceptions = consecutiveErrorCount < _maximumConsecutiveErrorsPerRequest; + var captureCurrentIterationExceptions = consecutiveErrorCount < MaximumConsecutiveErrorsPerRequest; // Process all functions. If there's more than one and concurrent invocation is enabled, do so in parallel. if (functionCallContents.Count == 1) { FunctionInvocationResult result = await ProcessFunctionCallAsync( - messages, options, functionCallContents, + messages, options, toolMap, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken); IList addedMessages = CreateResponseMessages([result]); @@ -620,7 +724,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions? options, stri results.AddRange(await Task.WhenAll( from callIndex in Enumerable.Range(0, functionCallContents.Count) select ProcessFunctionCallAsync( - messages, options, functionCallContents, + messages, options, toolMap, functionCallContents, iteration, callIndex, captureExceptions: true, isStreaming, cancellationToken))); shouldTerminate = results.Any(r => r.Terminate); @@ -631,7 +735,7 @@ select ProcessFunctionCallAsync( for (int callIndex = 0; callIndex < functionCallContents.Count; callIndex++) { var functionResult = await ProcessFunctionCallAsync( - messages, options, functionCallContents, + messages, options, toolMap, functionCallContents, iteration, callIndex, captureCurrentIterationExceptions, isStreaming, cancellationToken); results.Add(functionResult); @@ -670,7 +774,7 @@ private void UpdateConsecutiveErrorCountOrThrow(IList added, ref in if (allExceptions.Any()) { consecutiveErrorCount++; - if (consecutiveErrorCount > _maximumConsecutiveErrorsPerRequest) + if (consecutiveErrorCount > MaximumConsecutiveErrorsPerRequest) { var allExceptionsArray = allExceptions.ToArray(); if (allExceptionsArray.Length == 1) @@ -704,6 +808,7 @@ private void ThrowIfNoFunctionResultsAdded(IList? messages) /// Processes the function call described in []. /// The current chat contents, inclusive of the function call contents being processed. /// The options used for the response being processed. + /// Map from tool name to tool. /// The function call contents representing all the functions being invoked. /// The iteration number of how many roundtrips have been made to the inner client. /// The 0-based index of the function being called out of . @@ -712,14 +817,16 @@ private void ThrowIfNoFunctionResultsAdded(IList? messages) /// The to monitor for cancellation requests. /// A value indicating how the caller should proceed. private async Task ProcessFunctionCallAsync( - List messages, ChatOptions? options, List callContents, + List messages, ChatOptions? options, + Dictionary? toolMap, List callContents, int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken) { var callContent = callContents[functionCallIndex]; // Look up the AIFunction for the function call. If the requested function isn't available, send back an error. - AIFunction? aiFunction = FindAIFunction(options?.Tools, callContent.Name) ?? FindAIFunction(AdditionalTools, callContent.Name); - if (aiFunction is null) + if (toolMap is null || + !toolMap.TryGetValue(callContent.Name, out AITool? tool) || + tool is not AIFunction aiFunction) { return new(terminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null); } @@ -763,23 +870,6 @@ private async Task ProcessFunctionCallAsync( callContent, result, exception: null); - - static AIFunction? FindAIFunction(IList? tools, string functionName) - { - if (tools is not null) - { - int count = tools.Count; - for (int i = 0; i < count; i++) - { - if (tools[i] is AIFunction function && function.Name == functionName) - { - return function; - } - } - } - - return null; - } } /// Creates one or more response messages for function invocation results. diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json index 3e3f0426dd1..45b72f0aad1 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.json @@ -546,6 +546,10 @@ { "Member": "int Microsoft.Extensions.AI.FunctionInvokingChatClient.MaximumIterationsPerRequest { get; set; }", "Stage": "Stable" + }, + { + "Member": "bool Microsoft.Extensions.AI.FunctionInvokingChatClient.TerminateOnUnknownCalls { get; set; }", + "Stage": "Stable" } ] }, diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 08cb5ee5760..b9a71849034 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -1062,6 +1062,121 @@ public async Task FunctionInvocations_InvokedOnOriginalSynchronizationContext() await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configurePipeline); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task TerminateOnUnknownCalls_ControlsBehaviorForUnknownFunctions(bool terminateOnUnknown) + { + ChatOptions options = new() + { + Tools = [AIFunctionFactory.Create((int i) => $"Known: {i}", "KnownFunc")] + }; + + Func configure = b => b.Use( + s => new FunctionInvokingChatClient(s) { TerminateOnUnknownCalls = terminateOnUnknown }); + + if (!terminateOnUnknown) + { + List planForContinue = + [ + new(ChatRole.User, "hello"), + new(ChatRole.Assistant, [ + new FunctionCallContent("callId1", "UnknownFunc", new Dictionary { ["i"] = 1 }), + new FunctionCallContent("callId2", "KnownFunc", new Dictionary { ["i"] = 2 }) + ]), + new(ChatRole.Tool, [ + new FunctionResultContent("callId1", result: "Error: Requested function \"UnknownFunc\" not found."), + new FunctionResultContent("callId2", result: "Known: 2") + ]), + new(ChatRole.Assistant, "done"), + ]; + + await InvokeAndAssertAsync(options, planForContinue, configurePipeline: configure); + await InvokeAndAssertStreamingAsync(options, planForContinue, configurePipeline: configure); + } + else + { + List fullPlanWithUnknown = + [ + new(ChatRole.User, "hello"), + new(ChatRole.Assistant, [ + new FunctionCallContent("callId1", "UnknownFunc", new Dictionary { ["i"] = 1 }), + new FunctionCallContent("callId2", "KnownFunc", new Dictionary { ["i"] = 2 }) + ]), + new(ChatRole.Tool, [ + new FunctionResultContent("callId1", result: "Error: Requested function \"UnknownFunc\" not found."), + new FunctionResultContent("callId2", result: "Known: 2") + ]), + new(ChatRole.Assistant, "done"), + ]; + + var expected = fullPlanWithUnknown.Take(2).ToList(); + await InvokeAndAssertAsync(options, fullPlanWithUnknown, expected, configure); + await InvokeAndAssertStreamingAsync(options, fullPlanWithUnknown, expected, configure); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RequestsWithOnlyFunctionDeclarations_TerminatesRegardlessOfTerminateOnUnknownCalls(bool terminateOnUnknown) + { + var declarationOnly = AIFunctionFactory.Create(() => "unused", "DefOnly").AsDeclarationOnly(); + + ChatOptions options = new() { Tools = [declarationOnly] }; + + List fullPlan = + [ + new(ChatRole.User, "hello"), + new(ChatRole.Assistant, [new FunctionCallContent("callId1", "DefOnly")]), + new(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Should not be produced")]), + new(ChatRole.Assistant, "world"), + ]; + + List expected = fullPlan.Take(2).ToList(); + + Func configure = b => b.Use( + s => new FunctionInvokingChatClient(s) { TerminateOnUnknownCalls = terminateOnUnknown }); + + await InvokeAndAssertAsync(options, fullPlan, expected, configure); + await InvokeAndAssertStreamingAsync(options, fullPlan, expected, configure); + } + + [Fact] + public async Task MixedKnownFunctionAndDeclaration_TerminatesWithoutInvokingKnown() + { + int invoked = 0; + var known = AIFunctionFactory.Create(() => { invoked++; return "OK"; }, "Known"); + var defOnly = AIFunctionFactory.Create(() => "unused", "DefOnly").AsDeclarationOnly(); + + var options = new ChatOptions + { + Tools = [known, defOnly] + }; + + List fullPlan = + [ + new(ChatRole.User, "hi"), + new(ChatRole.Assistant, [ + new FunctionCallContent("callId1", "Known"), + new FunctionCallContent("callId2", "DefOnly") + ]), + new(ChatRole.Tool, [new FunctionResultContent("callId1", result: "OK"), new FunctionResultContent("callId2", result: "nope")]), + new(ChatRole.Assistant, "done"), + ]; + + List expected = fullPlan.Take(2).ToList(); + + Func configure = b => b.Use(s => new FunctionInvokingChatClient(s) { TerminateOnUnknownCalls = false }); + await InvokeAndAssertAsync(options, fullPlan, expected, configure); + Assert.Equal(0, invoked); + + invoked = 0; + configure = b => b.Use(s => new FunctionInvokingChatClient(s) { TerminateOnUnknownCalls = true }); + await InvokeAndAssertStreamingAsync(options, fullPlan, expected, configure); + Assert.Equal(0, invoked); + } + private sealed class CustomSynchronizationContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object? state) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 69787dc868b..7e61ad745c4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -931,6 +931,26 @@ public void AIFunctionFactory_ReturnTypeWithDescriptionAttribute() static int Add(int a, int b) => a + b; } + [Fact] + public void CreateDeclaration_Roundtrips() + { + JsonElement schema = AIJsonUtilities.CreateJsonSchema(typeof(int), serializerOptions: AIJsonUtilities.DefaultOptions); + + AIFunctionDeclaration f = AIFunctionFactory.CreateDeclaration("something", "amazing", schema); + Assert.Equal("something", f.Name); + Assert.Equal("amazing", f.Description); + Assert.Equal("""{"type":"integer"}""", f.JsonSchema.ToString()); + Assert.Null(f.ReturnJsonSchema); + + f = AIFunctionFactory.CreateDeclaration("other", null, default, schema); + Assert.Equal("other", f.Name); + Assert.Empty(f.Description); + Assert.Equal(default, f.JsonSchema); + Assert.Equal("""{"type":"integer"}""", f.ReturnJsonSchema.ToString()); + + Assert.Throws("name", () => AIFunctionFactory.CreateDeclaration(null!, "description", default)); + } + private sealed class MyService(int value) { public int Value => value; From 20349f88a5e8fb5a3dd0aa8a1f7920ebb5c75f9d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 3 Sep 2025 07:33:24 -0400 Subject: [PATCH 277/472] Update to genai standard convention 1.37 (#6767) --- .../Microsoft.Extensions.AI/CHANGELOG.md | 1 + .../FunctionInvokingChatClient.cs | 5 +- .../ChatCompletion/OpenTelemetryChatClient.cs | 278 +++++++++--------- .../OpenTelemetryEmbeddingGenerator.cs | 24 +- .../OpenTelemetryConsts.cs | 45 ++- .../OpenTelemetryChatClientTests.cs | 139 ++++++--- .../OpenTelemetryEmbeddingGeneratorTests.cs | 10 +- 7 files changed, 273 insertions(+), 229 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 2c1a6179a69..6d0c1a3818d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -3,6 +3,7 @@ ## NOT YET RELEASED - Added `FunctionInvokingChatClient` support for non-invocable tools and `TerminateOnUnknownCalls` property. +- Updated the Open Telemetry instrumentation to conform to the latest 1.37.0 draft specification of the Semantic Conventions for Generative AI systems. - Fixed `GetResponseAsync` to only look at the contents of the last message in the response. ## 9.8.0 diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index c585e007401..d503ef84630 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -932,7 +932,8 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul ActivityKind.Internal, default(ActivityContext), [ - new(OpenTelemetryConsts.GenAI.Operation.Name, "execute_tool"), + new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ExecuteTool), + new(OpenTelemetryConsts.GenAI.Tool.Type, OpenTelemetryConsts.ToolTypeFunction), new(OpenTelemetryConsts.GenAI.Tool.Call.Id, context.CallContent.CallId), new(OpenTelemetryConsts.GenAI.Tool.Name, context.Function.Name), new(OpenTelemetryConsts.GenAI.Tool.Description, context.Function.Description), @@ -962,7 +963,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul { if (activity is not null) { - _ = activity.SetTag("error.type", e.GetType().FullName) + _ = activity.SetTag(OpenTelemetryConsts.Error.Type, e.GetType().FullName) .SetStatus(ActivityStatusCode.Error, e.Message); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 232475b428c..234e56a68bc 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -4,17 +4,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Diagnostics; #pragma warning disable S3358 // Ternary operators should not be nested @@ -25,22 +23,19 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.36, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.37, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed partial class OpenTelemetryChatClient : DelegatingChatClient { - private const LogLevel EventLogLevel = LogLevel.Information; - private readonly ActivitySource _activitySource; private readonly Meter _meter; - private readonly ILogger _logger; private readonly Histogram _tokenUsageHistogram; private readonly Histogram _operationDurationHistogram; private readonly string? _defaultModelId; - private readonly string? _system; + private readonly string? _providerName; private readonly string? _serverAddress; private readonly int _serverPort; @@ -48,19 +43,19 @@ public sealed partial class OpenTelemetryChatClient : DelegatingChatClient /// Initializes a new instance of the class. /// The underlying . - /// The to use for emitting events. + /// The to use for emitting any logging data from the client. /// An optional source name that will be used on the telemetry data. +#pragma warning disable IDE0060 // Remove unused parameter; it exists for backwards compatibility and future use public OpenTelemetryChatClient(IChatClient innerClient, ILogger? logger = null, string? sourceName = null) +#pragma warning restore IDE0060 : base(innerClient) { Debug.Assert(innerClient is not null, "Should have been validated by the base ctor"); - _logger = logger ?? NullLogger.Instance; - if (innerClient!.GetService() is ChatClientMetadata metadata) { _defaultModelId = metadata.DefaultModelId; - _system = metadata.ProviderName; + _providerName = metadata.ProviderName; _serverAddress = metadata.ProviderUri?.Host; _serverPort = metadata.ProviderUri?.Port ?? 0; } @@ -139,7 +134,7 @@ public override async Task GetResponseAsync( Stopwatch? stopwatch = _operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null; string? requestModelId = options?.ModelId ?? _defaultModelId; - LogChatMessages(messages); + AddInputMessagesTags(messages, options, activity); ChatResponse? response = null; Exception? error = null; @@ -170,7 +165,7 @@ public override async IAsyncEnumerable GetStreamingResponseA Stopwatch? stopwatch = _operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null; string? requestModelId = options?.ModelId ?? _defaultModelId; - LogChatMessages(messages); + AddInputMessagesTags(messages, options, activity); IAsyncEnumerable updates; try @@ -219,6 +214,70 @@ public override async IAsyncEnumerable GetStreamingResponseA } } + private static string SerializeChatMessages(IEnumerable messages, ChatFinishReason? chatFinishReason = null) + { + List output = []; + + string? finishReason = + chatFinishReason?.Value is null ? null : + chatFinishReason == ChatFinishReason.Length ? "length" : + chatFinishReason == ChatFinishReason.ContentFilter ? "content_filter" : + chatFinishReason == ChatFinishReason.ToolCalls ? "tool_call" : + "stop"; + + foreach (ChatMessage message in messages) + { + OtelMessage m = new() + { + FinishReason = finishReason, + Role = + message.Role == ChatRole.Assistant ? "assistant" : + message.Role == ChatRole.Tool ? "tool" : + message.Role == ChatRole.System || message.Role == new ChatRole("developer") ? "system" : + "user", + }; + + foreach (AIContent content in message.Contents) + { + switch (content) + { + case FunctionCallContent fcc: + m.Parts.Add(new OtelToolCallRequestPart + { + Id = fcc.CallId, + Name = fcc.Name, + Arguments = fcc.Arguments, + }); + break; + + case FunctionResultContent frc: + m.Parts.Add(new OtelToolCallResponsePart + { + Id = frc.CallId, + Response = frc.Result, + }); + break; + + case TextContent tc: + m.Parts.Add(new OtelGenericPart { Content = tc.Text }); + break; + + default: + m.Parts.Add(new OtelGenericPart + { + Type = content.GetType().FullName!, + Content = content, + }); + break; + } + } + + output.Add(m); + } + + return JsonSerializer.Serialize(output, _defaultOptions.GetTypeInfo(typeof(IList))); + } + /// Creates an activity for a chat request, or returns if not enabled. private Activity? CreateAndConfigureActivity(ChatOptions? options) { @@ -236,7 +295,7 @@ public override async IAsyncEnumerable GetStreamingResponseA _ = activity .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat) .AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId) - .AddTag(OpenTelemetryConsts.GenAI.SystemName, _system); + .AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); if (_serverAddress is not null) { @@ -272,7 +331,7 @@ public override async IAsyncEnumerable GetStreamingResponseA _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.Seed, seed); } - if (options.StopSequences is IList stopSequences) + if (options.StopSequences is IList { Count: > 0 } stopSequences) { _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.StopSequences, $"[{string.Join(", ", stopSequences.Select(s => $"\"{s}\""))}]"); } @@ -297,15 +356,15 @@ public override async IAsyncEnumerable GetStreamingResponseA switch (options.ResponseFormat) { case ChatResponseFormatText: - _ = activity.AddTag(OpenTelemetryConsts.GenAI.Output.Type, "text"); + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeText); break; case ChatResponseFormatJson: - _ = activity.AddTag(OpenTelemetryConsts.GenAI.Output.Type, "json"); + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeJson); break; } } - if (_system is not null) + if (_providerName is not null) { // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data if (EnableSensitiveData && options.AdditionalProperties is { } props) @@ -316,7 +375,7 @@ public override async IAsyncEnumerable GetStreamingResponseA foreach (KeyValuePair prop in props) { _ = activity.AddTag( - OpenTelemetryConsts.GenAI.Request.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + OpenTelemetryConsts.GenAI.Request.PerProvider(_providerName, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), prop.Value); } } @@ -354,7 +413,7 @@ private void TraceResponse( if (usage.InputTokenCount is long inputTokens) { TagList tags = default; - tags.Add(OpenTelemetryConsts.GenAI.Token.Type, "input"); + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); AddMetricTags(ref tags, requestModelId, response); _tokenUsageHistogram.Record((int)inputTokens); } @@ -362,7 +421,7 @@ private void TraceResponse( if (usage.OutputTokenCount is long outputTokens) { TagList tags = default; - tags.Add(OpenTelemetryConsts.GenAI.Token.Type, "output"); + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeOutput); AddMetricTags(ref tags, requestModelId, response); _tokenUsageHistogram.Record((int)outputTokens); } @@ -377,7 +436,7 @@ private void TraceResponse( if (response is not null) { - LogChatResponse(response); + AddOutputMessagesTags(response, activity); if (activity is not null) { @@ -408,7 +467,7 @@ private void TraceResponse( _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.OutputTokens, (int)outputTokens); } - if (_system is not null) + if (_providerName is not null) { // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data if (EnableSensitiveData && response.AdditionalProperties is { } props) @@ -419,7 +478,7 @@ private void TraceResponse( foreach (KeyValuePair prop in props) { _ = activity.AddTag( - OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + OpenTelemetryConsts.GenAI.Response.PerProvider(_providerName, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), prop.Value); } } @@ -436,7 +495,7 @@ void AddMetricTags(ref TagList tags, string? requestModelId, ChatResponse? respo tags.Add(OpenTelemetryConsts.GenAI.Request.Model, requestModelId); } - tags.Add(OpenTelemetryConsts.GenAI.SystemName, _system); + tags.Add(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); if (_serverAddress is string endpointAddress) { @@ -451,155 +510,84 @@ void AddMetricTags(ref TagList tags, string? requestModelId, ChatResponse? respo } } - private void LogChatMessages(IEnumerable messages) + private void AddInputMessagesTags(IEnumerable messages, ChatOptions? options, Activity? activity) { - if (!_logger.IsEnabled(EventLogLevel)) + if (EnableSensitiveData && activity is { IsAllDataRequested: true }) { - return; - } - - foreach (ChatMessage message in messages) - { - if (message.Role == ChatRole.Assistant) - { - Log(new(1, OpenTelemetryConsts.GenAI.Assistant.Message), - JsonSerializer.Serialize(CreateAssistantEvent(message.Contents), OtelContext.Default.AssistantEvent)); - } - else if (message.Role == ChatRole.Tool) - { - foreach (FunctionResultContent frc in message.Contents.OfType()) - { - Log(new(1, OpenTelemetryConsts.GenAI.Tool.Message), - JsonSerializer.Serialize(new() - { - Id = frc.CallId, - Content = EnableSensitiveData && frc.Result is object result ? - JsonSerializer.SerializeToNode(result, _jsonSerializerOptions.GetTypeInfo(result.GetType())) : - null, - }, OtelContext.Default.ToolEvent)); - } - } - else + if (!string.IsNullOrWhiteSpace(options?.Instructions)) { - Log(new(1, message.Role == ChatRole.System ? OpenTelemetryConsts.GenAI.System.Message : OpenTelemetryConsts.GenAI.User.Message), - JsonSerializer.Serialize(new() - { - Role = message.Role != ChatRole.System && message.Role != ChatRole.User && !string.IsNullOrWhiteSpace(message.Role.Value) ? message.Role.Value : null, - Content = GetMessageContent(message.Contents), - }, OtelContext.Default.SystemOrUserEvent)); + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.SystemInstructions, + JsonSerializer.Serialize(new object[1] { new OtelGenericPart { Content = options!.Instructions } }, _defaultOptions.GetTypeInfo(typeof(IList)))); } - } - } - private void LogChatResponse(ChatResponse response) - { - if (!_logger.IsEnabled(EventLogLevel)) - { - return; + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Input.Messages, + SerializeChatMessages(messages)); } - - EventId id = new(1, OpenTelemetryConsts.GenAI.Choice); - Log(id, JsonSerializer.Serialize(new() - { - FinishReason = response.FinishReason?.Value ?? "error", - Index = 0, - Message = CreateAssistantEvent(response.Messages is { Count: 1 } ? response.Messages[0].Contents : response.Messages.SelectMany(m => m.Contents)), - }, OtelContext.Default.ChoiceEvent)); } - private void Log(EventId id, [StringSyntax(StringSyntaxAttribute.Json)] string eventBodyJson) + private void AddOutputMessagesTags(ChatResponse response, Activity? activity) { - // This is not the idiomatic way to log, but it's necessary for now in order to structure - // the data in a way that the OpenTelemetry collector can work with it. The event body - // can be very large and should not be logged as an attribute. - - KeyValuePair[] tags = - [ - new(OpenTelemetryConsts.Event.Name, id.Name), - new(OpenTelemetryConsts.GenAI.SystemName, _system), - ]; - - _logger.Log(EventLogLevel, id, tags, null, (_, __) => eventBodyJson); - } - - private AssistantEvent CreateAssistantEvent(IEnumerable contents) - { - var toolCalls = contents.OfType().Select(fc => new ToolCall + if (EnableSensitiveData && activity is { IsAllDataRequested: true }) { - Id = fc.CallId, - Function = new() - { - Name = fc.Name, - Arguments = EnableSensitiveData ? - JsonSerializer.SerializeToNode(fc.Arguments, _jsonSerializerOptions.GetTypeInfo(typeof(IDictionary))) : - null, - }, - }).ToArray(); - - return new() - { - Content = GetMessageContent(contents), - ToolCalls = toolCalls.Length > 0 ? toolCalls : null, - }; - } - - private string? GetMessageContent(IEnumerable contents) - { - if (EnableSensitiveData) - { - string content = string.Concat(contents.OfType()); - if (content.Length > 0) - { - return content; - } + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Output.Messages, + SerializeChatMessages(response.Messages, response.FinishReason)); } - - return null; } - private sealed class SystemOrUserEvent + private sealed class OtelMessage { public string? Role { get; set; } - public string? Content { get; set; } + public List Parts { get; set; } = []; + public string? FinishReason { get; set; } } - private sealed class AssistantEvent + private sealed class OtelGenericPart { - public string? Content { get; set; } - public ToolCall[]? ToolCalls { get; set; } + public string Type { get; set; } = "text"; + public object? Content { get; set; } // should be a string when Type == "text" } - private sealed class ToolEvent + private sealed class OtelToolCallRequestPart { + public string Type { get; set; } = "tool_call"; public string? Id { get; set; } - public JsonNode? Content { get; set; } - } - - private sealed class ChoiceEvent - { - public string? FinishReason { get; set; } - public int Index { get; set; } - public AssistantEvent? Message { get; set; } + public string? Name { get; set; } + public IDictionary? Arguments { get; set; } } - private sealed class ToolCall + private sealed class OtelToolCallResponsePart { + public string Type { get; set; } = "tool_call_response"; public string? Id { get; set; } - public string? Type { get; set; } = "function"; - public ToolCallFunction? Function { get; set; } + public object? Response { get; set; } } - private sealed class ToolCallFunction + private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions(); + + private static JsonSerializerOptions CreateDefaultOptions() { - public string? Name { get; set; } - public JsonNode? Arguments { get; set; } + JsonSerializerOptions options = new(OtelContext.Default.Options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.MakeReadOnly(); + + return options; } - [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] - [JsonSerializable(typeof(SystemOrUserEvent))] - [JsonSerializable(typeof(AssistantEvent))] - [JsonSerializable(typeof(ToolEvent))] - [JsonSerializable(typeof(ChoiceEvent))] - [JsonSerializable(typeof(object))] + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(IList))] + [JsonSerializable(typeof(OtelMessage))] + [JsonSerializable(typeof(OtelGenericPart))] + [JsonSerializable(typeof(OtelToolCallRequestPart))] + [JsonSerializable(typeof(OtelToolCallResponsePart))] private sealed partial class OtelContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 280e2a2ead3..6db30409b26 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.36, defined at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.37, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// /// The type of input used to produce embeddings. @@ -33,10 +33,9 @@ public sealed class OpenTelemetryEmbeddingGenerator : Delega private readonly Histogram _tokenUsageHistogram; private readonly Histogram _operationDurationHistogram; - private readonly string? _system; + private readonly string? _providerName; private readonly string? _defaultModelId; private readonly int? _defaultModelDimensions; - private readonly string? _modelProvider; private readonly string? _endpointAddress; private readonly int _endpointPort; @@ -44,7 +43,7 @@ public sealed class OpenTelemetryEmbeddingGenerator : Delega /// Initializes a new instance of the class. /// /// The underlying , which is the next stage of the pipeline. - /// The to use for emitting events. + /// The to use for emitting any logging data from the generator. /// An optional source name that will be used on the telemetry data. #pragma warning disable IDE0060 // Remove unused parameter; it exists for future use and consistency with OpenTelemetryChatClient public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator innerGenerator, ILogger? logger = null, string? sourceName = null) @@ -55,10 +54,9 @@ public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator i if (innerGenerator!.GetService() is EmbeddingGeneratorMetadata metadata) { - _system = metadata.ProviderName; _defaultModelId = metadata.DefaultModelId; _defaultModelDimensions = metadata.DefaultModelDimensions; - _modelProvider = metadata.ProviderName; + _providerName = metadata.ProviderName; _endpointAddress = metadata.ProviderUri?.Host; _endpointPort = metadata.ProviderUri?.Port ?? 0; } @@ -160,7 +158,7 @@ protected override void Dispose(bool disposing) [ new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings), new(OpenTelemetryConsts.GenAI.Request.Model, modelId), - new(OpenTelemetryConsts.GenAI.SystemName, _modelProvider), + new(OpenTelemetryConsts.GenAI.Provider.Name, _providerName), ]); if (activity is not null) @@ -182,13 +180,13 @@ protected override void Dispose(bool disposing) // and more generally cases where there's additional useful information to be logged. // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data. if (EnableSensitiveData && - _system is not null && + _providerName is not null && options?.AdditionalProperties is { } props) { foreach (KeyValuePair prop in props) { _ = activity.AddTag( - OpenTelemetryConsts.GenAI.Request.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + OpenTelemetryConsts.GenAI.Request.PerProvider(_providerName, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), prop.Value); } } @@ -232,7 +230,7 @@ private void TraceResponse( if (_tokenUsageHistogram.Enabled && inputTokens.HasValue) { TagList tags = default; - tags.Add(OpenTelemetryConsts.GenAI.Token.Type, "input"); + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); AddMetricTags(ref tags, requestModelId, responseModelId); _tokenUsageHistogram.Record(inputTokens.Value); @@ -261,13 +259,13 @@ private void TraceResponse( // there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.response.system_fingerprint), // and more generally cases where there's additional useful information to be logged. if (EnableSensitiveData && - _system is not null && + _providerName is not null && embeddings?.AdditionalProperties is { } props) { foreach (KeyValuePair prop in props) { _ = activity.AddTag( - OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + OpenTelemetryConsts.GenAI.Response.PerProvider(_providerName, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), prop.Value); } } @@ -283,7 +281,7 @@ private void AddMetricTags(ref TagList tags, string? requestModelId, string? res tags.Add(OpenTelemetryConsts.GenAI.Request.Model, requestModelId); } - tags.Add(OpenTelemetryConsts.GenAI.SystemName, _modelProvider); + tags.Add(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); if (_endpointAddress is string endpointAddress) { diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index e656a473ba2..a1934faa526 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -14,10 +14,13 @@ internal static class OpenTelemetryConsts public const string SecondsUnit = "s"; public const string TokensUnit = "token"; - public static class Event - { - public const string Name = "event.name"; - } + public const string ToolTypeFunction = "function"; + + public const string TypeText = "text"; + public const string TypeJson = "json"; + + public const string TokenTypeInput = "input"; + public const string TokenTypeOutput = "output"; public static class Error { @@ -26,17 +29,11 @@ public static class Error public static class GenAI { - public const string Choice = "gen_ai.choice"; - public const string SystemName = "gen_ai.system"; - public const string Chat = "chat"; public const string Embeddings = "embeddings"; public const string ExecuteTool = "execute_tool"; - public static class Assistant - { - public const string Message = "gen_ai.assistant.message"; - } + public const string SystemInstructions = "gen_ai.system_instructions"; public static class Client { @@ -60,6 +57,11 @@ public static class Conversation public const string Id = "gen_ai.conversation.id"; } + public static class Input + { + public const string Messages = "gen_ai.input.messages"; + } + public static class Operation { public const string Name = "gen_ai.operation.name"; @@ -67,9 +69,15 @@ public static class Operation public static class Output { + public const string Messages = "gen_ai.output.messages"; public const string Type = "gen_ai.output.type"; } + public static class Provider + { + public const string Name = "gen_ai.provider.name"; + } + public static class Request { public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions"; @@ -83,7 +91,7 @@ public static class Request public const string TopK = "gen_ai.request.top_k"; public const string TopP = "gen_ai.request.top_p"; - public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.request.{parameterName}"; + public static string PerProvider(string providerName, string parameterName) => $"{providerName}.request.{parameterName}"; } public static class Response @@ -92,12 +100,7 @@ public static class Response public const string Id = "gen_ai.response.id"; public const string Model = "gen_ai.response.model"; - public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.response.{parameterName}"; - } - - public static class System - { - public const string Message = "gen_ai.system.message"; + public static string PerProvider(string providerName, string parameterName) => $"{providerName}.response.{parameterName}"; } public static class Token @@ -110,6 +113,7 @@ public static class Tool public const string Name = "gen_ai.tool.name"; public const string Description = "gen_ai.tool.description"; public const string Message = "gen_ai.tool.message"; + public const string Type = "gen_ai.tool.type"; public static class Call { @@ -122,11 +126,6 @@ public static class Usage public const string InputTokens = "gen_ai.usage.input_tokens"; public const string OutputTokens = "gen_ai.usage.output_tokens"; } - - public static class User - { - public const string Message = "gen_ai.user.message"; - } } public static class Server diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index 1cd849e3c14..fcee53a68ee 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using OpenTelemetry.Trace; using Xunit; @@ -30,9 +30,6 @@ public async Task ExpectedInformationLogged_Async(bool enableSensitiveData, bool .AddInMemoryExporter(activities) .Build(); - var collector = new FakeLogCollector(); - using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddProvider(new FakeLoggerProvider(collector))); - using var innerClient = new TestChatClient { GetResponseAsyncCallback = async (messages, options, cancellationToken) => @@ -98,7 +95,7 @@ async static IAsyncEnumerable CallbackAsync( using var chatClient = innerClient .AsBuilder() - .UseOpenTelemetry(loggerFactory, sourceName, configure: instance => + .UseOpenTelemetry(null, sourceName, configure: instance => { instance.EnableSensitiveData = enableSensitiveData; instance.JsonSerializerOptions = TestJsonSerializerContext.Default.Options; @@ -132,6 +129,7 @@ async static IAsyncEnumerable CallbackAsync( ["service_tier"] = "value1", ["SomethingElse"] = "value2", }, + Instructions = "You are helpful.", }; if (streaming) @@ -155,7 +153,7 @@ async static IAsyncEnumerable CallbackAsync( Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); Assert.Equal("chat replacementmodel", activity.DisplayName); - Assert.Equal("testservice", activity.GetTagItem("gen_ai.system")); + Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name")); Assert.Equal("replacementmodel", activity.GetTagItem("gen_ai.request.model")); Assert.Equal(3.0f, activity.GetTagItem("gen_ai.request.frequency_penalty")); @@ -165,55 +163,114 @@ async static IAsyncEnumerable CallbackAsync( Assert.Equal(7, activity.GetTagItem("gen_ai.request.top_k")); Assert.Equal(123, activity.GetTagItem("gen_ai.request.max_tokens")); Assert.Equal("""["hello", "world"]""", activity.GetTagItem("gen_ai.request.stop_sequences")); - Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("gen_ai.testservice.request.service_tier")); - Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("gen_ai.testservice.request.something_else")); + Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("testservice.request.service_tier")); + Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("testservice.request.something_else")); Assert.Equal(42L, activity.GetTagItem("gen_ai.request.seed")); Assert.Equal("id123", activity.GetTagItem("gen_ai.response.id")); Assert.Equal("""["stop"]""", activity.GetTagItem("gen_ai.response.finish_reasons")); Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens")); Assert.Equal(20, activity.GetTagItem("gen_ai.usage.output_tokens")); - Assert.Equal(enableSensitiveData ? "abcdefgh" : null, activity.GetTagItem("gen_ai.testservice.response.system_fingerprint")); - Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("gen_ai.testservice.response.and_something_else")); + Assert.Equal(enableSensitiveData ? "abcdefgh" : null, activity.GetTagItem("testservice.response.system_fingerprint")); + Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("testservice.response.and_something_else")); Assert.True(activity.Duration.TotalMilliseconds > 0); - var logs = collector.GetSnapshot(); + var tags = activity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (enableSensitiveData) { - Assert.Collection(logs, - log => Assert.Equal("""{"content":"You are a close friend."}""", log.Message), - log => Assert.Equal("""{"content":"Hey!"}""", log.Message), - log => Assert.Equal("""{"tool_calls":[{"id":"12345","type":"function","function":{"name":"GetPersonName"}}]}""", log.Message), - log => Assert.Equal("""{"id":"12345","content":"John"}""", log.Message), - log => Assert.Equal("""{"content":"Hey John, what\u0027s up?"}""", log.Message), - log => Assert.Equal("""{"content":"What\u0027s the biggest animal?"}""", log.Message), - log => Assert.Equal("""{"finish_reason":"stop","index":0,"message":{"content":"The blue whale, I think."}}""", log.Message)); + Assert.Equal(ReplaceWhitespace(""" + [ + { + "role": "system", + "parts": [ + { + "type": "text", + "content": "You are a close friend." + } + ] + }, + { + "role": "user", + "parts": [ + { + "type": "text", + "content": "Hey!" + } + ] + }, + { + "role": "assistant", + "parts": [ + { + "type": "tool_call", + "id": "12345", + "name": "GetPersonName" + } + ] + }, + { + "role": "tool", + "parts": [ + { + "type": "tool_call_response", + "id": "12345", + "response": "John" + } + ] + }, + { + "role": "assistant", + "parts": [ + { + "type": "text", + "content": "Hey John, what's up?" + } + ] + }, + { + "role": "user", + "parts": [ + { + "type": "text", + "content": "What's the biggest animal?" + } + ] + } + ] + """), ReplaceWhitespace(tags["gen_ai.input.messages"])); + + Assert.Equal(ReplaceWhitespace(""" + [ + { + "role": "assistant", + "parts": [ + { + "type": "text", + "content": "The blue whale, I think." + } + ], + "finish_reason": "stop" + } + ] + """), ReplaceWhitespace(tags["gen_ai.output.messages"])); + + Assert.Equal(ReplaceWhitespace(""" + [ + { + "type": "text", + "content": "You are helpful." + } + ] + """), ReplaceWhitespace(tags["gen_ai.system_instructions"])); } else { - Assert.Collection(logs, - log => Assert.Equal("""{}""", log.Message), - log => Assert.Equal("""{}""", log.Message), - log => Assert.Equal("""{"tool_calls":[{"id":"12345","type":"function","function":{"name":"GetPersonName"}}]}""", log.Message), - log => Assert.Equal("""{"id":"12345"}""", log.Message), - log => Assert.Equal("""{}""", log.Message), - log => Assert.Equal("""{}""", log.Message), - log => Assert.Equal("""{"finish_reason":"stop","index":0,"message":{}}""", log.Message)); + Assert.False(tags.ContainsKey("gen_ai.input.messages")); + Assert.False(tags.ContainsKey("gen_ai.output.messages")); + Assert.False(tags.ContainsKey("gen_ai.system_instructions")); } - Assert.Collection(logs, - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.system.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.user.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.assistant.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.tool.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.assistant.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.user.message"), ((IList>)log.State!)[0]), - log => Assert.Equal(new KeyValuePair("event.name", "gen_ai.choice"), ((IList>)log.State!)[0])); - - Assert.All(logs, log => - { - Assert.Equal(new KeyValuePair("gen_ai.system", "testservice"), ((IList>)log.State!)[1]); - }); + static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs index 053f52a9d57..c3661f2c62b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs @@ -82,16 +82,16 @@ public async Task ExpectedInformationLogged_Async(string? perRequestModelId, boo Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); Assert.Equal($"embeddings {expectedModelName}", activity.DisplayName); - Assert.Equal("testservice", activity.GetTagItem("gen_ai.system")); + Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name")); Assert.Equal(expectedModelName, activity.GetTagItem("gen_ai.request.model")); Assert.Equal(1234, activity.GetTagItem("gen_ai.request.embedding.dimensions")); - Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("gen_ai.testservice.request.service_tier")); - Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("gen_ai.testservice.request.something_else")); + Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("testservice.request.service_tier")); + Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("testservice.request.something_else")); Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens")); - Assert.Equal(enableSensitiveData ? "abcdefgh" : null, activity.GetTagItem("gen_ai.testservice.response.system_fingerprint")); - Assert.Equal(enableSensitiveData ? "value3" : null, activity.GetTagItem("gen_ai.testservice.response.and_something_else")); + Assert.Equal(enableSensitiveData ? "abcdefgh" : null, activity.GetTagItem("testservice.response.system_fingerprint")); + Assert.Equal(enableSensitiveData ? "value3" : null, activity.GetTagItem("testservice.response.and_something_else")); Assert.True(activity.Duration.TotalMilliseconds > 0); } From d6718f6aef0290d137c959a4742915406aacfce0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:13:51 -0700 Subject: [PATCH 278/472] Branding updates for 9.10 (#6769) * Initial plan * Branding updates for 9.10 - Update versions from 9.9 to 9.10 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- eng/Versions.props | 2 +- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 2 +- .../aichatweb/aichatweb.csproj | 4 ++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index f79956ce23f..b33dac06757 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ 9 - 9 + 10 0 preview 1 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 323170224ee..fb2e9b9a23a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index af528b3f97d..a4c6ec68b94 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index ec0a5016de7..3f313c097c6 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 323170224ee..fb2e9b9a23a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 789ef7f95f7..7fe3b1022ee 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index 323170224ee..fb2e9b9a23a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 1fcb4440008..85dc735b4c4 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 6aaa0a005b3..ceb6b34e3bf 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,9 +8,9 @@ - + - + From 48be40c018f55cc197598bb008bce99a969b31e8 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 3 Sep 2025 23:43:33 +0000 Subject: [PATCH 279/472] Merged PR 52950: Getting ready for 9.9 release Getting ready for 9.9 release ---- #### AI description (iteration 1) #### PR Classification This pull request updates dependency versions and build configurations in preparation for the 9.9 release. #### PR Summary The changes bump most dependency and LTS versions from 9.0.8 to 9.0.9 (and 8.0.19 to 8.0.20), and adjust build settings for a stable release. Key changes include: - Updates in `eng/Version.Details.xml` and `eng/Versions.props` for dependency version bumps and setting `` to true and `` to release. - Removal of the code coverage stage in `azure-pipelines.yml` and corresponding dependency in the pipeline orchestration. - Modifications in `eng/pipelines/templates/BuildAndTest.yml` to add private feed credentials setup and comment out integration tests. - Adjustments in `NuGet.config` to update package source configurations. - Suppression of NU1507 warnings in `Directory.Build.props` due to internal feed usage. --- Directory.Build.props | 5 + NuGet.config | 82 +++++++--- azure-pipelines.yml | 46 ------ eng/Version.Details.xml | 188 +++++++++++------------ eng/Versions.props | 122 +++++++-------- eng/pipelines/templates/BuildAndTest.yml | 32 +++- 6 files changed, 245 insertions(+), 230 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0af806af628..0c0fcf22bfd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,11 @@ $(NetCoreTargetFrameworks) + + + $(NoWarn);NU1507 + + false latest diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..a93d883fdb6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -18,35 +48,43 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ec5e3d1cdb..0052dc9f706 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -239,51 +239,6 @@ extends: isWindows: false warnAsError: 0 - # ---------------------------------------------------------------- - # This stage performs quality gates enforcements - # ---------------------------------------------------------------- - - stage: codecoverage - displayName: CodeCoverage - dependsOn: - - build - condition: and(succeeded('build'), ne(variables['SkipQualityGates'], 'true')) - variables: - - template: /eng/common/templates-official/variables/pool-providers.yml@self - jobs: - - template: /eng/common/templates-official/jobs/jobs.yml@self - parameters: - enableMicrobuild: true - enableTelemetry: true - runAsPublic: ${{ variables['runAsPublic'] }} - workspace: - clean: all - - # ---------------------------------------------------------------- - # This stage downloads the code coverage reports from the build jobs, - # merges those and validates the combined test coverage. - # ---------------------------------------------------------------- - jobs: - - job: CodeCoverageReport - timeoutInMinutes: 180 - - pool: - name: NetCore1ESPool-Internal - image: 1es-mariner-2 - os: linux - - preSteps: - - checkout: self - clean: true - persistCredentials: true - fetchDepth: 1 - - steps: - - script: $(Build.SourcesDirectory)/build.sh --ci --restore - displayName: Init toolset - - - template: /eng/pipelines/templates/VerifyCoverageReport.yml - - # ---------------------------------------------------------------- # This stage only performs a build treating warnings as errors # to detect any kind of code style violations @@ -339,7 +294,6 @@ extends: parameters: validateDependsOn: - build - - codecoverage - correctness publishingInfraVersion: 3 enableSymbolValidation: false diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3070e2658d3..b75b66eaea6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,196 +1,196 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - aae90fa09086a9be09dac83fa66542232c7269d8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 215a587e52efa710de84138b0a3374b860b924d8 + ff66c263be7ed395794bdaf616322977b8ec897c - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 3f7d40ec7be104358780955b3f0fea62495264dc + 78871c83aac6c38eb5476c2f34aae98ef65314f5 diff --git a/eng/Versions.props b/eng/Versions.props index f79956ce23f..976ec2693dc 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -10,14 +10,14 @@ - false + true - + release true @@ -33,55 +33,55 @@ --> - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 - 9.0.8 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 - 9.0.8 + 9.0.9 9.0.0-beta.25428.3 @@ -107,8 +107,8 @@ 8.0.1 8.0.0 8.0.2 - 8.0.19 - 8.0.19 + 8.0.20 + 8.0.20 8.0.0 8.0.1 8.0.1 @@ -125,17 +125,17 @@ 8.0.6 8.0.0 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 - 8.0.19 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 - 8.0.19 + 8.0.20 + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 + Major + Exe enable enable @@ -10,6 +15,21 @@ true McpServer + + + + true + true + + + true + + + + + true + true + README.md diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md index cb11ac30eb5..95612c5e35e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -1,6 +1,26 @@ # MCP Server -This README was created using the C# MCP server project template. It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +#### ---#if (SelfContained) +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If your users require more platforms to be supported, update the list of runtime identifiers in the project's `` element. +#### ---#else +The MCP server is built as a framework-dependent application and requires the .NET runtime to be installed on the target machine. +The application is configured to roll-forward to the next highest major version of the runtime if one is available on the target machine. +If an applicable .NET runtime is not available, the MCP server will not start. +Consider building the MCP server as a self-contained application if you want to avoid this dependency. +#### ---#endif See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs index a3f3dedd1b5..0f75e2f6138 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs @@ -41,6 +41,24 @@ public async Task BasicTest() await TestTemplateCoreAsync(scenarioName: "Basic"); } + [Fact] + public async Task SelfContainedFalse() + { + await TestTemplateCoreAsync(scenarioName: "SelfContainedFalse", templateArgs: ["--self-contained", bool.FalseString]); + } + + [Fact] + public async Task AotTrue() + { + await TestTemplateCoreAsync(scenarioName: "AotTrue", templateArgs: ["--aot", bool.TrueString]); + } + + [Fact] + public async Task Net10() + { + await TestTemplateCoreAsync(scenarioName: "net10", templateArgs: ["--framework", "net10.0"]); + } + private async Task TestTemplateCoreAsync(string scenarioName, IEnumerable? templateArgs = null) { string workingDir = TestUtils.CreateTemporaryFolder(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..02908c09afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/README.md new file mode 100644 index 00000000000..31035a6370e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/README.md @@ -0,0 +1,98 @@ +# MCP Server + +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If your users require more platforms to be supported, update the list of runtime identifiers in the project's `` element. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey). + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Developing locally + +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +## Testing the MCP Server + +Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org + +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. + +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dnx", + "args": [ + "", + "--version", + "", + "--yes" + ] + } + } +} +``` + +## More information + +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..611745f4129 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..27a6ad45810 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,44 @@ + + + + net9.0 + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 + Exe + enable + enable + + + true + McpServer + + + true + true + + + true + + + true + true + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md index a0bf0fc082d..31035a6370e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -1,6 +1,19 @@ # MCP Server -This README was created using the C# MCP server project template. It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If your users require more platforms to be supported, update the list of runtime identifiers in the project's `` element. See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index 468230d16e4..a3199648740 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -1,8 +1,8 @@  - net8.0 - Major + net9.0 + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 Exe enable enable @@ -11,6 +11,13 @@ true McpServer + + true + true + + + true + README.md SampleMcpServer diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..02908c09afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/README.md new file mode 100644 index 00000000000..1702211733a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/README.md @@ -0,0 +1,91 @@ +# MCP Server + +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +The MCP server is built as a framework-dependent application and requires the .NET runtime to be installed on the target machine. +The application is configured to roll-forward to the next highest major version of the runtime if one is available on the target machine. +If an applicable .NET runtime is not available, the MCP server will not start. +Consider building the MCP server as a self-contained application if you want to avoid this dependency. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey). + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Developing locally + +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +## Testing the MCP Server + +Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org + +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. + +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dnx", + "args": [ + "", + "--version", + "", + "--yes" + ] + } + } +} +``` + +## More information + +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..611745f4129 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..cb3812885c6 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,33 @@ + + + + net9.0 + Major + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..02908c09afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/README.md new file mode 100644 index 00000000000..31035a6370e --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/README.md @@ -0,0 +1,98 @@ +# MCP Server + +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If your users require more platforms to be supported, update the list of runtime identifiers in the project's `` element. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey). + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Developing locally + +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +## Testing the MCP Server + +Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org + +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. + +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dnx", + "args": [ + "", + "--version", + "", + "--yes" + ] + } + } +} +``` + +## More information + +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..611745f4129 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..27c45f47efb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,40 @@ + + + + net10.0 + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 + Exe + enable + enable + + + true + McpServer + + + true + true + + + true + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + From 7f5c3c259e33cd84e8f3b3c70f1f9372f74fba50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as?= <50237907+ViveliDuCh@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:19:02 -0700 Subject: [PATCH 299/472] Add support for using ConversationID for AzureOpenAI and OpenAI (#6770) * Add support for using ConversationID for AzureOpenAI and OpenAI * After PR Feedback: Dynamically detect stateful or stateless chat client at runtime, remove Aspire-specific workarounds, simplify template logic, and default to stateful API where supported. --- .../Components/Pages/Chat/Chat.razor | 8 +++++++- .../ChatWithCustomData-CSharp.Web/Program.cs | 8 ++++++-- .../aichatweb.Web/Components/Pages/Chat/Chat.razor | 8 +++++++- .../aichatweb/Components/Pages/Chat/Chat.razor | 8 +++++++- .../aichatweb.Web/Components/Pages/Chat/Chat.razor | 8 +++++++- .../aichatweb.Web/Components/Pages/Chat/Chat.razor | 8 +++++++- .../aichatweb/Components/Pages/Chat/Chat.razor | 8 +++++++- .../aichatweb/Program.cs | 4 +++- 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index f3f5740066f..f8d2826ab9a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -47,7 +47,9 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -var chatClient = openAIClient.GetChatClient("gpt-4o-mini").AsIChatClient(); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #elif (IsAzureAiFoundry) @@ -66,7 +68,9 @@ #else new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details."))); #endif -var chatClient = azureOpenAi.GetChatClient("gpt-4o-mini").AsIChatClient(); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +var chatClient = azureOpenAi.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #endif diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor index a7b1502d894..8aa0ec9fd28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -40,6 +40,7 @@ Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. "; + private int statefulMessageCount; private readonly ChatOptions chatOptions = new(); private readonly List messages = new(); private CancellationTokenSource? currentResponseCancellation; @@ -49,6 +50,7 @@ protected override void OnInitialized() { + statefulMessageCount = 0; messages.Add(new(ChatRole.System, SystemPrompt)); chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)]; } @@ -66,15 +68,17 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) { messages.AddMessages(update, filter: c => c is not TextContent); responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; ChatMessageItem.NotifyChanged(currentResponseMessage); } // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; currentResponseMessage = null; chatSuggestions?.Update(messages); } @@ -96,6 +100,8 @@ CancelAnyCurrentResponse(); messages.Clear(); messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; chatSuggestions?.Clear(); await chatInput!.FocusAsync(); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index 2b9f0790817..d469d9c43db 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -16,7 +16,9 @@ // dotnet user-secrets set OpenAI:Key YOUR-API-KEY var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -var chatClient = openAIClient.GetChatClient("gpt-4o-mini").AsIChatClient(); +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); +#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); // You will need to set the endpoint and key to your own values From d299e16f15234f9808b18fef50bf7770113fb4b2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 12 Sep 2025 12:55:54 -0400 Subject: [PATCH 300/472] Add ChatResponseFormat.ForJsonSchema (#6786) * Add ChatResponseFormat.ForJsonSchema Moves the implementation used by `GetResponseAsync` to be exposed as its own helper that can be used directly, for cases where folks want the formatting but don't want to use the generic `GetResponseAsync`. * Address PR feedback --- .../CHANGELOG.md | 2 + .../ChatCompletion/ChatResponseFormat.cs | 77 +++++++++++-- .../ChatCompletion/DelegatingChatClient.cs | 1 + .../Microsoft.Extensions.AI.Abstractions.json | 8 ++ .../ChatClientStructuredOutputExtensions.cs | 101 +++++------------- .../ChatCompletion/ChatResponseFormatTests.cs | 97 +++++++++++++++++ .../TestJsonSerializerContext.cs | 1 + ...atClientStructuredOutputExtensionsTests.cs | 86 +++++++-------- 8 files changed, 245 insertions(+), 128 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index a5cf85aba1c..4bb67980439 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -2,6 +2,8 @@ ## NOT YET RELEASED +- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. + ## 9.9.0 - Added non-invocable `AIFunctionDeclaration` (base class for `AIFunction`), `AIFunctionFactory.CreateDeclaration`, and `AIFunction.AsDeclarationOnly`. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs index ac59cfc263e..088fc533d05 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs @@ -1,19 +1,30 @@ // 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 + /// Represents the response format that is desired by the caller. [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(ChatResponseFormatText), typeDiscriminator: "text")] [JsonDerivedType(typeof(ChatResponseFormatJson), typeDiscriminator: "json")] -#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable -public class ChatResponseFormat -#pragma warning restore CA1052 +public partial class ChatResponseFormat { + private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new() + { + IncludeSchemaKeyword = true, + }; + /// Initializes a new instance of the class. /// Prevents external instantiation. Close the inheritance hierarchy for now until we have good reason to open it. private protected ChatResponseFormat() @@ -33,7 +44,61 @@ private protected ChatResponseFormat() /// The instance. public static ChatResponseFormatJson ForJsonSchema( JsonElement schema, string? schemaName = null, string? schemaDescription = null) => - new(schema, - schemaName, - schemaDescription); + new(schema, schemaName, schemaDescription); + + /// Creates a representing structured JSON data with a schema based on . + /// The type for which a schema should be exported and used as the response schema. + /// The JSON serialization options to use. + /// An optional name of the schema. By default, this will be inferred from . + /// An optional description of the schema. By default, this will be inferred from . + /// The instance. + /// + /// Many AI services that support structured output require that the JSON schema have a top-level 'type=object'. + /// If is a primitive type like , , or , + /// 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 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. + /// + public static ChatResponseFormatJson ForJsonSchema( + JsonSerializerOptions? serializerOptions = null, string? schemaName = null, string? schemaDescription = null) => + ForJsonSchema(typeof(T), serializerOptions, schemaName, schemaDescription); + + /// Creates a representing structured JSON data with a schema based on . + /// The for which a schema should be exported and used as the response schema. + /// The JSON serialization options to use. + /// An optional name of the schema. By default, this will be inferred from . + /// An optional description of the schema. By default, this will be inferred from . + /// The instance. + /// + /// Many AI services that support structured output require that the JSON schema have a top-level 'type=object'. + /// If is a primitive type like , , or , + /// 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 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. + /// + /// is . + 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()?.Description); + } + + /// Regex that flags any character other than ASCII digits, ASCII letters, or underscore. +#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 } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/DelegatingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/DelegatingChatClient.cs index 112e846d41f..34aa665450b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/DelegatingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/DelegatingChatClient.cs @@ -23,6 +23,7 @@ public class DelegatingChatClient : IChatClient /// Initializes a new instance of the class. /// /// The wrapped client instance. + /// is . protected DelegatingChatClient(IChatClient innerClient) { InnerClient = Throw.IfNull(innerClient); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index ca42fdacd20..034b5787eab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1156,6 +1156,14 @@ { "Member": "static Microsoft.Extensions.AI.ChatResponseFormatJson Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema(System.Text.Json.JsonElement schema, string? schemaName = null, string? schemaDescription = null);", "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.ChatResponseFormatJson Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema(System.Text.Json.JsonSerializerOptions? serializerOptions = null, string? schemaName = null, string? schemaDescription = null);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.ChatResponseFormatJson Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema(System.Type schemaType, System.Text.Json.JsonSerializerOptions? serializerOptions = null, string? schemaName = null, string? schemaDescription = null);", + "Stage": "Stable" } ], "Properties": [ diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs index 69c4cc7ee89..09ec568d749 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Reflection; +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -23,17 +21,6 @@ namespace Microsoft.Extensions.AI; /// Request a response with structured output. public static partial class ChatClientStructuredOutputExtensions { - private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new() - { - IncludeSchemaKeyword = true, - TransformOptions = new AIJsonSchemaTransformOptions - { - DisallowAdditionalProperties = true, - RequireAllProperties = true, - MoveDefaultKeywordToDescription = true, - }, - }; - /// Sends chat messages, requesting a response matching the type . /// The . /// The chat content to send. @@ -161,20 +148,12 @@ public static async Task> GetResponseAsync( serializerOptions.MakeReadOnly(); - var schemaElement = AIJsonUtilities.CreateJsonSchema( - type: typeof(T), - serializerOptions: serializerOptions, - inferenceOptions: _inferenceOptions); + var responseFormat = ChatResponseFormat.ForJsonSchema(serializerOptions); - bool isWrappedInObject; - JsonElement schema; - if (SchemaRepresentsObject(schemaElement)) - { - // For object-representing schemas, we can use them as-is - isWrappedInObject = false; - schema = schemaElement; - } - else + Debug.Assert(responseFormat.Schema is not null, "ForJsonSchema should always populate Schema"); + var schema = responseFormat.Schema!.Value; + bool isWrappedInObject = false; + if (!SchemaRepresentsObject(schema)) { // For non-object-representing schemas, we wrap them in an object schema, because all // the real LLM providers today require an object schema as the root. This is currently @@ -184,10 +163,11 @@ public static async Task> GetResponseAsync( { { "$schema", "https://json-schema.org/draft/2020-12/schema" }, { "type", "object" }, - { "properties", new JsonObject { { "data", JsonElementToJsonNode(schemaElement) } } }, + { "properties", new JsonObject { { "data", JsonElementToJsonNode(schema) } } }, { "additionalProperties", false }, { "required", new JsonArray("data") }, }, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonObject))); + responseFormat = ChatResponseFormat.ForJsonSchema(schema, responseFormat.SchemaName, responseFormat.SchemaDescription); } ChatMessage? promptAugmentation = null; @@ -200,10 +180,7 @@ public static async Task> GetResponseAsync( { // When using native structured output, we don't add any additional prompt, because // the LLM backend is meant to do whatever's needed to explain the schema to the LLM. - options.ResponseFormat = ChatResponseFormat.ForJsonSchema( - schema, - schemaName: SanitizeMemberName(typeof(T).Name), - schemaDescription: typeof(T).GetCustomAttribute()?.Description); + options.ResponseFormat = responseFormat; } else { @@ -213,7 +190,7 @@ public static async Task> GetResponseAsync( promptAugmentation = new ChatMessage(ChatRole.User, $$""" Respond with a JSON value conforming to the following schema: ``` - {{schema}} + {{responseFormat.Schema}} ``` """); @@ -222,53 +199,31 @@ public static async Task> GetResponseAsync( var result = await chatClient.GetResponseAsync(messages, options, cancellationToken); return new ChatResponse(result, serializerOptions) { IsWrappedInObject = isWrappedInObject }; - } - private static bool SchemaRepresentsObject(JsonElement schemaElement) - { - if (schemaElement.ValueKind is JsonValueKind.Object) + static bool SchemaRepresentsObject(JsonElement schemaElement) { - foreach (var property in schemaElement.EnumerateObject()) + if (schemaElement.ValueKind is JsonValueKind.Object) { - if (property.NameEquals("type"u8)) + foreach (var property in schemaElement.EnumerateObject()) { - return property.Value.ValueKind == JsonValueKind.String - && property.Value.ValueEquals("object"u8); + if (property.NameEquals("type"u8)) + { + return property.Value.ValueKind == JsonValueKind.String + && property.Value.ValueEquals("object"u8); + } } } - } - return false; - } + return false; + } - private static JsonNode? JsonElementToJsonNode(JsonElement element) - { - return element.ValueKind switch - { - JsonValueKind.Null => null, - JsonValueKind.Array => JsonArray.Create(element), - JsonValueKind.Object => JsonObject.Create(element), - _ => JsonValue.Create(element) - }; + static JsonNode? JsonElementToJsonNode(JsonElement element) => + element.ValueKind switch + { + JsonValueKind.Null => null, + JsonValueKind.Array => JsonArray.Create(element), + JsonValueKind.Object => JsonObject.Create(element), + _ => JsonValue.Create(element) + }; } - - /// - /// Removes characters from a .NET member name that shouldn't be used in an AI function name. - /// - /// The .NET member name that should be sanitized. - /// - /// Replaces non-alphanumeric characters in the identifier with the underscore character. - /// Primarily intended to remove characters produced by compiler-generated method name mangling. - /// - private static string SanitizeMemberName(string memberName) => - InvalidNameCharsRegex().Replace(memberName, "_"); - - /// Regex that flags any character other than ASCII digits or letters or the underscore. -#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 } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs index c65bef12fc8..9ac67ff20dc 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs @@ -2,9 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using System.Text.Json; using Xunit; +#pragma warning disable SA1204 // Static elements should appear before instance elements + namespace Microsoft.Extensions.AI; public class ChatResponseFormatTests @@ -81,4 +86,96 @@ public void Serialization_ForJsonSchemaRoundtrips() Assert.Equal("name", actual.SchemaName); Assert.Equal("description", actual.SchemaDescription); } + + [Fact] + public void ForJsonSchema_NullType_Throws() + { + Assert.Throws("schemaType", () => ChatResponseFormat.ForJsonSchema(null!)); + Assert.Throws("schemaType", () => ChatResponseFormat.ForJsonSchema(null!, TestJsonSerializerContext.Default.Options)); + Assert.Throws("schemaType", () => ChatResponseFormat.ForJsonSchema(null!, TestJsonSerializerContext.Default.Options, "name")); + Assert.Throws("schemaType", () => ChatResponseFormat.ForJsonSchema(null!, TestJsonSerializerContext.Default.Options, "name", "description")); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_PrimitiveType_Succeeds(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema() : + ChatResponseFormat.ForJsonSchema(typeof(int)); + + Assert.NotNull(format); + Assert.NotNull(format.Schema); + Assert.Equal("""{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"integer"}""", format.Schema.ToString()); + Assert.Equal("Int32", format.SchemaName); + Assert.Null(format.SchemaDescription); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_IncludedType_Succeeds(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema() : + ChatResponseFormat.ForJsonSchema(typeof(DataContent)); + + Assert.NotNull(format); + Assert.NotNull(format.Schema); + Assert.Contains("\"uri\"", format.Schema.ToString()); + Assert.Equal("DataContent", format.SchemaName); + Assert.Null(format.SchemaDescription); + } + + public static IEnumerable ForJsonSchema_ComplexType_Succeeds_MemberData() => + from generic in new[] { false, true } + from name in new string?[] { null, "CustomName" } + from description in new string?[] { null, "CustomDescription" } + select new object?[] { generic, name, description }; + + [Theory] + [MemberData(nameof(ForJsonSchema_ComplexType_Succeeds_MemberData))] + public void ForJsonSchema_ComplexType_Succeeds(bool generic, string? name, string? description) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema(TestJsonSerializerContext.Default.Options, name, description) : + ChatResponseFormat.ForJsonSchema(typeof(SomeType), TestJsonSerializerContext.Default.Options, name, description); + + Assert.NotNull(format); + Assert.Equal( + """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "abcd", + "type": "object", + "properties": { + "someInteger": { + "description": "efg", + "type": "integer" + }, + "someString": { + "description": "hijk", + "type": [ + "string", + "null" + ] + } + } + } + """, + JsonSerializer.Serialize(format.Schema, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))); + Assert.Equal(name ?? "SomeType", format.SchemaName); + Assert.Equal(description ?? "abcd", format.SchemaDescription); + } + + [Description("abcd")] + public class SomeType + { + [Description("efg")] + public int SomeInteger { get; set; } + + [Description("hijk")] + public string? SomeString { get; set; } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 01de984d949..93c7a124e38 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -36,4 +36,5 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(Guid))] // Used in Content tests [JsonSerializable(typeof(decimal))] // Used in Content tests [JsonSerializable(typeof(HostedMcpServerToolApprovalMode))] +[JsonSerializable(typeof(ChatResponseFormatTests.SomeType))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs index dcb372d0571..431b2053d62 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs @@ -37,34 +37,28 @@ public async Task SuccessUsage_Default() Assert.NotNull(responseFormat.Schema); AssertDeepEquals(JsonDocument.Parse(""" { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Some test description", - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "fullName": { - "type": [ - "string", - "null" - ] - }, - "species": { - "type": "string", - "enum": [ - "Bear", - "Tiger", - "Walrus" - ] + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Some test description", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "fullName": { + "type": [ + "string", + "null" + ] + }, + "species": { + "type": "string", + "enum": [ + "Bear", + "Tiger", + "Walrus" + ] + } } - }, - "additionalProperties": false, - "required": [ - "id", - "fullName", - "species" - ] } """).RootElement, responseFormat.Schema.Value); Assert.Equal(nameof(Animal), responseFormat.SchemaName); @@ -380,29 +374,23 @@ public async Task CanSpecifyCustomJsonSerializationOptions() Assert.NotNull(responseFormat.Schema); AssertDeepEquals(JsonDocument.Parse(""" { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Some test description", - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "full_name": { - "type": [ - "string", - "null" - ] - }, - "species": { - "type": "integer" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Some test description", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "full_name": { + "type": [ + "string", + "null" + ] + }, + "species": { + "type": "integer" + } } - }, - "additionalProperties": false, - "required": [ - "id", - "full_name", - "species" - ] } """).RootElement, responseFormat.Schema.Value); From c4e8b3bca850a45899ccc33dc7ab031025f3bdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire?= Date: Mon, 15 Sep 2025 11:23:35 +0200 Subject: [PATCH 301/472] Make serviceKey nullable for ImageGenerator and ISpeechToTextClient (#6807) * Make serviceKey nullable for ImageGenerator Aligned with AddKeyedEmbeddingGenerator and AddKeyedChatClient. * Remove null check * Add test * Same for SpeechToTextClient --- ...eneratorBuilderServiceCollectionExtensions.cs | 5 ++--- ...xtClientBuilderServiceCollectionExtensions.cs | 5 ++--- .../ImageGeneratorDependencyInjectionPatterns.cs | 16 ++++++++++++++++ ...echToTextClientDependencyInjectionPatterns.cs | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs index ec19252a319..7868adf2eb3 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ImageGeneratorBuilderServiceCollectionExtensions.cs @@ -55,7 +55,7 @@ public static ImageGeneratorBuilder AddImageGenerator( /// The generator is registered as a scoped service. public static ImageGeneratorBuilder AddKeyedImageGenerator( this IServiceCollection serviceCollection, - object serviceKey, + object? serviceKey, IImageGenerator innerGenerator, ServiceLifetime lifetime = ServiceLifetime.Singleton) => AddKeyedImageGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime); @@ -70,12 +70,11 @@ public static ImageGeneratorBuilder AddKeyedImageGenerator( /// The generator is registered as a scoped service. public static ImageGeneratorBuilder AddKeyedImageGenerator( this IServiceCollection serviceCollection, - object serviceKey, + object? serviceKey, Func innerGeneratorFactory, ServiceLifetime lifetime = ServiceLifetime.Singleton) { _ = Throw.IfNull(serviceCollection); - _ = Throw.IfNull(serviceKey); _ = Throw.IfNull(innerGeneratorFactory); var builder = new ImageGeneratorBuilder(innerGeneratorFactory); diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs index 5ef54e8db26..243cb057068 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilderServiceCollectionExtensions.cs @@ -52,7 +52,7 @@ public static SpeechToTextClientBuilder AddSpeechToTextClient( /// The client is registered as a scoped service. public static SpeechToTextClientBuilder AddKeyedSpeechToTextClient( this IServiceCollection serviceCollection, - object serviceKey, + object? serviceKey, ISpeechToTextClient innerClient, ServiceLifetime lifetime = ServiceLifetime.Singleton) => AddKeyedSpeechToTextClient(serviceCollection, serviceKey, _ => innerClient, lifetime); @@ -66,12 +66,11 @@ public static SpeechToTextClientBuilder AddKeyedSpeechToTextClient( /// The client is registered as a scoped service. public static SpeechToTextClientBuilder AddKeyedSpeechToTextClient( this IServiceCollection serviceCollection, - object serviceKey, + object? serviceKey, Func innerClientFactory, ServiceLifetime lifetime = ServiceLifetime.Singleton) { _ = Throw.IfNull(serviceCollection); - _ = Throw.IfNull(serviceKey); _ = Throw.IfNull(innerClientFactory); var builder = new SpeechToTextClientBuilder(innerClientFactory); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs index b65495e506b..a17cd5a5c41 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorDependencyInjectionPatterns.cs @@ -154,6 +154,22 @@ public void AddKeyedImageGenerator_RegistersExpectedLifetime(ServiceLifetime? li Assert.Equal(expectedLifetime, sd.Lifetime); } + [Fact] + public void AddKeyedImageGenerator_WorksWithNullServiceKey() + { + ServiceCollection sc = new(); + sc.AddKeyedImageGenerator(null, _ => new TestImageGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IImageGenerator), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ServiceKey); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(ServiceLifetime.Singleton, sd.Lifetime); + } + public class SingletonMiddleware(IImageGenerator inner, IServiceProvider services) : DelegatingImageGenerator(inner) { public new IImageGenerator InnerGenerator => base.InnerGenerator; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs index 07596a1bb6f..5595e1c82ce 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/SpeechToTextClientDependencyInjectionPatterns.cs @@ -154,6 +154,22 @@ public void AddKeyedSpeechToTextClient_RegistersExpectedLifetime(ServiceLifetime Assert.Equal(expectedLifetime, sd.Lifetime); } + [Fact] + public void AddKeyedSpeechToTextClient_WorksWithNullServiceKey() + { + ServiceCollection sc = new(); + sc.AddKeyedSpeechToTextClient(null, _ => new TestSpeechToTextClient()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(ISpeechToTextClient), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ServiceKey); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(ServiceLifetime.Singleton, sd.Lifetime); + } + public class SingletonMiddleware(ISpeechToTextClient inner, IServiceProvider services) : DelegatingSpeechToTextClient(inner) { public new ISpeechToTextClient InnerClient => base.InnerClient; From 9a6863957e362249656a2d1b96afa2662cb36ea9 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 15 Sep 2025 08:54:58 -0400 Subject: [PATCH 302/472] Expose response format conversions for OpenAI types (#6806) --- .../CHANGELOG.md | 2 + .../MicrosoftExtensionsAIChatExtensions.cs | 9 +++ ...icrosoftExtensionsAIResponsesExtensions.cs | 9 +++ .../OpenAIChatClient.cs | 35 ++++----- .../OpenAIResponsesChatClient.cs | 41 +++++------ .../OpenAIConversionTests.cs | 73 +++++++++++++++++++ 6 files changed, 131 insertions(+), 38 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 6673d09d52f..4d858edeb89 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -2,6 +2,8 @@ ## NOT YET RELEASED +- Added M.E.AI to OpenAI conversions for response format types + ## 9.9.0-preview.1.25458.4 - Updated to depend on OpenAI 2.4.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs index 113e91c3305..79c9b74d4da 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -26,6 +26,15 @@ public static class MicrosoftExtensionsAIChatExtensions public static ChatTool AsOpenAIChatTool(this AIFunctionDeclaration function) => OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function)); + /// + /// Creates an OpenAI from a . + /// + /// The format. + /// The options to use when interpreting the format. + /// The converted OpenAI . + public static ChatResponseFormat? AsOpenAIChatResponseFormat(this Microsoft.Extensions.AI.ChatResponseFormat? format, ChatOptions? options = null) => + OpenAIChatClient.ToOpenAIChatResponseFormat(format, options); + /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. /// The options employed while processing . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 51df2653695..ca4bfd3f736 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -21,6 +21,15 @@ public static class MicrosoftExtensionsAIResponsesExtensions public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function)); + /// + /// Creates an OpenAI from a . + /// + /// The format. + /// The options to use when interpreting the format. + /// The converted OpenAI . + public static ResponseTextFormat? AsOpenAIResponseTextFormat(this ChatResponseFormat? format, ChatOptions? options = null) => + OpenAIResponsesChatClient.ToOpenAIResponseTextFormat(format, options); + /// Creates a sequence of OpenAI instances from the specified input messages. /// The input messages to convert. /// The options employed while processing . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 8c65766413f..67037001071 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -582,27 +582,28 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) } } - if (result.ResponseFormat is null) - { - if (options.ResponseFormat is ChatResponseFormatText) - { - result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat(); - } - else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) - { - result.ResponseFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? - OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( - jsonFormat.SchemaName ?? "json_schema", - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription, - OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) : - OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(); - } - } + result.ResponseFormat ??= ToOpenAIChatResponseFormat(options.ResponseFormat, options); return result; } + internal static OpenAI.Chat.ChatResponseFormat? ToOpenAIChatResponseFormat(ChatResponseFormat? format, ChatOptions? options) => + format switch + { + ChatResponseFormatText => OpenAI.Chat.ChatResponseFormat.CreateTextFormat(), + + ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema => + OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat( + jsonFormat.SchemaName ?? "json_schema", + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), + jsonFormat.SchemaDescription, + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)), + + ChatResponseFormatJson => OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(), + + _ => null + }; + private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage) { var destination = new UsageDetails diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index c025ae14d8a..051e7c15dd7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -520,33 +520,32 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt } } - if (result.TextOptions is null) + if (result.TextOptions?.TextFormat is null && + ToOpenAIResponseTextFormat(options.ResponseFormat, options) is { } newFormat) { - if (options.ResponseFormat is ChatResponseFormatText) - { - result.TextOptions = new() - { - TextFormat = ResponseTextFormat.CreateTextFormat() - }; - } - else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) - { - result.TextOptions = new() - { - TextFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ? - ResponseTextFormat.CreateJsonSchemaFormat( - jsonFormat.SchemaName ?? "json_schema", - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), - jsonFormat.SchemaDescription, - OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) : - ResponseTextFormat.CreateJsonObjectFormat(), - }; - } + (result.TextOptions ??= new()).TextFormat = newFormat; } return result; } + internal static ResponseTextFormat? ToOpenAIResponseTextFormat(ChatResponseFormat? format, ChatOptions? options = null) => + format switch + { + ChatResponseFormatText => ResponseTextFormat.CreateTextFormat(), + + ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema => + ResponseTextFormat.CreateJsonSchemaFormat( + jsonFormat.SchemaName ?? "json_schema", + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)), + jsonFormat.SchemaDescription, + OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)), + + ChatResponseFormatJson => ResponseTextFormat.CreateJsonObjectFormat(), + + _ => null, + }; + /// Convert a sequence of s to s. internal static IEnumerable ToOpenAIResponseItems(IEnumerable inputs, ChatOptions? options) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 378ab8e8101..b919592da4d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using OpenAI.Assistants; using OpenAI.Chat; @@ -22,6 +24,75 @@ public class OpenAIConversionTests "test_function", "A test function for conversion"); + [Fact] + public void AsOpenAIChatResponseFormat_HandlesVariousFormats() + { + Assert.Null(MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(null)); + + var text = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Text); + Assert.NotNull(text); + Assert.Equal("""{"type":"text"}""", ((IJsonModel)text).Write(ModelReaderWriterOptions.Json).ToString()); + + var json = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Json); + Assert.NotNull(json); + Assert.Equal("""{"type":"json_object"}""", ((IJsonModel)json).Write(ModelReaderWriterOptions.Json).ToString()); + + var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat(); + Assert.NotNull(jsonSchema); + Assert.Equal(RemoveWhitespace(""" + {"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + }}} + """), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString())); + + jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat( + new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } }); + Assert.NotNull(jsonSchema); + Assert.Equal(RemoveWhitespace(""" + { + "type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + },"strict":true}} + """), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString())); + } + + [Fact] + public void AsOpenAIResponseTextFormat_HandlesVariousFormats() + { + Assert.Null(MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(null)); + + var text = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Text); + Assert.NotNull(text); + Assert.Equal(ResponseTextFormatKind.Text, text.Kind); + + var json = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Json); + Assert.NotNull(json); + Assert.Equal(ResponseTextFormatKind.JsonObject, json.Kind); + + var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat(); + Assert.NotNull(jsonSchema); + Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind); + Assert.Equal(RemoveWhitespace(""" + {"type":"json_schema","description":"A test schema","name":"my_schema","schema":{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + }} + """), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString())); + + jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat( + new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } }); + Assert.NotNull(jsonSchema); + Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind); + Assert.Equal(RemoveWhitespace(""" + {"type":"json_schema","description":"A test schema","name":"my_schema","schema":{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + },"strict":true} + """), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString())); + } + [Fact] public void AsOpenAIChatTool_ProducesValidInstance() { @@ -1113,4 +1184,6 @@ private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable yield return item; } } + + private static string RemoveWhitespace(string input) => Regex.Replace(input, @"\s+", ""); } From 3176d4c550ab2f8172d4b7aec3fd8d25b880e5e1 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 15 Sep 2025 08:55:12 -0400 Subject: [PATCH 303/472] Add TextReasoningContent.ProtectedData (#6784) --- .../CHANGELOG.md | 1 + .../ChatCompletion/ChatResponseExtensions.cs | 32 +++++++++++-------- .../Contents/TextReasoningContent.cs | 16 ++++++++++ .../Microsoft.Extensions.AI.Abstractions.json | 4 +++ .../OpenAIResponsesChatClient.cs | 13 ++++++-- .../Contents/TextReasoningContentTests.cs | 7 ++++ 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 4bb67980439..cc911fd64bb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -3,6 +3,7 @@ ## NOT YET RELEASED - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. +- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index b9909857be2..933c6412c20 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -186,17 +186,17 @@ static async Task ToChatResponseAsync( /// Coalesces sequential content elements. internal static void CoalesceTextContent(IList contents) { - Coalesce(contents, mergeSingle: false, static (contents, start, end) => - new(MergeText(contents, start, end)) - { - AdditionalProperties = contents[start].AdditionalProperties?.Clone() - }); - - Coalesce(contents, mergeSingle: false, static (contents, start, end) => - new(MergeText(contents, start, end)) - { - AdditionalProperties = contents[start].AdditionalProperties?.Clone() - }); + Coalesce( + contents, + mergeSingle: false, + canMerge: null, + static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() }); + + Coalesce( + contents, + mergeSingle: false, + canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does + static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() }); static string MergeText(IList contents, int start, int end) { @@ -209,7 +209,11 @@ static string MergeText(IList contents, int start, int end) return sb.ToString(); } - static void Coalesce(IList contents, bool mergeSingle, Func, int, int, TContent> merge) + static void Coalesce( + IList contents, + bool mergeSingle, + Func? canMerge, + Func, int, int, TContent> merge) where TContent : AIContent { // Iterate through all of the items in the list looking for contiguous items that can be coalesced. @@ -224,9 +228,11 @@ static void Coalesce(IList contents, bool mergeSingle, Func // Iterate until we find a non-coalescable item. int i = start + 1; - while (i < contents.Count && TryAsCoalescable(contents[i], out _)) + TContent prev = firstContent; + while (i < contents.Count && TryAsCoalescable(contents[i], out TContent? next) && (canMerge is null || canMerge(prev, next))) { i++; + prev = next; } // If there's only one item in the run, and we don't want to merge single items, skip it. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs index ccf84af2e3d..345cb430dbd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs @@ -38,6 +38,22 @@ public string Text set => _text = value; } + /// Gets or sets an optional opaque blob of data associated with this reasoning content. + /// + /// + /// This property is used to store data from a provider that should be roundtripped back to the provider but that is not + /// intended for human consumption. It is often encrypted or otherwise redacted information that is only intended to be + /// sent back to the provider and not displayed to the user. It's possible for a to contain + /// only and have an empty property. This data also may be associated with + /// the corresponding , acting as a validation signature for it. + /// + /// + /// Note that whereas can be provider agnostic, + /// is provider-specific, and is likely to only be understood by the provider that created it. + /// + /// + public string? ProtectedData { get; set; } + /// public override string ToString() => Text; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 034b5787eab..24239ee52b3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -2430,6 +2430,10 @@ { "Member": "string Microsoft.Extensions.AI.TextReasoningContent.Text { get; set; }", "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ProtectedData { get; set; }", + "Stage": "Stable" } ] }, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 051e7c15dd7..36bf4ca68b4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -17,6 +17,7 @@ #pragma warning disable S907 // "goto" statement should not be used #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3254 // Default parameter values should not be passed as arguments #pragma warning disable S3604 // Member initializer values should not be redundant #pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1204 // Static elements should appear before instance elements @@ -149,8 +150,12 @@ internal static IEnumerable ToChatMessages(IEnumerable)message.Contents).AddRange(ToAIContents(messageItem.Content)); break; - case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary: - message.Contents.Add(new TextReasoningContent(summary) { RawRepresentation = outputItem }); + case ReasoningResponseItem reasoningItem: + message.Contents.Add(new TextReasoningContent(reasoningItem.GetSummaryText()) + { + ProtectedData = reasoningItem.EncryptedContent, + RawRepresentation = outputItem, + }); break; case FunctionCallResponseItem functionCall: @@ -626,7 +631,9 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable Date: Mon, 15 Sep 2025 09:23:55 -0400 Subject: [PATCH 304/472] Add otel middleware for IImageGenerator (#6809) --- .../Image/ImageGenerationOptions.cs | 4 + .../Image/ImageGenerationResponse.cs | 3 + .../OpenAIImageGenerator.cs | 21 +- .../Microsoft.Extensions.AI/CHANGELOG.md | 1 + .../ChatCompletion/OpenTelemetryChatClient.cs | 40 ++- .../OpenTelemetryImageGenerator.cs | 313 ++++++++++++++++++ ...elemetryImageGeneratorBuilderExtensions.cs | 42 +++ .../OpenTelemetryEmbeddingGenerator.cs | 2 +- .../OpenTelemetryConsts.cs | 3 + .../Image/ImageGeneratorBuilderTests.cs | 106 ++++++ .../Image/OpenTelemetryImageGeneratorTests.cs | 163 +++++++++ 11 files changed, 690 insertions(+), 8 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGeneratorBuilderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorBuilderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/Image/OpenTelemetryImageGeneratorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs index 3931173acfc..bf95b5d5aed 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs @@ -59,12 +59,16 @@ public class ImageGenerationOptions /// public ImageGenerationResponseFormat? ResponseFormat { get; set; } + /// Gets or sets any additional properties associated with the options. + public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } + /// Produces a clone of the current instance. /// A clone of the current instance. public virtual ImageGenerationOptions Clone() { ImageGenerationOptions options = new() { + AdditionalProperties = AdditionalProperties?.Clone(), Count = Count, MediaType = MediaType, ImageSize = ImageSize, diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs index 40011821293..53c33b22978 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs @@ -51,4 +51,7 @@ public IList Contents get => _contents ??= []; set => _contents = value; } + + /// Gets or sets usage details for the image generation response. + public UsageDetails? Usage { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs index b2ceb5cb317..9281167d917 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -163,9 +163,28 @@ private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageC } } + UsageDetails? ud = null; + if (generatedImages.Usage is { } usage) + { + ud = new() + { + InputTokenCount = usage.InputTokenCount, + OutputTokenCount = usage.OutputTokenCount, + TotalTokenCount = usage.TotalTokenCount, + }; + + if (usage.InputTokenDetails is { } inputDetails) + { + ud.AdditionalCounts ??= []; + ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.ImageTokenCount)}", inputDetails.ImageTokenCount); + ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.TextTokenCount)}", inputDetails.TextTokenCount); + } + } + return new ImageGenerationResponse(contents) { - RawRepresentation = generatedImages + RawRepresentation = generatedImages, + Usage = ud, }; } diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 1a4e630ca0a..71aa3927034 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -3,6 +3,7 @@ ## NOT YET RELEASED - Updated the EnableSensitiveData properties on OpenTelemetryChatClient/EmbeddingGenerator to respect a OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT environment variable. +- Added OpenTelemetryImageGenerator to provide OpenTelemetry instrumentation for IImageGenerator implementations. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index d06ec3d3b5e..41cbc3a140f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -217,7 +217,7 @@ public override async IAsyncEnumerable GetStreamingResponseA } } - private static string SerializeChatMessages(IEnumerable messages, ChatFinishReason? chatFinishReason = null) + internal static string SerializeChatMessages(IEnumerable messages, ChatFinishReason? chatFinishReason = null) { List output = []; @@ -244,6 +244,12 @@ private static string SerializeChatMessages(IEnumerable messages, C { switch (content) { + // These are all specified in the convention: + + case TextContent tc when !string.IsNullOrWhiteSpace(tc.Text): + m.Parts.Add(new OtelGenericPart { Content = tc.Text }); + break; + case FunctionCallContent fcc: m.Parts.Add(new OtelToolCallRequestPart { @@ -261,8 +267,30 @@ private static string SerializeChatMessages(IEnumerable messages, C }); break; - case TextContent tc: - m.Parts.Add(new OtelGenericPart { Content = tc.Text }); + // These are non-standard and are using the "generic" non-text part that provides an extensibility mechanism: + + case TextReasoningContent trc when !string.IsNullOrWhiteSpace(trc.Text): + m.Parts.Add(new OtelGenericPart { Type = "reasoning", Content = trc.Text }); + break; + + case UriContent uc: + m.Parts.Add(new OtelGenericPart { Type = "image", Content = uc.Uri.ToString() }); + break; + + case DataContent dc: + m.Parts.Add(new OtelGenericPart { Type = "image", Content = dc.Uri }); + break; + + case HostedFileContent fc: + m.Parts.Add(new OtelGenericPart { Type = "file", Content = fc.FileId }); + break; + + case HostedVectorStoreContent vsc: + m.Parts.Add(new OtelGenericPart { Type = "vector_store", Content = vsc.VectorStoreId }); + break; + + case ErrorContent ec: + m.Parts.Add(new OtelGenericPart { Type = "error", Content = ec.Message }); break; default: @@ -293,7 +321,7 @@ private static string SerializeChatMessages(IEnumerable messages, C string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Chat : $"{OpenTelemetryConsts.GenAI.Chat} {modelId}", ActivityKind.Client); - if (activity is not null) + if (activity is { IsAllDataRequested: true }) { _ = activity .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat) @@ -411,7 +439,7 @@ private void TraceResponse( TagList tags = default; tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); AddMetricTags(ref tags, requestModelId, response); - _tokenUsageHistogram.Record((int)inputTokens); + _tokenUsageHistogram.Record((int)inputTokens, tags); } if (usage.OutputTokenCount is long outputTokens) @@ -419,7 +447,7 @@ private void TraceResponse( TagList tags = default; tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeOutput); AddMetricTags(ref tags, requestModelId, response); - _tokenUsageHistogram.Record((int)outputTokens); + _tokenUsageHistogram.Record((int)outputTokens, tags); } } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs new file mode 100644 index 00000000000..bf156a2ad69 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs @@ -0,0 +1,313 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using System.Drawing; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable S3358 // Ternary operators should not be nested +#pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter +#pragma warning disable SA1113 // Comma should be on the same line as previous parameter + +namespace Microsoft.Extensions.AI; + +/// Represents a delegating image generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. +/// +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.37, defined at . +/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. +/// +[Experimental("MEAI001")] +public sealed class OpenTelemetryImageGenerator : DelegatingImageGenerator +{ + private readonly ActivitySource _activitySource; + private readonly Meter _meter; + + private readonly Histogram _tokenUsageHistogram; + private readonly Histogram _operationDurationHistogram; + + private readonly string? _defaultModelId; + private readonly string? _providerName; + private readonly string? _serverAddress; + private readonly int _serverPort; + + /// Initializes a new instance of the class. + /// The underlying . + /// The to use for emitting any logging data from the client. + /// An optional source name that will be used on the telemetry data. +#pragma warning disable IDE0060 // Remove unused parameter; it exists for consistency with IChatClient and future use + public OpenTelemetryImageGenerator(IImageGenerator innerGenerator, ILogger? logger = null, string? sourceName = null) +#pragma warning restore IDE0060 + : base(innerGenerator) + { + Debug.Assert(innerGenerator is not null, "Should have been validated by the base ctor"); + + if (innerGenerator!.GetService() is ImageGeneratorMetadata metadata) + { + _defaultModelId = metadata.DefaultModelId; + _providerName = metadata.ProviderName; + _serverAddress = metadata.ProviderUri?.Host; + _serverPort = metadata.ProviderUri?.Port ?? 0; + } + + string name = string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!; + _activitySource = new(name); + _meter = new(name); + + _tokenUsageHistogram = _meter.CreateHistogram( + OpenTelemetryConsts.GenAI.Client.TokenUsage.Name, + OpenTelemetryConsts.TokensUnit, + OpenTelemetryConsts.GenAI.Client.TokenUsage.Description +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.TokenUsage.ExplicitBucketBoundaries } +#endif + ); + + _operationDurationHistogram = _meter.CreateHistogram( + OpenTelemetryConsts.GenAI.Client.OperationDuration.Name, + OpenTelemetryConsts.SecondsUnit, + OpenTelemetryConsts.GenAI.Client.OperationDuration.Description +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.OperationDuration.ExplicitBucketBoundaries } +#endif + ); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activitySource.Dispose(); + _meter.Dispose(); + } + + base.Dispose(disposing); + } + + /// + /// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry. + /// + /// + /// if potentially sensitive information should be included in telemetry; + /// if telemetry shouldn't include raw inputs and outputs. + /// The default value is , unless the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT + /// environment variable is set to "true" (case-insensitive). + /// + /// + /// By default, telemetry includes metadata, such as token counts, but not raw inputs + /// and outputs, such as message content, function call arguments, and function call results. + /// The default value can be overridden by setting the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT + /// environment variable to "true". Explicitly setting this property will override the environment variable. + /// + public bool EnableSensitiveData { get; set; } = TelemetryHelpers.EnableSensitiveDataDefault; + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) => + serviceType == typeof(ActivitySource) ? _activitySource : + base.GetService(serviceType, serviceKey); + + /// + public async override Task GenerateAsync( + ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(request); + + using Activity? activity = CreateAndConfigureActivity(request, options); + Stopwatch? stopwatch = _operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null; + string? requestModelId = options?.ModelId ?? _defaultModelId; + + ImageGenerationResponse? response = null; + Exception? error = null; + try + { + response = await base.GenerateAsync(request, options, cancellationToken).ConfigureAwait(false); + return response; + } + catch (Exception ex) + { + error = ex; + throw; + } + finally + { + TraceResponse(activity, requestModelId, response, error, stopwatch); + } + } + + /// Creates an activity for an image generation request, or returns if not enabled. + private Activity? CreateAndConfigureActivity(ImageGenerationRequest request, ImageGenerationOptions? options) + { + Activity? activity = null; + if (_activitySource.HasListeners()) + { + string? modelId = options?.ModelId ?? _defaultModelId; + + activity = _activitySource.StartActivity( + string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContent : $"{OpenTelemetryConsts.GenAI.GenerateContent} {modelId}", + ActivityKind.Client); + + if (activity is { IsAllDataRequested: true }) + { + _ = activity + .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent) + .AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeImage) + .AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId) + .AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); + + if (_serverAddress is not null) + { + _ = activity + .AddTag(OpenTelemetryConsts.Server.Address, _serverAddress) + .AddTag(OpenTelemetryConsts.Server.Port, _serverPort); + } + + if (options is not null) + { + if (options.Count is int count) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.ChoiceCount, count); + } + + // Otel hasn't yet standardized tags for image generation parameters; these are based on other systems. + if (options.ImageSize is Size size) + { + _ = activity + .AddTag("gen_ai.request.image.width", size.Width) + .AddTag("gen_ai.request.image.height", size.Height); + } + } + + if (EnableSensitiveData) + { + List content = []; + + if (request.Prompt is not null) + { + content.Add(new TextContent(request.Prompt)); + } + + if (request.OriginalImages is not null) + { + content.AddRange(request.OriginalImages); + } + + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Input.Messages, + OpenTelemetryChatClient.SerializeChatMessages([new(ChatRole.User, content)])); + + if (options?.AdditionalProperties is { } props) + { + foreach (KeyValuePair prop in props) + { + _ = activity.AddTag(prop.Key, prop.Value); + } + } + } + } + } + + return activity; + } + + /// Adds image generation response information to the activity. + private void TraceResponse( + Activity? activity, + string? requestModelId, + ImageGenerationResponse? response, + Exception? error, + Stopwatch? stopwatch) + { + if (_operationDurationHistogram.Enabled && stopwatch is not null) + { + TagList tags = default; + + AddMetricTags(ref tags, requestModelId); + if (error is not null) + { + tags.Add(OpenTelemetryConsts.Error.Type, error.GetType().FullName); + } + + _operationDurationHistogram.Record(stopwatch.Elapsed.TotalSeconds, tags); + } + + if (error is not null) + { + _ = activity? + .AddTag(OpenTelemetryConsts.Error.Type, error.GetType().FullName) + .SetStatus(ActivityStatusCode.Error, error.Message); + } + + if (response is not null) + { + if (EnableSensitiveData && + response.Contents is { Count: > 0 } contents && + activity is { IsAllDataRequested: true }) + { + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Output.Messages, + OpenTelemetryChatClient.SerializeChatMessages([new(ChatRole.Assistant, contents)])); + } + + if (response.Usage is { } usage) + { + if (_tokenUsageHistogram.Enabled) + { + if (usage.InputTokenCount is long inputTokens) + { + TagList tags = default; + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); + AddMetricTags(ref tags, requestModelId); + _tokenUsageHistogram.Record((int)inputTokens, tags); + } + + if (usage.OutputTokenCount is long outputTokens) + { + TagList tags = default; + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeOutput); + AddMetricTags(ref tags, requestModelId); + _tokenUsageHistogram.Record((int)outputTokens, tags); + } + } + + if (activity is { IsAllDataRequested: true }) + { + if (usage.InputTokenCount is long inputTokens) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.InputTokens, (int)inputTokens); + } + + if (usage.OutputTokenCount is long outputTokens) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.OutputTokens, (int)outputTokens); + } + } + } + } + + void AddMetricTags(ref TagList tags, string? requestModelId) + { + tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent); + + if (requestModelId is not null) + { + tags.Add(OpenTelemetryConsts.GenAI.Request.Model, requestModelId); + } + + tags.Add(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); + + if (_serverAddress is string endpointAddress) + { + tags.Add(OpenTelemetryConsts.Server.Address, endpointAddress); + tags.Add(OpenTelemetryConsts.Server.Port, _serverPort); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGeneratorBuilderExtensions.cs new file mode 100644 index 00000000000..63919505590 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGeneratorBuilderExtensions.cs @@ -0,0 +1,42 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class OpenTelemetryImageGeneratorBuilderExtensions +{ + /// + /// Adds OpenTelemetry support to the image generator pipeline, following the OpenTelemetry Semantic Conventions for Generative AI systems. + /// + /// + /// The draft specification this follows is available at . + /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. + /// + /// The . + /// An optional to use to create a logger for logging events. + /// An optional source name that will be used on the telemetry data. + /// An optional callback that can be used to configure the instance. + /// The . + public static ImageGeneratorBuilder UseOpenTelemetry( + this ImageGeneratorBuilder builder, + ILoggerFactory? loggerFactory = null, + string? sourceName = null, + Action? configure = null) => + Throw.IfNull(builder).Use((innerGenerator, services) => + { + loggerFactory ??= services.GetService(); + + var g = new OpenTelemetryImageGenerator(innerGenerator, loggerFactory?.CreateLogger(typeof(OpenTelemetryImageGenerator)), sourceName); + configure?.Invoke(g); + + return g; + }); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 4b8f58ed7fb..e23f59e56c6 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -229,7 +229,7 @@ private void TraceResponse( tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); AddMetricTags(ref tags, requestModelId, responseModelId); - _tokenUsageHistogram.Record(inputTokens.Value); + _tokenUsageHistogram.Record(inputTokens.Value, tags); } if (activity is not null) diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index 096b2091908..48c32e2992d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -21,6 +21,7 @@ internal static class OpenTelemetryConsts public const string TypeText = "text"; public const string TypeJson = "json"; + public const string TypeImage = "image"; public const string TokenTypeInput = "input"; public const string TokenTypeOutput = "output"; @@ -35,6 +36,7 @@ public static class GenAI public const string Chat = "chat"; public const string Embeddings = "embeddings"; public const string ExecuteTool = "execute_tool"; + public const string GenerateContent = "generate_content"; public const string SystemInstructions = "gen_ai.system_instructions"; @@ -83,6 +85,7 @@ public static class Provider public static class Request { + public const string ChoiceCount = "gen_ai.request.choice.count"; public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions"; public const string FrequencyPenalty = "gen_ai.request.frequency_penalty"; public const string Model = "gen_ai.request.model"; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorBuilderTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorBuilderTests.cs new file mode 100644 index 00000000000..c0cfdc3ea06 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/ImageGeneratorBuilderTests.cs @@ -0,0 +1,106 @@ +// 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.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ImageGeneratorBuilderTests +{ + [Fact] + public void PassesServiceProviderToFactories() + { + var expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); + using TestImageGenerator expectedInnerGenerator = new(); + using TestImageGenerator expectedOuterGenerator = new(); + + var builder = new ImageGeneratorBuilder(services => + { + Assert.Same(expectedServiceProvider, services); + return expectedInnerGenerator; + }); + + builder.Use((innerGenerator, serviceProvider) => + { + Assert.Same(expectedServiceProvider, serviceProvider); + Assert.Same(expectedInnerGenerator, innerGenerator); + return expectedOuterGenerator; + }); + + Assert.Same(expectedOuterGenerator, builder.Build(expectedServiceProvider)); + } + + [Fact] + public void BuildsPipelineInOrderAdded() + { + // Arrange + using TestImageGenerator expectedInnerGenerator = new(); + var builder = new ImageGeneratorBuilder(expectedInnerGenerator); + + builder.Use(next => new InnerGeneratorCapturingImageGenerator("First", next)); + builder.Use(next => new InnerGeneratorCapturingImageGenerator("Second", next)); + builder.Use(next => new InnerGeneratorCapturingImageGenerator("Third", next)); + + // Act + var first = (InnerGeneratorCapturingImageGenerator)builder.Build(); + + // Assert + Assert.Equal("First", first.Name); + var second = (InnerGeneratorCapturingImageGenerator)first.InnerGenerator; + Assert.Equal("Second", second.Name); + var third = (InnerGeneratorCapturingImageGenerator)second.InnerGenerator; + Assert.Equal("Third", third.Name); + Assert.Same(expectedInnerGenerator, third.InnerGenerator); + } + + [Fact] + public void DoesNotAcceptNullInnerService() + { + Assert.Throws("innerGenerator", () => new ImageGeneratorBuilder((IImageGenerator)null!)); + Assert.Throws("innerGenerator", () => ((IImageGenerator)null!).AsBuilder()); + } + + [Fact] + public void DoesNotAcceptNullFactories() + { + Assert.Throws("innerGeneratorFactory", () => new ImageGeneratorBuilder((Func)null!)); + } + + [Fact] + public void DoesNotAllowFactoriesToReturnNull() + { + using var innerGenerator = new TestImageGenerator(); + ImageGeneratorBuilder builder = new(innerGenerator); + builder.Use(_ => null!); + var ex = Assert.Throws(() => builder.Build()); + Assert.Contains("entry at index 0", ex.Message); + } + + [Fact] + public void UsesEmptyServiceProviderWhenNoServicesProvided() + { + using var innerGenerator = new TestImageGenerator(); + ImageGeneratorBuilder builder = new(innerGenerator); + builder.Use((innerGenerator, serviceProvider) => + { + Assert.Null(serviceProvider.GetService(typeof(object))); + + var keyedServiceProvider = Assert.IsAssignableFrom(serviceProvider); + Assert.Null(keyedServiceProvider.GetKeyedService(typeof(object), "key")); + Assert.Throws(() => keyedServiceProvider.GetRequiredKeyedService(typeof(object), "key")); + + return innerGenerator; + }); + builder.Build(); + } + + private sealed class InnerGeneratorCapturingImageGenerator(string name, IImageGenerator innerGenerator) : DelegatingImageGenerator(innerGenerator) + { +#pragma warning disable S3604 // False positive: Member initializer values should not be redundant + public string Name { get; } = name; +#pragma warning restore S3604 + public new IImageGenerator InnerGenerator => base.InnerGenerator; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Image/OpenTelemetryImageGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/OpenTelemetryImageGeneratorTests.cs new file mode 100644 index 00000000000..21b72600d43 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Image/OpenTelemetryImageGeneratorTests.cs @@ -0,0 +1,163 @@ +// 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; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using OpenTelemetry.Trace; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenTelemetryImageGeneratorTests +{ + [Fact] + public void InvalidArgs_Throws() + { + Assert.Throws("innerGenerator", () => new OpenTelemetryImageGenerator(null!)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExpectedInformationLogged_Async(bool enableSensitiveData) + { + var sourceName = Guid.NewGuid().ToString(); + var activities = new List(); + using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddInMemoryExporter(activities) + .Build(); + + using var innerGenerator = new TestImageGenerator + { + GenerateImagesAsyncCallback = async (request, options, cancellationToken) => + { + await Task.Yield(); + + return new() + { + Contents = + [ + new UriContent("http://example/output.png", "image/png"), + new DataContent(new byte[] { 1, 2, 3, 4 }, "image/png") { Name = "moreOutput.png" }, + ], + + Usage = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + }, + }; + }, + + GetServiceCallback = (serviceType, serviceKey) => + serviceType == typeof(ImageGeneratorMetadata) ? new ImageGeneratorMetadata("testservice", new Uri("http://localhost:12345/something"), "amazingmodel") : + null, + }; + + using var g = innerGenerator + .AsBuilder() + .UseOpenTelemetry(null, sourceName, configure: instance => + { + instance.EnableSensitiveData = enableSensitiveData; + }) + .Build(); + + ImageGenerationRequest request = new() + { + Prompt = "This is the input prompt.", + OriginalImages = [new UriContent("http://example/input.png", "image/png")], + }; + + ImageGenerationOptions options = new() + { + Count = 2, + ImageSize = new(1024, 768), + MediaType = "image/jpeg", + ModelId = "mycoolimagemodel", + AdditionalProperties = new() + { + ["service_tier"] = "value1", + ["SomethingElse"] = "value2", + }, + }; + + await g.GenerateAsync(request, options); + + var activity = Assert.Single(activities); + + Assert.NotNull(activity.Id); + Assert.NotEmpty(activity.Id); + + Assert.Equal("localhost", activity.GetTagItem("server.address")); + Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); + + Assert.Equal("generate_content mycoolimagemodel", activity.DisplayName); + Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name")); + + Assert.Equal("mycoolimagemodel", activity.GetTagItem("gen_ai.request.model")); + Assert.Equal(2, activity.GetTagItem("gen_ai.request.choice.count")); + Assert.Equal(1024, activity.GetTagItem("gen_ai.request.image.width")); + Assert.Equal(768, activity.GetTagItem("gen_ai.request.image.height")); + Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("service_tier")); + Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("SomethingElse")); + + Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens")); + Assert.Equal(20, activity.GetTagItem("gen_ai.usage.output_tokens")); + + Assert.True(activity.Duration.TotalMilliseconds > 0); + + var tags = activity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + if (enableSensitiveData) + { + Assert.Equal(ReplaceWhitespace(""" + [ + { + "role": "user", + "parts": [ + { + "type": "text", + "content": "This is the input prompt." + }, + { + "type": "image", + "content": "http://example/input.png" + } + ] + } + ] + """), ReplaceWhitespace(tags["gen_ai.input.messages"])); + + Assert.Equal(ReplaceWhitespace(""" + [ + { + "role": "assistant", + "parts": [ + { + "type": "image", + "content": "http://example/output.png" + }, + { + "type": "image", + "content": "data:image/png;base64,AQIDBA==" + } + ] + } + ] + """), ReplaceWhitespace(tags["gen_ai.output.messages"])); + } + else + { + Assert.False(tags.ContainsKey("gen_ai.input.messages")); + Assert.False(tags.ContainsKey("gen_ai.output.messages")); + } + + static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); + } +} From f04bd506a04b585486be20fb7b08157162f2f527 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 16 Sep 2025 19:28:12 +0300 Subject: [PATCH 305/472] Fix mapping of MinLength/MaxLength/Length attribute mapping in nullable string properties. (#6812) --- .../AIJsonUtilities.Schema.Create.cs | 58 ++++++++++++++++++- .../Utilities/AIJsonUtilitiesTests.cs | 40 +++++++++++-- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index da0124639de..986f39d07ef 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -427,7 +427,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx) if (ResolveAttribute() is { } minLengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); - if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string") { obj[MinLengthStringPropertyName] ??= minLengthAttribute.Length; } @@ -440,7 +440,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx) if (ResolveAttribute() is { } maxLengthAttribute) { JsonObject obj = ConvertSchemaToObject(ref schema); - if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string") { obj[MaxLengthStringPropertyName] ??= maxLengthAttribute.Length; } @@ -530,7 +530,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx) { JsonObject obj = ConvertSchemaToObject(ref schema); - if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue() is "string") + if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string") { if (lengthAttribute.MinimumLength > 0) { @@ -629,6 +629,58 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali } } #endif +#if NET || NETFRAMEWORK + static bool TryGetSchemaType(JsonObject schema, [NotNullWhen(true)] out string? schemaType, out bool isNullable) + { + schemaType = null; + isNullable = false; + + if (!schema.TryGetPropertyValue(TypePropertyName, out JsonNode? typeNode)) + { + return false; + } + + switch (typeNode?.GetValueKind()) + { + case JsonValueKind.String: + schemaType = typeNode.GetValue(); + return true; + + case JsonValueKind.Array: + string? foundSchemaType = null; + foreach (JsonNode? entry in (JsonArray)typeNode) + { + if (entry?.GetValueKind() is not JsonValueKind.String) + { + return false; + } + + string entryValue = entry.GetValue(); + if (entryValue is "null") + { + isNullable = true; + continue; + } + + if (foundSchemaType is null) + { + foundSchemaType = entryValue; + } + else if (foundSchemaType != entryValue) + { + return false; + } + } + + schemaType = foundSchemaType; + return schemaType is not null; + + default: + return false; + } + } +#endif + TAttribute? ResolveAttribute() where TAttribute : Attribute { 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 11926e5132e..d83e4ad716b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -670,22 +670,22 @@ public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_Net() "string", "null" ], - "minItems": 5 + "minLength": 5 }, "MaxLengthProp": { "type": [ "string", "null" ], - "maxItems": 50 + "maxLength": 50 }, "LengthProp": { "type": [ "string", "null" ], - "minItems": 3, - "maxItems": 10 + "minLength": 3, + "maxLength": 10 }, "MinLengthArrayProp": { "type": [ @@ -848,14 +848,14 @@ public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_NetFx() "string", "null" ], - "minItems": 5 + "minLength": 5 }, "MaxLengthProp": { "type": [ "string", "null" ], - "maxItems": 50 + "maxLength": 50 }, "MinLengthArrayProp": { "type": [ @@ -972,6 +972,33 @@ private sealed class CreateJsonSchema_IncorporatesTypesAndAnnotations_Type #endif } + [Fact] + public static void ClassWithNullableMaxLengthProperty_ReturnsExpectedSchema() + { + JsonElement expectedSchema = JsonDocument.Parse(""" + { + "type": "object", + "properties": { + "Value": { + "type": ["string", "null"], + "maxLength": 24, + "minLength": 10 + } + } + } + """).RootElement; + + JsonElement actualSchema = AIJsonUtilities.CreateJsonSchema(typeof(ClassWithNullableMaxLengthProperty), serializerOptions: JsonContext.Default.Options); + AssertDeepEquals(expectedSchema, actualSchema); + } + + public class ClassWithNullableMaxLengthProperty + { + [MinLength(10)] + [MaxLength(24)] + public string? Value { get; set; } + } + [Fact] public static void AddAIContentType_DerivedAIContent() { @@ -1362,6 +1389,7 @@ private class DerivedAIContent : AIContent [JsonSerializable(typeof(MyPoco))] [JsonSerializable(typeof(MyEnumValue?))] [JsonSerializable(typeof(object[]))] + [JsonSerializable(typeof(ClassWithNullableMaxLengthProperty))] private partial class JsonContext : JsonSerializerContext; private static bool DeepEquals(JsonElement element1, JsonElement element2) From fdcaa2f7c05ade3243615b1be0f0038e952a3bee Mon Sep 17 00:00:00 2001 From: Kellyyyy Date: Wed, 17 Sep 2025 15:46:59 +0800 Subject: [PATCH 306/472] Support keyed HybridCache with keyed DistributedCaches and named options (#6694) --- .../HybridCacheOptions.cs | 5 + .../HybridCacheServiceExtensions.cs | 94 ++++- .../Internal/DefaultHybridCache.cs | 14 +- .../Microsoft.Extensions.Caching.Hybrid.json | Bin 8282 -> 5722 bytes .../BasicConfig.json | 12 - ...oft.Extensions.Caching.Hybrid.Tests.csproj | 6 - .../ServiceConstructionTests.cs | 371 +++++++++++++++++- 7 files changed, 468 insertions(+), 34 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/BasicConfig.json diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs index 7b87755da7b..d55ac1a4ea1 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs @@ -57,4 +57,9 @@ public class HybridCacheOptions /// should not be visible in metrics systems. /// public bool ReportTagMetrics { get; set; } + + /// + /// Gets or sets the key used to resolve the distributed cache service from the . + /// + public object? DistributedCacheServiceKey { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs index d28dc4e47d5..060307026d6 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -17,28 +18,111 @@ public static class HybridCacheServiceExtensions /// /// Adds support for multi-tier caching services. /// - /// A builder instance that allows further configuration of the system. + /// The to add the service to. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services, Action setupAction) { _ = Throw.IfNull(setupAction); - _ = AddHybridCache(services); + + var builder = AddHybridCache(services); _ = services.Configure(setupAction); - return new HybridCacheBuilder(services); + + return builder; } /// /// Adds support for multi-tier caching services. /// - /// A builder instance that allows further configuration of the system. + /// The to add the service to. + /// A builder instance that allows further configuration of the service. public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services) { _ = Throw.IfNull(services); + var builder = PrepareServices(services); + + services.TryAddSingleton(); + + return builder; + } + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, Action setupAction) => + AddKeyedHybridCache(services, serviceKey, serviceKey?.ToString() ?? Options.Options.DefaultName, setupAction); + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// The named options name to use for the instance. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, string optionsName, Action setupAction) + { + _ = Throw.IfNull(setupAction); + + var builder = AddKeyedHybridCache(services, serviceKey, optionsName); + _ = services.AddOptions(optionsName).Configure(setupAction); + + return builder; + } + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey) => + AddKeyedHybridCache(services, serviceKey, serviceKey?.ToString() ?? Options.Options.DefaultName); + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// The named options name to use for the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, string optionsName) + { + _ = Throw.IfNull(optionsName); + + var builder = PrepareServices(services); + _ = services.AddOptions(optionsName); + + _ = services.AddKeyedSingleton(serviceKey, (sp, key) => + { + var optionsService = sp.GetRequiredService>(); + var options = optionsService.Get(optionsName); + + return new DefaultHybridCache(options, sp); + }); + + return builder; + } + + /// + /// Adds the services required for hybrid caching. + /// + /// The to prepare with prerequisites. + /// A builder instance that allows further configuration of the service. + private static HybridCacheBuilder PrepareServices(IServiceCollection services) + { + _ = Throw.IfNull(services); + services.TryAddSingleton(TimeProvider.System); _ = services.AddOptions().AddMemoryCache(); services.TryAddSingleton(); services.TryAddSingleton>(InbuiltTypeSerializer.Instance); services.TryAddSingleton>(InbuiltTypeSerializer.Instance); - services.TryAddSingleton(); + return new HybridCacheBuilder(services); } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs index 84de2fe52e8..93e1e5457cb 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs @@ -65,13 +65,23 @@ internal enum CacheFeatures internal bool HasBackendCache => (_features & CacheFeatures.BackendCache) != 0; public DefaultHybridCache(IOptions options, IServiceProvider services) + : this(Throw.IfNull(options).Value, services) + { + } + + public DefaultHybridCache(HybridCacheOptions options, IServiceProvider services) { _services = Throw.IfNull(services); _localCache = services.GetRequiredService(); - _options = options.Value; + _options = options; _logger = services.GetService()?.CreateLogger(typeof(HybridCache)) ?? NullLogger.Instance; _clock = services.GetService() ?? TimeProvider.System; - _backendCache = services.GetService(); // note optional + + // The backend cache service is optional; if not provided, we operate as a pure L1 cache. + // If a service key is provided, the service must be present. + _backendCache = _options.DistributedCacheServiceKey is null + ? services.GetService() + : services.GetRequiredKeyedService(_options.DistributedCacheServiceKey); // ignore L2 if it is really just the same L1, wrapped // (note not just an "is" test; if someone has a custom subclass, who knows what it does?) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json index 2c1a811b223e6ab9931cc251f1f7e4a25b6d283f..10be31168ba9928b7a3ee7dc2681278225f2829a 100644 GIT binary patch literal 5722 zcmeHL-A>yu7`^8ytX?rGikLdk7=;N6)2gi-TC3emTsX2sLIAa$od^bJCj>}vluP^7|&_{gVNN>q#M5@KKhFhA|FpWrKv$zn8!;UXb40C6{v ztXtDpYoKtLjaUEhDBL~hgBtVT4pkLd_GTX<4@3oDEwcNJxxJjZDyq$l$UP5&0SJOA z`2(UeJlj`evVg4yn7o`Zryksr2JT@%vPAUiq@=c%cRS|@O-)7QpP~@57oIoW4jT5N zm_h{16f0+&;eV%D`+$ALMf&80YH^@Zj^}^iyz|7>YM1y;_H9>uvDz~?FtWNt?`H(4 zBC20wv-)HyHX9N6CdxjKq3S>u)_(KiDkv)CuyrkX3__8LQyO6`Xqyqq2SUDLN~9L# z`$nQ)Gu}%WVeS9bw$KEHpiZ(sm za?eDO)!8Ic3F0LRf-Ej*XZ=ll&{1kjcnMRha@xnYBuXNU?K7}Yjkko5au%)$@fxR; zl zE~hl#l$bzl7O`~DAe9Ot_dMIIbRHG{pmi}=hQ(=9d0I`Nq= z^yZ{C3Pw5ygV2Wo&?suw7+|^0S-kn&z-F#t;#kqdT`Ov30&To`yU(${yV;xm1>c8F AUH||9 literal 8282 zcmeHMTaVH}82z42{0|LJYhoJB>PFp*S=lvi6eTY4#rVLbi&dZ-3TDyp*VS`o7)sk= zxwQx@A*FL|XU?4YzB6C{{Qf2P<(@eB7395Cr7a#(FI2uHzSN~FfrPlOq$jSN%MI?H zbWrA_ly_H(=vkMqDCtNO`8P6<8hZNDkcW8IC-mr!dmPJiw0ShX*rF(XY2nFzwDaT` z&+(A%Ay-8?Ta=NnAi~#I%vNyk5PMt`!jhe2k6y6`aqtbejgqv&9){I6qF7r1{z zuCAUuq&;~BJbb*DeY@6mnVfSplS?)7CVH8<=n`VfvIwWBPYE?WtZ#s~*)cP1 z)7Y7s0+8{*uNwZl<9Ek#oF6-bgN<=6P@H6-X`94slh(?WQ()VWONEXPHj(K+RqhB( zKaoAP0{(&=Gjm@Jr&^3_+>Lp!)N~viAV3DiF?t|}R_;dZo5g!$9ZV12hnC&I@*T?~ z`)(MP++B80aXZV&tO|B-8+MV_h~B89>=i97t>Gr^gB|Clt^FeQA#X2un`M0sJ4;U+ zz@7|t7FObTIM7p*`0V(8xA;z7@xR;Jq}TrvI|(HHf7#MuG8#l z_5-}6@rzXDHswA`_1OC4r}nc_jq~6!?c5YcZBO#DYH4QKur9;%1nLynRckrzOlnkV znt`Uqe!j{V@pGll=sV-6zRd81FyC5JMbKO6D8jUHQMo&aQ1CQ+h!jr}IyJQJp9tGo z`zX2E?3uJ4=4p$rVGRrkpobSxRjb<*SI(fhLtv^S14WF1x}5PDMmn?}-%dnnWqWJK zvji+}fLj|ZZI5DD?|>#_W{j?ECzdVnf-_h*JL71Uf$ac$B*49a*w_GwlBe5=>A%|X zAA#95Fr+W40Li9`*)a;sIOqD&Oj^~|)mc2=xSUp7?Zm1hTR#+{-Y#y7ooc=mY+l@U z_F9?ff?cI77qgw0Ul*fjvEdRubW6p}qOS)gT58&aI3+mfH^6SDg>2*d!s*1tG2BrA=*X - - - PreserveNewest - - - diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs index d66b325e802..6aabe04f693 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs @@ -6,13 +6,15 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Hybrid.Internal; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.SqlServer; +using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; #if NET9_0_OR_GREATER using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Json; #endif #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -51,12 +53,228 @@ public void CanCreateServiceWithManualOptions() Assert.Null(defaults.LocalCacheExpiration); // wasn't specified } + [Fact] + public void CanCreateServiceWithKeyedDistributedCache() + { + var services = new ServiceCollection(); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache1)); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybrid = Assert.IsType(provider.GetRequiredService()); + var hybridOptions = hybrid.Options; + + var backend = Assert.IsType(hybrid.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache1), hybridOptions.DistributedCacheServiceKey); + Assert.Same(backend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + } + + [Fact] + public void ThrowsWhenDistributedCacheKeyNotRegistered() + { + var services = new ServiceCollection(); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.Throws(provider.GetRequiredService); + } + + [Fact] + public void ThrowsWhenRegisteredDistributedCacheIsNotKeyed() + { + var services = new ServiceCollection(); + services.AddDistributedMemoryCache(); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.Throws(provider.GetRequiredService); + } + + [Fact] + public void CanCreateKeyedHybridCacheServiceWithNullKey() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(null); + using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolves using null key registration + Assert.IsType(provider.GetRequiredKeyedService(null)); + + // Resolves as the non-keyed registration + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithStringKeys() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache("one"); + services.AddKeyedHybridCache("two"); + services.AddHybridCache(); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.IsType(provider.GetRequiredKeyedService("one")); + Assert.IsType(provider.GetRequiredKeyedService("two")); + Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithStringKeysAndSetupActions() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache("one", options => options.MaximumKeyLength = 1); + services.AddKeyedHybridCache("two", options => options.MaximumKeyLength = 2); + services.AddKeyedHybridCache(null, options => options.MaximumKeyLength = 3); + using ServiceProvider provider = services.BuildServiceProvider(); + + var one = Assert.IsType(provider.GetRequiredKeyedService("one")); + Assert.Equal(1, one.Options.MaximumKeyLength); + + var two = Assert.IsType(provider.GetRequiredKeyedService("two")); + Assert.Equal(2, two.Options.MaximumKeyLength); + + var threeKeyed = Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.Equal(3, threeKeyed.Options.MaximumKeyLength); + + var threeUnkeyed = Assert.IsType(provider.GetRequiredService()); + Assert.Equal(3, threeUnkeyed.Options.MaximumKeyLength); + } + + [Fact] + public void CanCreateKeyedServicesWithTypeKeys() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(typeof(string)); + services.AddKeyedHybridCache(typeof(int)); + services.AddHybridCache(); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.IsType(provider.GetRequiredKeyedService(typeof(string))); + Assert.IsType(provider.GetRequiredKeyedService(typeof(int))); + Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithTypeKeysAndSetupActions() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(typeof(string), options => options.MaximumKeyLength = 1); + services.AddKeyedHybridCache(typeof(int), options => options.MaximumKeyLength = 2); + services.AddKeyedHybridCache(null, options => options.MaximumKeyLength = 3); + + using ServiceProvider provider = services.BuildServiceProvider(); + var one = Assert.IsType(provider.GetRequiredKeyedService(typeof(string))); + Assert.Equal(1, one.Options.MaximumKeyLength); + + var two = Assert.IsType(provider.GetRequiredKeyedService(typeof(int))); + Assert.Equal(2, two.Options.MaximumKeyLength); + + var threeKeyed = Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.Equal(3, threeKeyed.Options.MaximumKeyLength); + + var threeUnkeyed = Assert.IsType(provider.GetRequiredService()); + Assert.Equal(3, threeUnkeyed.Options.MaximumKeyLength); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches() + { + var services = new ServiceCollection(); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache1)); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache2)); + + services.AddKeyedHybridCache("one", options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + services.AddKeyedHybridCache("two", options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache2)); + using ServiceProvider provider = services.BuildServiceProvider(); + + var cacheOne = Assert.IsType(provider.GetRequiredKeyedService("one")); + var cacheOneOptions = cacheOne.Options; + var cacheOneBackend = Assert.IsType(cacheOne.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache1), cacheOneOptions.DistributedCacheServiceKey); + Assert.Same(cacheOneBackend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + + var cacheTwo = Assert.IsType(provider.GetRequiredKeyedService("two")); + var cacheTwoOptions = cacheTwo.Options; + var cacheTwoBackend = Assert.IsType(cacheTwo.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache2), cacheTwoOptions.DistributedCacheServiceKey); + Assert.Same(cacheTwoBackend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + } + + [Fact] + public async Task KeyedHybridCaches_ShareLocalMemoryCache() + { + var services = new ServiceCollection(); + services.AddMemoryCache(options => options.SizeLimit = 2); + services.AddSingleton(); + services.AddKeyedHybridCache("hybrid1"); + services.AddKeyedHybridCache("hybrid2"); + services.AddKeyedHybridCache("hybrid3"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybrid1 = provider.GetRequiredKeyedService("hybrid1"); + var hybrid2 = provider.GetRequiredKeyedService("hybrid2"); + var hybrid3 = provider.GetRequiredKeyedService("hybrid3"); + + await hybrid1.SetAsync("entry1", 1); + await hybrid2.SetAsync("entry2", 2); + await hybrid3.SetAsync("entry3", 3); + + var localCache = provider.GetRequiredService(); + Assert.True(localCache.TryGetValue("entry1", out object? _)); + Assert.True(localCache.TryGetValue("entry2", out object? _)); + + // The third item fails to be cached locally because of the shared local cache size limit + Assert.False(localCache.TryGetValue("entry3", out object? _)); + + // But we can still get it from the hybrid cache (which gets it from the distributed cache) + var actual3 = await hybrid3.GetOrCreateAsync("entry3", ct => + { + Assert.Fail("Should not be called as the item should be found in the distributed cache"); + return new ValueTask(-1); + }); + + Assert.Equal(3, actual3); + } + + [Fact] + public void CanCreateRedisAndSqlServerBackedHybridCaches() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("Redis"); + + services.AddKeyedSingleton("SqlServer", + (sp, key) => new SqlServerCache(new SqlServerCacheOptions + { + ConnectionString = "test", + SchemaName = "test", + TableName = "test" + })); + + services.AddKeyedHybridCache("HybridWithRedis", options => options.DistributedCacheServiceKey = "Redis"); + services.AddKeyedHybridCache("HybridWithSqlServer", options => options.DistributedCacheServiceKey = "SqlServer"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridWithRedis = Assert.IsType(provider.GetRequiredKeyedService("HybridWithRedis")); + var hybridWithRedisBackend = Assert.IsType(hybridWithRedis.BackendCache); + Assert.Same(hybridWithRedisBackend, provider.GetRequiredKeyedService("Redis")); + + var hybridWithSqlServer = Assert.IsType(provider.GetRequiredKeyedService("HybridWithSqlServer")); + var hybridWithSqlServerBackend = Assert.IsType(hybridWithSqlServer.BackendCache); + Assert.Same(hybridWithSqlServerBackend, provider.GetRequiredKeyedService("SqlServer")); + } + #if NET9_0_OR_GREATER // for Bind API [Fact] public void CanParseOptions_NoEntryOptions() { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("no_entry_options:MaximumKeyLength", "937") + ]); + var config = configBuilder.Build(); var options = new HybridCacheOptions(); ConfigurationBinder.Bind(config, "no_entry_options", options); @@ -68,8 +286,14 @@ public void CanParseOptions_NoEntryOptions() [Fact] public void CanParseOptions_WithEntryOptions() // in particular, check we can parse the timespan and [Flags] enums { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("with_entry_options:MaximumKeyLength", "937"), + new("with_entry_options:DefaultEntryOptions:Flags", "DisableCompression, DisableLocalCacheRead"), + new("with_entry_options:DefaultEntryOptions:LocalCacheExpiration", "00:02:00") + ]); + var config = configBuilder.Build(); var options = new HybridCacheOptions(); ConfigurationBinder.Bind(config, "with_entry_options", options); @@ -81,6 +305,122 @@ public void CanParseOptions_WithEntryOptions() // in particular, check we can pa Assert.Equal(TimeSpan.FromSeconds(120), defaults.LocalCacheExpiration); Assert.Null(defaults.Expiration); // wasn't specified } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingNamedOptions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddOptions("HybridOne").Configure(options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddOptions("HybridTwo").Configure(options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache1), "HybridOne"); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache2), "HybridTwo"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingSetupActions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddKeyedHybridCache("HybridOne", options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddKeyedHybridCache("HybridTwo", options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService("HybridOne")); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService("HybridTwo")); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService("HybridOne"); + provider.GetRequiredKeyedService("HybridOne"); + provider.GetRequiredKeyedService("HybridOne"); + + provider.GetRequiredKeyedService("HybridTwo"); + provider.GetRequiredKeyedService("HybridTwo"); + provider.GetRequiredKeyedService("HybridTwo"); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingNamedOptionsAndSetupActions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache1), "HybridOne", options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache2), "HybridTwo", options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + } #endif [Fact] @@ -173,7 +513,7 @@ public void DefaultMemoryDistributedCacheIsIgnored(bool manual) public void SubclassMemoryDistributedCacheIsNotIgnored() { var services = new ServiceCollection(); - services.AddSingleton(); + services.AddSingleton(); services.AddHybridCache(); using ServiceProvider provider = services.BuildServiceProvider(); var cache = Assert.IsType(provider.GetRequiredService()); @@ -293,14 +633,27 @@ public CustomMemoryCache(IOptions options, ILoggerFactory lo } } - internal class CustomMemoryDistributedCache : MemoryDistributedCache + internal class CustomMemoryDistributedCache1 : MemoryDistributedCache + { + public CustomMemoryDistributedCache1(IOptions options) + : base(options) + { + } + + public CustomMemoryDistributedCache1(IOptions options, ILoggerFactory loggerFactory) + : base(options, loggerFactory) + { + } + } + + internal class CustomMemoryDistributedCache2 : MemoryDistributedCache { - public CustomMemoryDistributedCache(IOptions options) + public CustomMemoryDistributedCache2(IOptions options) : base(options) { } - public CustomMemoryDistributedCache(IOptions options, ILoggerFactory loggerFactory) + public CustomMemoryDistributedCache2(IOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { } From cdb9ce445cf26ea3969284f0f9e53fdb2f7faab8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 17 Sep 2025 09:56:22 -0400 Subject: [PATCH 307/472] Add `IList.Add(ResponseTool)` extension method (#6813) For someone that knows they're using an OpenAI Responses IChatClient, they can Add a ResponseTool directly into ChatOptions.Tools, rather than needing to go through RawRepresentationFactory. --- .../CHANGELOG.md | 1 + .../CHANGELOG.md | 3 +- ...icrosoftExtensionsAIResponsesExtensions.cs | 42 +++++++++++++++++++ .../OpenAIResponsesChatClient.cs | 11 +++++ .../OpenAIConversionTests.cs | 27 +++++++++++- .../OpenAIResponseClientTests.cs | 19 +++++---- 6 files changed, 94 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index cc911fd64bb..526072374c4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -4,6 +4,7 @@ - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. - Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content. +- Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 4d858edeb89..a50aea8dea3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -2,7 +2,8 @@ ## NOT YET RELEASED -- Added M.E.AI to OpenAI conversions for response format types +- Added M.E.AI to OpenAI conversions for response format types. +- Added `ResponseTool` to `AITool` conversions. ## 9.9.0-preview.1.25458.4 diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index ca4bfd3f736..c632f3c45c7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -93,4 +93,46 @@ public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOp previousResponseId: options?.ConversationId, instructions: options?.Instructions); } + + /// Adds the to the list of s. + /// The list of s to which the provided tool should be added. + /// The to add. + /// + /// does not derive from , so it cannot be added directly to a list of s. + /// Instead, this method wraps the provided in an and adds that to the list. + /// The returned by will + /// be able to unwrap the when it processes the list of tools and use the provided as-is. + /// + public static void Add(this IList tools, ResponseTool tool) + { + _ = Throw.IfNull(tools); + + tools.Add(AsAITool(tool)); + } + + /// Creates an to represent a raw . + /// The tool to wrap as an . + /// The wrapped as an . + /// + /// + /// The returned tool is only suitable for use with the returned by + /// (or s that delegate + /// to such an instance). It is likely to be ignored by any other implementation. + /// + /// + /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, + /// such as , , , or + /// , those types should be preferred instead of this method, as they are more portable, + /// capable of being respected by any implementation. This method does not attempt to + /// map the supplied to any of those types, it simply wraps it as-is: + /// the returned by will + /// be able to unwrap the when it processes the list of tools. + /// + /// + public static AITool AsAITool(this ResponseTool tool) + { + _ = Throw.IfNull(tool); + + return new OpenAIResponsesChatClient.ResponseToolAITool(tool); + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 36bf4ca68b4..e2fbfdf3f84 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -414,6 +414,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt { switch (tool) { + case ResponseToolAITool rtat: + result.Tools.Add(rtat.Tool); + break; + case AIFunctionDeclaration aiFunction: result.Tools.Add(ToResponseTool(aiFunction, options)); break; @@ -877,4 +881,11 @@ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filt filter.ToolNames.Add(toolName); } } + + /// Provides an wrapper for a . + internal sealed class ResponseToolAITool(ResponseTool tool) : AITool + { + public ResponseTool Tool => tool; + public override string Name => Tool.GetType().Name; + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index b919592da4d..7724ad98360 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -316,7 +316,7 @@ public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates() updates.Add(update); } - ChatResponse response = updates.ToChatResponse(); + var response = updates.ToChatResponse(); Assert.Equal("id", response.ResponseId); Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason); @@ -1176,6 +1176,31 @@ public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId() Assert.Equal("response-model-id", openAIResponse.Model); } + [Fact] + public void ListAddResponseTool_AddsToolCorrectly() + { + Assert.Throws("tools", () => ((IList)null!).Add(ResponseTool.CreateWebSearchTool())); + Assert.Throws("tool", () => new List().Add((ResponseTool)null!)); + + Assert.Throws("tool", () => ((ResponseTool)null!).AsAITool()); + + ChatOptions options; + + options = new() + { + Tools = new List { ResponseTool.CreateWebSearchTool() }, + }; + Assert.Single(options.Tools); + Assert.NotNull(options.Tools[0]); + + options = new() + { + Tools = [ResponseTool.CreateWebSearchTool().AsAITool()], + }; + Assert.Single(options.Tools); + Assert.NotNull(options.Tools[0]); + } + private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source) { foreach (var item in source) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 9175b6afd57..866e43e172d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -825,8 +825,10 @@ public async Task MultipleOutputItems_NonStreaming() Assert.Equal(36, response.Usage.TotalTokenCount); } - [Fact] - public async Task McpToolCall_ApprovalNotRequired_NonStreaming() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) { const string Input = """ { @@ -1031,13 +1033,16 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming() using HttpClient httpClient = new(handler); using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + AITool mcpTool = rawTool ? + ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() : + new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + }; + ChatOptions chatOptions = new() { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") - { - ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, - } - ], + Tools = [mcpTool], }; var response = await client.GetResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions); From e5ad845807aa7ad571f6f4bbac822e11651d18fe Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 18 Sep 2025 05:32:44 -0400 Subject: [PATCH 308/472] Use RequestOptions from chat/responses clients (#6819) --- .../OpenAIChatClient.cs | 33 ++++++++- .../OpenAIResponsesChatClient.cs | 27 ++++++- .../RequestOptionsExtensions.cs | 74 +++++++++++++++++++ 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 67037001071..fb0ee48a236 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -25,6 +28,27 @@ namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . internal sealed class OpenAIChatClient : IChatClient { + // These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept + // a RequestOptions. These should be replaced once a better way to pass RequestOptions is available. + private static readonly Func, ChatCompletionOptions, RequestOptions, Task>>? + _completeChatAsync = + (Func, ChatCompletionOptions, RequestOptions, Task>>?) + typeof(ChatClient) + .GetMethod( + nameof(ChatClient.CompleteChatAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, [typeof(IEnumerable), typeof(ChatCompletionOptions), typeof(RequestOptions)], null) + ?.CreateDelegate( + typeof(Func, ChatCompletionOptions, RequestOptions, Task>>)); + private static readonly Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>? + _completeChatStreamingAsync = + (Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>?) + typeof(ChatClient) + .GetMethod( + nameof(ChatClient.CompleteChatStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, [typeof(IEnumerable), typeof(ChatCompletionOptions), typeof(RequestOptions)], null) + ?.CreateDelegate( + typeof(Func, ChatCompletionOptions, RequestOptions, AsyncCollectionResult>)); + /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -64,7 +88,10 @@ public async Task GetResponseAsync( var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. - var response = await _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken).ConfigureAwait(false); + var task = _completeChatAsync is not null ? + _completeChatAsync(_chatClient, openAIChatMessages, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) : + _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken); + var response = await task.ConfigureAwait(false); return FromOpenAIChatCompletion(response.Value, openAIOptions); } @@ -79,7 +106,9 @@ public IAsyncEnumerable GetStreamingResponseAsync( var openAIOptions = ToOpenAIOptions(options); // Make the call to OpenAI. - var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken); + var chatCompletionUpdates = _completeChatStreamingAsync is not null ? + _completeChatStreamingAsync(_chatClient, openAIChatMessages, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) : + _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken); return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index e2fbfdf3f84..fc9f84ed228 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -32,6 +33,23 @@ internal sealed class OpenAIResponsesChatClient : IChatClient private static readonly Type? _internalResponseReasoningSummaryTextDeltaEventType = Type.GetType("OpenAI.Responses.InternalResponseReasoningSummaryTextDeltaEvent, OpenAI"); private static readonly PropertyInfo? _summaryTextDeltaProperty = _internalResponseReasoningSummaryTextDeltaEventType?.GetProperty("Delta"); + // These delegate instances are used to call the internal overloads of CreateResponseAsync and CreateResponseStreamingAsync that accept + // a RequestOptions. These should be replaced once a better way to pass RequestOptions is available. + private static readonly Func, ResponseCreationOptions, RequestOptions, Task>>? + _createResponseAsync = + (Func, ResponseCreationOptions, RequestOptions, Task>>?) + typeof(OpenAIResponseClient).GetMethod( + nameof(OpenAIResponseClient.CreateResponseAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null) + ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, Task>>)); + private static readonly Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>? + _createResponseStreamingAsync = + (Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>?) + typeof(OpenAIResponseClient).GetMethod( + nameof(OpenAIResponseClient.CreateResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null) + ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>)); + /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -79,7 +97,10 @@ public async Task GetResponseAsync( var openAIOptions = ToOpenAIResponseCreationOptions(options); // Make the call to the OpenAIResponseClient. - var openAIResponse = (await _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + var task = _createResponseAsync is not null ? + _createResponseAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) : + _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken); + var openAIResponse = (await task.ConfigureAwait(false)).Value; // Convert the response to a ChatResponse. return FromOpenAIResponse(openAIResponse, openAIOptions); @@ -208,7 +229,9 @@ public IAsyncEnumerable GetStreamingResponseAsync( var openAIResponseItems = ToOpenAIResponseItems(messages, options); var openAIOptions = ToOpenAIResponseCreationOptions(options); - var streamingUpdates = _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken); + var streamingUpdates = _createResponseStreamingAsync is not null ? + _createResponseStreamingAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) : + _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken); return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs new file mode 100644 index 00000000000..dbe38e42a2c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/RequestOptionsExtensions.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CA1307 // Specify StringComparison + +namespace Microsoft.Extensions.AI; + +/// Provides utility methods for creating . +internal static class RequestOptionsExtensions +{ + /// Creates a configured for use with OpenAI. + public static RequestOptions ToRequestOptions(this CancellationToken cancellationToken, bool streaming) + { + RequestOptions requestOptions = new() + { + CancellationToken = cancellationToken, + BufferResponse = !streaming + }; + + requestOptions.AddPolicy(MeaiUserAgentPolicy.Instance, PipelinePosition.PerCall); + + return requestOptions; + } + + /// Provides a pipeline policy that adds a "MEAI/x.y.z" user-agent header. + private sealed class MeaiUserAgentPolicy : PipelinePolicy + { + public static MeaiUserAgentPolicy Instance { get; } = new MeaiUserAgentPolicy(); + + private static readonly string _userAgentValue = CreateUserAgentValue(); + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + AddUserAgentHeader(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + AddUserAgentHeader(message); + return ProcessNextAsync(message, pipeline, currentIndex); + } + + private static void AddUserAgentHeader(PipelineMessage message) => + message.Request.Headers.Add("User-Agent", _userAgentValue); + + private static string CreateUserAgentValue() + { + const string Name = "MEAI"; + + if (typeof(MeaiUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version) + { + int pos = version.IndexOf('+'); + if (pos >= 0) + { + version = version.Substring(0, pos); + } + + if (version.Length > 0) + { + return $"{Name}/{version}"; + } + } + + return Name; + } + } +} From df41bfaa1bed75915ca87c2e27e1b40dc1aa8ccb Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 18 Sep 2025 06:40:41 -0400 Subject: [PATCH 309/472] Fix HostedCodeInterpreterTool with Responses (#6817) When no file IDs are present, we're outputting the wrong JSON. --- .../OpenAIResponsesChatClient.cs | 2 +- .../OpenAIResponseClientIntegrationTests.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index fc9f84ed228..d094f5f8581 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -479,7 +479,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt } else { - json = """{"type":"code_interpreter","container":"auto"}"""; + json = """{"type":"code_interpreter","container":{"type":"auto"}}"""; } result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json))); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index a46a06bb770..c8bdc819ddb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -21,6 +21,22 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests // Test structure doesn't make sense with Responses. public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; + [ConditionalFact] + public async Task UseCodeInterpreter_ProducesCodeExecutionResults() + { + SkipIfNotEnabled(); + + var response = await ChatClient.GetResponseAsync("Use the code interpreter to calculate the square root of 42. Return only the nearest integer value and no other text.", new() + { + Tools = [new HostedCodeInterpreterTool()], + }); + Assert.NotNull(response); + + ChatMessage message = Assert.Single(response.Messages); + + Assert.Equal("6", message.Text); + } + [ConditionalFact] public async Task UseWebSearch_AnnotationsReflectResults() { From 53ef1158f9f42632e111d6873a8cd72b803b4ae6 Mon Sep 17 00:00:00 2001 From: William Godbe Date: Thu, 18 Sep 2025 11:36:10 -0700 Subject: [PATCH 310/472] Inventory drift (#6820) Co-authored-by: GitOps --- es-metadata.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 es-metadata.yml diff --git a/es-metadata.yml b/es-metadata.yml new file mode 100644 index 00000000000..9061e11812c --- /dev/null +++ b/es-metadata.yml @@ -0,0 +1,8 @@ +schemaVersion: 0.0.1 +isProduction: true +accountableOwners: + service: 4db45fa9-fb0f-43ce-b523-ad1da773dfbc +routing: + defaultAreaPath: + org: devdiv + path: DevDiv\ASP.NET Core From 6e61568b4a11a5de181a0f58f0eea8a4bad8283a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 22 Sep 2025 08:57:09 -0400 Subject: [PATCH 311/472] Update genai otel implementation with recent additions (#6829) * Add gen_ai.embeddings.dimension.count tag * Add gen_ai.tool.call.arguments/result tags * Add gen_ai.tool.definitions tag --- .../Functions/AIFunctionDeclaration.cs | 4 +- .../FunctionInvokingChatClient.cs | 50 +++++++++++++------ .../ChatCompletion/OpenTelemetryChatClient.cs | 40 ++++++++++++--- .../OpenTelemetryImageGenerator.cs | 6 +-- .../OpenTelemetryEmbeddingGenerator.cs | 8 +-- .../OpenTelemetryConsts.cs | 20 ++++++-- .../EmbeddingGeneratorIntegrationTests.cs | 2 +- .../FunctionInvokingChatClientTests.cs | 26 ++++++++-- .../OpenTelemetryChatClientTests.cs | 44 ++++++++++++++++ .../OpenTelemetryEmbeddingGeneratorTests.cs | 2 +- 10 files changed, 158 insertions(+), 44 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs index 911f165f97b..e61deb65b82 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs @@ -30,12 +30,10 @@ protected AIFunctionDeclaration() /// /// /// { - /// "title" : "addNumbers", - /// "description": "A simple function that adds two numbers together.", /// "type": "object", /// "properties": { /// "a" : { "type": "number" }, - /// "b" : { "type": "number", "default": 1 } + /// "b" : { "type": ["number","null"], "default": 1 } /// }, /// "required" : ["a"] /// } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 2c01433563c..f98d02278f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1113,31 +1113,43 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul _ = Throw.IfNull(context); using Activity? activity = _activitySource?.StartActivity( - $"{OpenTelemetryConsts.GenAI.ExecuteTool} {context.Function.Name}", + $"{OpenTelemetryConsts.GenAI.ExecuteToolName} {context.Function.Name}", ActivityKind.Internal, default(ActivityContext), [ - new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ExecuteTool), + new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ExecuteToolName), new(OpenTelemetryConsts.GenAI.Tool.Type, OpenTelemetryConsts.ToolTypeFunction), new(OpenTelemetryConsts.GenAI.Tool.Call.Id, context.CallContent.CallId), new(OpenTelemetryConsts.GenAI.Tool.Name, context.Function.Name), new(OpenTelemetryConsts.GenAI.Tool.Description, context.Function.Description), ]); - long startingTimestamp = 0; - if (_logger.IsEnabled(LogLevel.Debug)) + long startingTimestamp = Stopwatch.GetTimestamp(); + + bool enableSensitiveData = activity is { IsAllDataRequested: true } && InnerClient.GetService()?.EnableSensitiveData is true; + bool traceLoggingEnabled = _logger.IsEnabled(LogLevel.Trace); + bool loggedInvoke = false; + if (enableSensitiveData || traceLoggingEnabled) { - startingTimestamp = Stopwatch.GetTimestamp(); - if (_logger.IsEnabled(LogLevel.Trace)) + string functionArguments = TelemetryHelpers.AsJson(context.Arguments, context.Function.JsonSerializerOptions); + + if (enableSensitiveData) { - LogInvokingSensitive(context.Function.Name, TelemetryHelpers.AsJson(context.Arguments, context.Function.JsonSerializerOptions)); + _ = activity?.SetTag(OpenTelemetryConsts.GenAI.Tool.Call.Arguments, functionArguments); } - else + + if (traceLoggingEnabled) { - LogInvoking(context.Function.Name); + LogInvokingSensitive(context.Function.Name, functionArguments); + loggedInvoke = true; } } + if (!loggedInvoke && _logger.IsEnabled(LogLevel.Debug)) + { + LogInvoking(context.Function.Name); + } + object? result = null; try { @@ -1165,19 +1177,27 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul } finally { - if (_logger.IsEnabled(LogLevel.Debug)) + bool loggedResult = false; + if (enableSensitiveData || traceLoggingEnabled) { - TimeSpan elapsed = GetElapsedTime(startingTimestamp); + string functionResult = TelemetryHelpers.AsJson(result, context.Function.JsonSerializerOptions); - if (result is not null && _logger.IsEnabled(LogLevel.Trace)) + if (enableSensitiveData) { - LogInvocationCompletedSensitive(context.Function.Name, elapsed, TelemetryHelpers.AsJson(result, context.Function.JsonSerializerOptions)); + _ = activity?.SetTag(OpenTelemetryConsts.GenAI.Tool.Call.Result, functionResult); } - else + + if (traceLoggingEnabled) { - LogInvocationCompleted(context.Function.Name, elapsed); + LogInvocationCompletedSensitive(context.Function.Name, GetElapsedTime(startingTimestamp), functionResult); + loggedResult = true; } } + + if (!loggedResult && _logger.IsEnabled(LogLevel.Debug)) + { + LogInvocationCompleted(context.Function.Name, GetElapsedTime(startingTimestamp)); + } } return result; diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 41cbc3a140f..137392bf1ce 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -318,13 +318,13 @@ internal static string SerializeChatMessages(IEnumerable messages, string? modelId = options?.ModelId ?? _defaultModelId; activity = _activitySource.StartActivity( - string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Chat : $"{OpenTelemetryConsts.GenAI.Chat} {modelId}", + string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.ChatName : $"{OpenTelemetryConsts.GenAI.ChatName} {modelId}", ActivityKind.Client); if (activity is { IsAllDataRequested: true }) { _ = activity - .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat) + .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ChatName) .AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId) .AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); @@ -395,13 +395,28 @@ internal static string SerializeChatMessages(IEnumerable messages, } } - // Log all additional request options as raw values on the span. - // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data. - if (EnableSensitiveData && options.AdditionalProperties is { } props) + if (EnableSensitiveData) { - foreach (KeyValuePair prop in props) + if (options.Tools?.Any(t => t is AIFunctionDeclaration) is true) { - _ = activity.AddTag(prop.Key, prop.Value); + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Tool.Definitions, + JsonSerializer.Serialize(options.Tools.OfType().Select(t => new OtelFunction + { + Name = t.Name, + Description = t.Description, + Parameters = t.JsonSchema, + }), OtelContext.Default.IEnumerableOtelFunction)); + } + + // Log all additional request options as raw values on the span. + // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data. + if (options.AdditionalProperties is { } props) + { + foreach (KeyValuePair prop in props) + { + _ = activity.AddTag(prop.Key, prop.Value); + } } } } @@ -505,7 +520,7 @@ private void TraceResponse( void AddMetricTags(ref TagList tags, string? requestModelId, ChatResponse? response) { - tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat); + tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ChatName); if (requestModelId is not null) { @@ -582,6 +597,14 @@ private sealed class OtelToolCallResponsePart public object? Response { get; set; } } + private sealed class OtelFunction + { + public string Type { get; set; } = "function"; + public string? Name { get; set; } + public string? Description { get; set; } + public JsonElement Parameters { get; set; } + } + private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions(); private static JsonSerializerOptions CreateDefaultOptions() @@ -606,5 +629,6 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(OtelGenericPart))] [JsonSerializable(typeof(OtelToolCallRequestPart))] [JsonSerializable(typeof(OtelToolCallResponsePart))] + [JsonSerializable(typeof(IEnumerable))] private sealed partial class OtelContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs index bf156a2ad69..28be6407d19 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs @@ -151,13 +151,13 @@ public async override Task GenerateAsync( string? modelId = options?.ModelId ?? _defaultModelId; activity = _activitySource.StartActivity( - string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContent : $"{OpenTelemetryConsts.GenAI.GenerateContent} {modelId}", + string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContentName : $"{OpenTelemetryConsts.GenAI.GenerateContentName} {modelId}", ActivityKind.Client); if (activity is { IsAllDataRequested: true }) { _ = activity - .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent) + .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName) .AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeImage) .AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId) .AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); @@ -294,7 +294,7 @@ private void TraceResponse( void AddMetricTags(ref TagList tags, string? requestModelId) { - tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent); + tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName); if (requestModelId is not null) { diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index e23f59e56c6..7859cb81f31 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -154,11 +154,11 @@ protected override void Dispose(bool disposing) string? modelId = options?.ModelId ?? _defaultModelId; activity = _activitySource.StartActivity( - string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embeddings : $"{OpenTelemetryConsts.GenAI.Embeddings} {modelId}", + string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.EmbeddingsName : $"{OpenTelemetryConsts.GenAI.EmbeddingsName} {modelId}", ActivityKind.Client, default(ActivityContext), [ - new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings), + new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.EmbeddingsName), new(OpenTelemetryConsts.GenAI.Request.Model, modelId), new(OpenTelemetryConsts.GenAI.Provider.Name, _providerName), ]); @@ -174,7 +174,7 @@ protected override void Dispose(bool disposing) if ((options?.Dimensions ?? _defaultModelDimensions) is int dimensionsValue) { - _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.EmbeddingDimensions, dimensionsValue); + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Embeddings.Dimension.Count, dimensionsValue); } // Log all additional request options as raw values on the span. @@ -265,7 +265,7 @@ private void TraceResponse( private void AddMetricTags(ref TagList tags, string? requestModelId, string? responseModelId) { - tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings); + tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.EmbeddingsName); if (requestModelId is not null) { diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index 48c32e2992d..f7344c329e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -33,10 +33,10 @@ public static class Error public static class GenAI { - public const string Chat = "chat"; - public const string Embeddings = "embeddings"; - public const string ExecuteTool = "execute_tool"; - public const string GenerateContent = "generate_content"; + public const string ChatName = "chat"; + public const string EmbeddingsName = "embeddings"; + public const string ExecuteToolName = "execute_tool"; + public const string GenerateContentName = "generate_content"; public const string SystemInstructions = "gen_ai.system_instructions"; @@ -62,6 +62,14 @@ public static class Conversation public const string Id = "gen_ai.conversation.id"; } + public static class Embeddings + { + public static class Dimension + { + public const string Count = "gen_ai.embeddings.dimension.count"; + } + } + public static class Input { public const string Messages = "gen_ai.input.messages"; @@ -86,7 +94,6 @@ public static class Provider public static class Request { public const string ChoiceCount = "gen_ai.request.choice.count"; - public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions"; public const string FrequencyPenalty = "gen_ai.request.frequency_penalty"; public const string Model = "gen_ai.request.model"; public const string MaxTokens = "gen_ai.request.max_tokens"; @@ -116,10 +123,13 @@ public static class Tool public const string Description = "gen_ai.tool.description"; public const string Message = "gen_ai.tool.message"; public const string Type = "gen_ai.tool.type"; + public const string Definitions = "gen_ai.tool.definitions"; public static class Call { public const string Id = "gen_ai.tool.call.id"; + public const string Arguments = "gen_ai.tool.call.arguments"; + public const string Result = "gen_ai.tool.call.result"; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs index 0f422d950b8..20423ae9e8b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/EmbeddingGeneratorIntegrationTests.cs @@ -124,7 +124,7 @@ public virtual async Task OpenTelemetry_CanEmitTracesAndMetrics() Assert.Single(activities); var activity = activities.Single(); Assert.StartsWith("embed", activity.DisplayName); - Assert.StartsWith("http", (string)activity.GetTagItem("server.address")!); + Assert.Contains(".", (string)activity.GetTagItem("server.address")!); Assert.Equal(embeddingGenerator.GetService()?.ProviderUri?.Port, (int)activity.GetTagItem("server.port")!); Assert.NotNull(activity.Id); Assert.NotEmpty(activity.Id); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 0657cb7edfa..7a3c09db438 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -655,9 +655,10 @@ async Task InvokeAsync(Func work) } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry, bool enableSensitiveData) { string sourceName = Guid.NewGuid().ToString(); @@ -675,7 +676,7 @@ public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry) }; Func configure = b => b.Use(c => - new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: sourceName))); + new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: sourceName) { EnableSensitiveData = enableSensitiveData })); await InvokeAsync(() => InvokeAndAssertAsync(options, plan, configurePipeline: configure), streaming: false); @@ -701,6 +702,23 @@ async Task InvokeAsync(Func work, bool streaming) activity => Assert.Equal("chat", activity.DisplayName), activity => Assert.Equal(streaming ? "FunctionInvokingChatClient.GetStreamingResponseAsync" : "FunctionInvokingChatClient.GetResponseAsync", activity.DisplayName)); + var executeTool = activities[1]; + if (enableSensitiveData) + { + var args = Assert.Single(executeTool.Tags, t => t.Key == "gen_ai.tool.call.arguments"); + Assert.Equal( + JsonSerializer.Serialize(new Dictionary { ["arg1"] = "value1" }, AIJsonUtilities.DefaultOptions), + args.Value); + + var result = Assert.Single(executeTool.Tags, t => t.Key == "gen_ai.tool.call.result"); + Assert.Equal("Result 1", JsonSerializer.Deserialize(result.Value!, AIJsonUtilities.DefaultOptions)); + } + else + { + Assert.DoesNotContain(executeTool.Tags, t => t.Key == "gen_ai.tool.call.arguments"); + Assert.DoesNotContain(executeTool.Tags, t => t.Key == "gen_ai.tool.call.result"); + } + for (int i = 0; i < activities.Count - 1; i++) { // Activities are exported in the order of completion, so all except the last are children of the last (i.e., outer) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index 4640b78f75d..eb052c3d3e2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -130,6 +130,12 @@ async static IAsyncEnumerable CallbackAsync( ["SomethingElse"] = "value2", }, Instructions = "You are helpful.", + Tools = + [ + AIFunctionFactory.Create((string personName) => personName, "GetPersonAge", "Gets the age of a person by name."), + new HostedWebSearchTool(), + AIFunctionFactory.Create((string location) => "", "GetCurrentWeather", "Gets the current weather for a location.").AsDeclarationOnly(), + ], }; if (streaming) @@ -263,12 +269,50 @@ async static IAsyncEnumerable CallbackAsync( } ] """), ReplaceWhitespace(tags["gen_ai.system_instructions"])); + + Assert.Equal(ReplaceWhitespace(""" + [ + { + "type": "function", + "name": "GetPersonAge", + "description": "Gets the age of a person by name.", + "parameters": { + "type": "object", + "properties": { + "personName": { + "type": "string" + } + }, + "required": [ + "personName" + ] + } + }, + { + "type": "function", + "name": "GetCurrentWeather", + "description": "Gets the current weather for a location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ] + } + } + ] + """), ReplaceWhitespace(tags["gen_ai.tool.definitions"])); } else { Assert.False(tags.ContainsKey("gen_ai.input.messages")); Assert.False(tags.ContainsKey("gen_ai.output.messages")); Assert.False(tags.ContainsKey("gen_ai.system_instructions")); + Assert.False(tags.ContainsKey("gen_ai.tool.definitions")); } static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs index c8419338e44..4fbc637a1a4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Embeddings/OpenTelemetryEmbeddingGeneratorTests.cs @@ -85,7 +85,7 @@ public async Task ExpectedInformationLogged_Async(string? perRequestModelId, boo Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name")); Assert.Equal(expectedModelName, activity.GetTagItem("gen_ai.request.model")); - Assert.Equal(1234, activity.GetTagItem("gen_ai.request.embedding.dimensions")); + Assert.Equal(1234, activity.GetTagItem("gen_ai.embeddings.dimension.count")); Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("service_tier")); Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("SomethingElse")); From 6fb8ab7a3f62e12ddd4c53beb75389d5aa5d19da Mon Sep 17 00:00:00 2001 From: Shyam N Date: Tue, 23 Sep 2025 05:50:40 -0700 Subject: [PATCH 312/472] Increase output token limit for EquivalenceEvaluator (#6835) EquivalenceEvaluator was specifying MaxOutputTokens = 1 since its prompt instructs the LLM to produce a response (score) that is a single digit (between 1 and 5). Turns out that while this works for most models (including the OpenAI models that were used to test the prompt), some models require more than one token for this. For example, looks like Claude requires two tokens for this - see https://github.com/dotnet/extensions/issues/6814). This PR bumps the MaxOutputTokens to 5 to address the above issue. Fixes #6814 --- .../EquivalenceEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs index 9d820aeffc0..e166f573573 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs @@ -52,7 +52,7 @@ public sealed class EquivalenceEvaluator : IEvaluator new ChatOptions { Temperature = 0.0f, - MaxOutputTokens = 1, + MaxOutputTokens = 5, // See https://github.com/dotnet/extensions/issues/6814. TopP = 1.0f, PresencePenalty = 0.0f, FrequencyPenalty = 0.0f, From e5b7f5d0614fb7380f9d7e826b00869e0873ac8b Mon Sep 17 00:00:00 2001 From: Sergei Smelov Date: Tue, 23 Sep 2025 17:36:10 +0200 Subject: [PATCH 313/472] Fix KeyNotFoundException on HttpRequestLatencyListener.OnEventWritten for uknown event sources (#6821) * Fix KeyNotFoundException on HttpRequestLatencyListener.OnEventWritten for unknown event sources * Update test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Darius Letterman Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Internal/HttpRequestLatencyListener.cs | 4 +++- .../Internal/HttpRequestLatencyListenerTest.cs | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs index 5d7238ed8a3..4179759df35 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Latency/Internal/HttpRequestLatencyListener.cs @@ -49,7 +49,9 @@ public void Enable() internal void OnEventWritten(string eventSourceName, string? eventName) { // If event of interest, add a checkpoint for it. - if (eventName != null && _eventToTokenMap[eventSourceName].TryGetValue(eventName, out var token)) + if (eventName != null + && _eventToTokenMap.TryGetValue(eventSourceName, out var tokenMap) + && tokenMap.TryGetValue(eventName, out var token)) { LatencyContext.Get()?.AddCheckpoint(token); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs index fa91daa1c3e..6b384eae928 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs @@ -191,4 +191,19 @@ public void HttpRequestLatencyListener_OnEventWritten_AddsCheckpoints_Http() lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Exactly(numHttpEvents + numSocketEvents + numDnsEvents)); } + + [Fact] + public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_UnknownEventSource() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + + listener.OnEventWritten("System.Runtime", "EventCounters"); + + lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Never); + } } From 7200baad21adec535bdf3cab530e52a103b1263d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as?= <50237907+ViveliDuCh@users.noreply.github.com> Date: Tue, 23 Sep 2025 23:55:39 -0700 Subject: [PATCH 314/472] Add project name normalization to match aspire's code generator logic (#6818) * Add project name normalization to match aspire's code generator for --aspire projects. Add execution tests for the fix. * Address PR feedback: Test combine all available providers with aspire flag. More test cases coverage. * Add an [EnvironmentVariableSkipCondition] attribute for conditional tests. Make the Aspire project name theory conditional. * Regex logic and _Web append into one step. Rename test skip for clarity. --------- Co-authored-by: Jeff Handley --- .../.template.config/template.json | 12 +++ .../Program.cs | 2 +- .../AIChatWebExecutionTests.cs | 61 +++++++++++++ ...osoft.Extensions.AI.Templates.Tests.csproj | 4 + .../EnvironmentVariableConditionAttribute.cs | 86 +++++++++++++++++++ 5 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 test/TestUtilities/XUnit/EnvironmentVariableConditionAttribute.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index caeceae1dde..d95f7c41c13 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -499,6 +499,12 @@ "valueTransform": "vectorStoreIndexNameTransform", "replaces": "data-ChatWithCustomData-CSharp.Web-" }, + "aspireClassNameReplacer": { + "type": "derived", + "valueSource": "name", + "valueTransform": "aspireClassName_ReplaceInvalidChars", + "replaces": "ChatWithCustomData_CSharp_Web_AspireClassName" + }, "webProjectNamespaceAdjuster": { "type": "generated", "generator": "switch", @@ -524,6 +530,12 @@ } }, "forms": { + "aspireClassName_ReplaceInvalidChars": { + "identifier": "replace", + "pattern": "(((?<=\\.)|^)(?=\\d)|\\W)", + "replacement": "_", + "description": "Insert underscore before digits at start, or after a dot, or to replace non-word characters" + }, "vectorStoreIndexNameTransform": { "identifier": "chain", "steps": [ diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs index c3045240cda..df2f6765d9e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs @@ -51,7 +51,7 @@ #else // UseLocalVectorStore #endif -var webApp = builder.AddProject("aichatweb-app"); +var webApp = builder.AddProject("aichatweb-app"); #if (IsOllama) // AI SERVICE PROVIDER REFERENCES webApp .WithReference(chat) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs index 1b8c4177f40..f5d2bc52e3a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.TestUtilities; using Xunit; using Xunit.Abstractions; @@ -71,6 +72,44 @@ public async Task CreateRestoreAndBuild_AspireTemplate(params string[] args) await Fixture.BuildProjectAsync(project); } + /// + /// Runs a single test with --aspire true and a project name that will trigger the class + /// name normalization bug reported in https://github.com/dotnet/extensions/issues/6811. + /// + [Fact] + public async Task CreateRestoreAndBuild_AspireProjectName() + { + await CreateRestoreAndBuild_AspireProjectName_Variants("azureopenai", "mix.ed-dash_name 123"); + } + + /// + /// Tests build for various project name formats, including dots and other + /// separators, to trigger the class name normalization bug described + /// in https://github.com/dotnet/extensions/issues/6811 + /// This runs for all provider combinations with --aspire true and different + /// project names to ensure the bug is caught in all scenarios. + /// + /// + /// Because this test takes a long time to run, it is skipped by default. Set the + /// environment variable AI_TEMPLATES_TEST_PROJECT_NAMES to "true" or "1" + /// to enable it. + /// + [ConditionalTheory] + [EnvironmentVariableCondition("AI_TEMPLATES_TEST_PROJECT_NAMES", "true", "1")] + [MemberData(nameof(GetAspireProjectNameVariants))] + public async Task CreateRestoreAndBuild_AspireProjectName_Variants(string provider, string projectName) + { + var project = await Fixture.CreateProjectAsync( + templateName: "aichatweb", + projectName: projectName, + args: new[] { "--aspire", $"--provider={provider}" }); + + project.StartupProjectRelativePath = $"{projectName}.AppHost"; + + await Fixture.RestoreProjectAsync(project); + await Fixture.BuildProjectAsync(project); + } + private static readonly (string name, string[] values)[] _templateOptions = [ ("--provider", ["azureopenai", "githubmodels", "ollama", "openai"]), ("--vector-store", ["azureaisearch", "local", "qdrant"]), @@ -158,4 +197,26 @@ private static IEnumerable GetAllPossibleOptions(ReadOnlyMemory<(strin } } } + + public static IEnumerable GetAspireProjectNameVariants() + { + foreach (string provider in new[] { "ollama", "openai", "azureopenai", "githubmodels" }) + { + foreach (string projectName in new[] + { + "mix.ed-dash_name 123", + "dot.name", + "project.123", + "space name", + ".1My.Projec-", + "1Project123", + "11double", + "1", + "nomatch" + }) + { + yield return new object[] { provider, projectName }; + } + } + } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Microsoft.Extensions.AI.Templates.Tests.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Microsoft.Extensions.AI.Templates.Tests.csproj index d2fc26ea0ab..e4c52714b79 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Microsoft.Extensions.AI.Templates.Tests.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Microsoft.Extensions.AI.Templates.Tests.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/test/TestUtilities/XUnit/EnvironmentVariableConditionAttribute.cs b/test/TestUtilities/XUnit/EnvironmentVariableConditionAttribute.cs new file mode 100644 index 00000000000..45a54409047 --- /dev/null +++ b/test/TestUtilities/XUnit/EnvironmentVariableConditionAttribute.cs @@ -0,0 +1,86 @@ +// 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; + +namespace Microsoft.TestUtilities; + +/// +/// Skips a test based on the value of an environment variable. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] +public class EnvironmentVariableConditionAttribute : Attribute, ITestCondition +{ + private string? _currentValue; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the environment variable. + /// Value(s) of the environment variable to match for the condition. + /// + /// By default, the test will be run if the value of the variable matches any of the supplied values. + /// Set to False to run the test only if the value does not match. + /// + public EnvironmentVariableConditionAttribute(string variableName, params string[] values) + { + if (string.IsNullOrEmpty(variableName)) + { + throw new ArgumentException("Value cannot be null or empty.", nameof(variableName)); + } + + if (values == null || values.Length == 0) + { + throw new ArgumentException("You must supply at least one value to match.", nameof(values)); + } + + VariableName = variableName; + Values = values; + } + + /// + /// Gets or sets a value indicating whether the test should run if the value of the variable matches any + /// of the supplied values. If False, the test runs only if the value does not match any of the + /// supplied values. Default is True. + /// + public bool RunOnMatch { get; set; } = true; + + /// + /// Gets the name of the environment variable. + /// + public string VariableName { get; } + + /// + /// Gets the value(s) of the environment variable to match for the condition. + /// + public string[] Values { get; } + + /// + /// Gets a value indicating whether the condition is met for the configured environment variable and values. + /// + public bool IsMet + { + get + { + _currentValue ??= Environment.GetEnvironmentVariable(VariableName); + var hasMatched = Values.Any(value => string.Equals(value, _currentValue, StringComparison.OrdinalIgnoreCase)); + + return RunOnMatch ? hasMatched : !hasMatched; + } + } + + /// + /// Gets a value indicating the reason the test was skipped. + /// + public string SkipReason + { + get + { + var value = _currentValue ?? "(null)"; + + return $"Test skipped on environment variable with name '{VariableName}' and value '{value}' " + + $"for the '{nameof(RunOnMatch)}' value of '{RunOnMatch}'."; + } + } +} From d649cbb14fca279da029b150b5054b131fc0ad36 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 24 Sep 2025 08:09:08 -0400 Subject: [PATCH 315/472] Update to OpenAI 2.5.0 (#6839) --- eng/packages/General.props | 2 +- .../CHANGELOG.md | 3 +- .../OpenAIEmbeddingGenerator.cs | 21 ++++++++++++- .../OpenAIResponsesChatClient.cs | 31 ++++++------------- .../OpenAIChatClientTests.cs | 13 ++++++++ .../OpenAIEmbeddingGeneratorTests.cs | 30 +++++++++++++----- .../OpenAIResponseClientTests.cs | 15 ++++++++- .../ThrowUserAgentExceptionHandler.cs | 15 +++++++++ 8 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs diff --git a/eng/packages/General.props b/eng/packages/General.props index b7066789238..5be4031ad4d 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -16,7 +16,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index a50aea8dea3..8c1febc6bc8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -2,12 +2,13 @@ ## NOT YET RELEASED +- Updated to depend on OpenAI 2.5.0. - Added M.E.AI to OpenAI conversions for response format types. - Added `ResponseTool` to `AITool` conversions. ## 9.9.0-preview.1.25458.4 -- Updated to depend on OpenAI 2.4.0 +- Updated to depend on OpenAI 2.4.0. - Updated tool mappings to recognize any `AIFunctionDeclaration`. - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - Fixed handling of annotated but empty content in the `AsIChatClient` for `AssistantClient`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index dbe9bf10237..9c3da231065 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ClientModel; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -11,12 +14,25 @@ #pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) namespace Microsoft.Extensions.AI; /// An for an OpenAI . internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator> { + // This delegate instance is used to call the internal overload of GenerateEmbeddingsAsync that accepts + // a RequestOptions. This should be replaced once a better way to pass RequestOptions is available. + private static readonly Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>? + _generateEmbeddingsAsync = + (Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>?) + typeof(EmbeddingClient) + .GetMethod( + nameof(EmbeddingClient.GenerateEmbeddingsAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, [typeof(IEnumerable), typeof(OpenAI.Embeddings.EmbeddingGenerationOptions), typeof(RequestOptions)], null) + ?.CreateDelegate( + typeof(Func, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task>>)); + /// Metadata about the embedding generator. private readonly EmbeddingGeneratorMetadata _metadata; @@ -49,7 +65,10 @@ public async Task>> GenerateAsync(IEnumerab { OpenAI.Embeddings.EmbeddingGenerationOptions? openAIOptions = ToOpenAIOptions(options); - var embeddings = (await _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken).ConfigureAwait(false)).Value; + var t = _generateEmbeddingsAsync is not null ? + _generateEmbeddingsAsync(_embeddingClient, values, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) : + _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken); + var embeddings = (await t.ConfigureAwait(false)).Value; return new(embeddings.Select(e => new Embedding(e.ToFloats()) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index d094f5f8581..ec662a68891 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -65,12 +65,7 @@ public OpenAIResponsesChatClient(OpenAIResponseClient responseClient) _responseClient = responseClient; - // https://github.com/openai/openai-dotnet/issues/662 - // Update to avoid reflection once OpenAIResponseClient.Model is exposed publicly. - string? model = typeof(OpenAIResponseClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(responseClient) as string; - - _metadata = new("openai", responseClient.Endpoint, model); + _metadata = new("openai", responseClient.Endpoint, responseClient.Model); } /// @@ -469,27 +464,19 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt break; case HostedCodeInterpreterTool codeTool: - string json; - if (codeTool.Inputs is { Count: > 0 } inputs) - { - string jsonArray = JsonSerializer.Serialize( - inputs.OfType().Select(c => c.FileId), - OpenAIJsonContext.Default.IEnumerableString); - json = $$"""{"type":"code_interpreter","container":{"type":"auto",files:{{jsonArray}}} }"""; - } - else - { - json = """{"type":"code_interpreter","container":{"type":"auto"}}"""; - } - - result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json))); + result.Tools.Add( + ResponseTool.CreateCodeInterpreterTool( + new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? + CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : + new()))); break; case HostedMcpServerTool mcpTool: McpTool responsesMcpTool = ResponseTool.CreateMcpTool( mcpTool.ServerName, mcpTool.Url, - mcpTool.Headers); + serverDescription: mcpTool.ServerDescription, + headers: mcpTool.Headers); if (mcpTool.AllowedTools is not null) { @@ -673,8 +660,8 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable(() => client.GetResponseAsync("hello")); + + Assert.StartsWith("User-Agent header: OpenAI", e.Message); + Assert.Contains("MEAI", e.Message); + } + private static IChatClient CreateChatClient(HttpClient httpClient, string modelId) => new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) .GetChatClient(modelId) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs index 43112fa88e3..0db88d499e1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs @@ -125,10 +125,7 @@ public async Task GetEmbeddingsAsync_ExpectedRequestResponse() using VerbatimHttpHandler handler = new(Input, Output); using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions - { - Transport = new HttpClientPipelineTransport(httpClient), - }).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); + using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small"); var response = await generator.GenerateAsync([ "hello, world!", @@ -188,10 +185,7 @@ public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInR using VerbatimHttpHandler handler = new(Input, Output); using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions - { - Transport = new HttpClientPipelineTransport(httpClient), - }).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); + using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small"); var response = await generator.GenerateAsync([ "hello, world!", @@ -221,4 +215,24 @@ public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInR Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); } } + + [Fact] + public async Task RequestHeaders_UserAgent_ContainsMEAI() + { + using var handler = new ThrowUserAgentExceptionHandler(); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small"); + + InvalidOperationException e = await Assert.ThrowsAsync(() => generator.GenerateAsync("hello")); + + Assert.StartsWith("User-Agent header: OpenAI", e.Message); + Assert.Contains("MEAI", e.Message); + } + + private static IEmbeddingGenerator> CreateEmbeddingGenerator(HttpClient httpClient, string modelId) => + new OpenAIClient( + new ApiKeyCredential("apikey"), + new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) + .GetEmbeddingClient(modelId) + .AsIEmbeddingGenerator(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 866e43e172d..79e63162923 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1034,7 +1034,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); AITool mcpTool = rawTool ? - ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() : + ResponseTool.CreateMcpTool("deepwiki", serverUri: new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() : new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, @@ -1506,6 +1506,19 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal(1569, response.Usage.TotalTokenCount); } + [Fact] + public async Task RequestHeaders_UserAgent_ContainsMEAI() + { + using var handler = new ThrowUserAgentExceptionHandler(); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + InvalidOperationException e = await Assert.ThrowsAsync(() => client.GetResponseAsync("hello")); + + Assert.StartsWith("User-Agent header: OpenAI", e.Message); + Assert.Contains("MEAI", e.Message); + } + private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) => new OpenAIClient( new ApiKeyCredential("apikey"), diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs new file mode 100644 index 00000000000..6b4b0fa0f1c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/ThrowUserAgentExceptionHandler.cs @@ -0,0 +1,15 @@ +// 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.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +internal sealed class ThrowUserAgentExceptionHandler : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + throw new InvalidOperationException($"User-Agent header: {request.Headers.UserAgent}"); +} From 039224794deb6aa9d1e8ed71b7fdcee10f319806 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 24 Sep 2025 08:10:51 -0400 Subject: [PATCH 316/472] Add AITool.GetService (#6830) Following the same pattern as elsewhere in M.E.AI, this enables a consumer to reach through layers of delegating tools to grab information from inner ones, such as whether they were marked as requiring approval. --- .../CHANGELOG.md | 1 + .../Functions/DelegatingAIFunction.cs | 10 ++++++ .../DelegatingAIFunctionDeclaration.cs | 10 ++++++ .../Microsoft.Extensions.AI.Abstractions.json | 12 +++++++ .../Tools/AITool.cs | 31 +++++++++++++++++++ .../CHANGELOG.md | 2 ++ .../OpenAIResponsesChatClient.cs | 10 ++++++ .../Microsoft.Extensions.AI/CHANGELOG.md | 5 +-- .../FunctionInvokingChatClient.cs | 10 +++--- .../Functions/DelegatingAIFunctionTests.cs | 3 +- .../Tools/AIToolTests.cs | 23 ++++++++++++++ .../OpenAIConversionTests.cs | 7 ++++- 12 files changed, 115 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 526072374c4..8a0e875557a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -3,6 +3,7 @@ ## NOT YET RELEASED - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. +- Added new `AITool.GetService` virtual method. - Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content. - Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs index a52c5acd959..ebb783d35b3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs @@ -58,4 +58,14 @@ protected DelegatingAIFunction(AIFunction innerFunction) /// protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => InnerFunction.InvokeAsync(arguments, cancellationToken); + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + InnerFunction.GetService(serviceType, serviceKey); + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs index 874745610c3..d113e9539f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs @@ -45,4 +45,14 @@ protected DelegatingAIFunctionDeclaration(AIFunctionDeclaration innerFunction) /// public override string ToString() => InnerFunction.ToString(); + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + InnerFunction.GetService(serviceType, serviceKey); + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 24239ee52b3..73799733746 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -685,6 +685,14 @@ "Member": "Microsoft.Extensions.AI.AITool.AITool();", "Stage": "Stable" }, + { + "Member": "virtual object? Microsoft.Extensions.AI.AITool.GetService(System.Type serviceType, object? serviceKey = null);", + "Stage": "Stable" + }, + { + "Member": "TService? Microsoft.Extensions.AI.AITool.GetService(object? serviceKey = null);", + "Stage": "Stable" + }, { "Member": "override string Microsoft.Extensions.AI.AITool.ToString();", "Stage": "Stable" @@ -1477,6 +1485,10 @@ "Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", "Stage": "Stable" }, + { + "Member": "override object? Microsoft.Extensions.AI.DelegatingAIFunction.GetService(System.Type serviceType, object? serviceKey = null);", + "Stage": "Stable" + }, { "Member": "override System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs index ab9f010ae57..4ab1a1a456f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs @@ -1,10 +1,12 @@ // 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; using System.Text; using Microsoft.Shared.Collections; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -31,6 +33,35 @@ protected AITool() /// public override string ToString() => Name; + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , + /// including itself or any services it might be wrapping. + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(this) ? this : + null; + } + + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. + /// + public TService? GetService(object? serviceKey = null) => + GetService(typeof(TService), serviceKey) is TService service ? service : default; + /// Gets the string to display in the debugger for this instance. [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index 8c1a5f5aba5..412427dcad1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -2,6 +2,8 @@ ## NOT YET RELEASED +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + ## 9.9.0-preview.1.25458.4 - Updated tool mapping to recognize any `AIFunctionDeclaration`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index ec662a68891..402f3bf0b81 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -897,5 +897,15 @@ internal sealed class ResponseToolAITool(ResponseTool tool) : AITool { public ResponseTool Tool => tool; public override string Name => Tool.GetType().Name; + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(Tool) ? Tool : + base.GetService(serviceType, serviceKey); + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 71aa3927034..e5c3028693f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -2,8 +2,9 @@ ## NOT YET RELEASED -- Updated the EnableSensitiveData properties on OpenTelemetryChatClient/EmbeddingGenerator to respect a OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT environment variable. -- Added OpenTelemetryImageGenerator to provide OpenTelemetry instrumentation for IImageGenerator implementations. +- Updated the `EnableSensitiveData` properties on `OpenTelemetryChatClient/EmbeddingGenerator` to respect a `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. +- Updated `OpenTelemetryChatClient/EmbeddingGenerator` to emit recent additions to the OpenTelemetry Semantic Conventions for Generative AI systems. +- Added `OpenTelemetryImageGenerator` to provide OpenTelemetry instrumentation for `IImageGenerator` implementations. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index f98d02278f9..0137ac06c7f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -430,7 +430,7 @@ public override async IAsyncEnumerable GetStreamingResponseA List originalMessages = [.. messages]; messages = originalMessages; - ApprovalRequiredAIFunction[]? approvalRequiredFunctions = null; // available tools that require approval + AITool[]? approvalRequiredFunctions = null; // available tools that require approval List? augmentedHistory = null; // the actual history of messages sent on turns other than the first List? functionCallContents = null; // function call contents that need responding to in the current turn List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history @@ -539,7 +539,7 @@ public override async IAsyncEnumerable GetStreamingResponseA approvalRequiredFunctions = (options?.Tools ?? Enumerable.Empty()) .Concat(AdditionalTools ?? Enumerable.Empty()) - .OfType() + .Where(t => t.GetService() is not null) .ToArray(); } @@ -741,7 +741,7 @@ private static (Dictionary? ToolMap, bool AnyRequireApproval) Cr for (int i = 0; i < count; i++) { AITool tool = toolList[i]; - anyRequireApproval |= tool is ApprovalRequiredAIFunction; + anyRequireApproval |= tool.GetService() is not null; map[tool.Name] = tool; } } @@ -1475,7 +1475,7 @@ private static ChatMessage ConvertToFunctionCallContentMessage(ApprovalResultWit /// private static (bool hasApprovalRequiringFcc, int lastApprovalCheckedFCCIndex) CheckForApprovalRequiringFCC( List? functionCallContents, - ApprovalRequiredAIFunction[] approvalRequiredFunctions, + AITool[] approvalRequiredFunctions, bool hasApprovalRequiringFcc, int lastApprovalCheckedFCCIndex) { @@ -1556,7 +1556,7 @@ private static IList ReplaceFunctionCallsWithApprovalRequests( { foreach (var t in toolMap) { - if (t.Value is ApprovalRequiredAIFunction araf && araf.Name == functionCall.Name) + if (t.Value.GetService() is { } araf && araf.Name == functionCall.Name) { anyApprovalRequired = true; break; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs index cfad15efdc0..65a832c4f7e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs @@ -20,7 +20,7 @@ public void Constructor_NullInnerFunction_ThrowsArgumentNullException() [Fact] public void DefaultOverrides_DelegateToInnerFunction() { - AIFunction expected = AIFunctionFactory.Create(() => 42); + AIFunction expected = new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => 42)); DerivedFunction actual = new(expected); Assert.Same(expected, actual.InnerFunction); @@ -32,6 +32,7 @@ public void DefaultOverrides_DelegateToInnerFunction() Assert.Same(expected.UnderlyingMethod, actual.UnderlyingMethod); Assert.Same(expected.AdditionalProperties, actual.AdditionalProperties); Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Same(expected, actual.GetService()); } private sealed class DerivedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs index 5e092d107ec..1a22f2d838e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs @@ -1,6 +1,7 @@ // 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; @@ -17,5 +18,27 @@ public void Constructor_Roundtrips() Assert.Empty(tool.AdditionalProperties); } + [Fact] + public void GetService_ReturnsExpectedObject() + { + DerivedAITool tool = new(); + + Assert.Throws("serviceType", () => tool.GetService(null!)); + + Assert.Same(tool, tool.GetService(typeof(object))); + Assert.Same(tool, tool.GetService(typeof(AITool))); + Assert.Same(tool, tool.GetService(typeof(DerivedAITool))); + + Assert.Same(tool, tool.GetService()); + Assert.Same(tool, tool.GetService()); + Assert.Same(tool, tool.GetService()); + + Assert.Null(tool.GetService(typeof(string))); + Assert.Null(tool.GetService()); + Assert.Null(tool.GetService("key")); + Assert.Null(tool.GetService("key")); + Assert.Null(tool.GetService("key")); + } + private sealed class DerivedAITool : AITool; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 7724ad98360..55c78cf0ea0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -1193,12 +1193,17 @@ public void ListAddResponseTool_AddsToolCorrectly() Assert.Single(options.Tools); Assert.NotNull(options.Tools[0]); + var rawSearchTool = ResponseTool.CreateWebSearchTool(); options = new() { - Tools = [ResponseTool.CreateWebSearchTool().AsAITool()], + Tools = [rawSearchTool.AsAITool()], }; Assert.Single(options.Tools); Assert.NotNull(options.Tools[0]); + + Assert.Same(rawSearchTool, options.Tools[0].GetService()); + Assert.Same(rawSearchTool, options.Tools[0].GetService()); + Assert.Null(options.Tools[0].GetService("key")); } private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source) From fe1cc44ba78fc81fc47c06e2d1b8f71eda14aa24 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 24 Sep 2025 08:35:30 -0400 Subject: [PATCH 317/472] Fix a couple of issues in M.E.AI.OpenAI clients (#6827) 1. Setting AllowMultipleToolCalls if there aren't any tools results in failure. Fix it to only set the property if tools have been added. 2. The chat completion service fails if a participant name containing anything other than a constrained set of characters are included. Fix it with a sanitized value. --- .../CHANGELOG.md | 3 ++ .../OpenAIChatClient.cs | 50 +++++++++++++++--- .../OpenAIResponsesChatClient.cs | 6 ++- .../OpenAIChatClientTests.cs | 51 +++++++++++++++---- .../OpenAIConversionTests.cs | 9 ++-- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 8c1febc6bc8..da67eb7c841 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -5,6 +5,9 @@ - Updated to depend on OpenAI 2.5.0. - Added M.E.AI to OpenAI conversions for response format types. - Added `ResponseTool` to `AITool` conversions. +- Fixed the handling of `HostedCodeInterpreterTool` with Responses when no file IDs were provided. +- Fixed an issue where requests would fail when AllowMultipleToolCalls was set with no tools provided. +- Fixed an issue where requests would fail when an AuthorName was provided containing invalid characters. ## 9.9.0-preview.1.25458.4 diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index fb0ee48a236..33f22dda420 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -19,14 +20,16 @@ #pragma warning disable CA1308 // Normalize strings to uppercase #pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) #pragma warning disable S1067 // Expressions should not be too complex +#pragma warning disable S2333 // Unnecessary partial #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable SA1202 // Elements should be ordered by access +#pragma warning disable SA1203 // Constants should appear before fields #pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . -internal sealed class OpenAIChatClient : IChatClient +internal sealed partial class OpenAIChatClient : IChatClient { // These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept // a RequestOptions. These should be replaced once a better way to pass RequestOptions is available. @@ -157,10 +160,11 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat input.Role == OpenAIClientExtensions.ChatRoleDeveloper) { var parts = ToOpenAIChatContent(input.Contents); + string? name = SanitizeAuthorName(input.AuthorName); yield return - input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } : - input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } : - new UserChatMessage(parts) { ParticipantName = input.AuthorName }; + input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = name } : + input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = name } : + new UserChatMessage(parts) { ParticipantName = name }; } else if (input.Role == ChatRole.Tool) { @@ -233,7 +237,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat new(ChatMessageContentPart.CreateTextPart(string.Empty)); } - message.ParticipantName = input.AuthorName; + message.ParticipantName = SanitizeAuthorName(input.AuthorName); message.Refusal = refusal; yield return message; @@ -568,7 +572,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) result.TopP ??= options.TopP; result.PresencePenalty ??= options.PresencePenalty; result.Temperature ??= options.Temperature; - result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; result.Seed ??= options.Seed; if (options.StopSequences is { Count: > 0 } stopSequences) @@ -589,6 +592,11 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) } } + if (result.Tools.Count > 0) + { + result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls; + } + if (result.ToolChoice is null && result.Tools.Count > 0) { switch (options.ToolMode) @@ -749,6 +757,27 @@ internal static void ConvertContentParts(ChatMessageContent content, IList new ChatFinishReason(s), }; + /// Sanitizes the author name to be appropriate for including as an OpenAI participant name. + private static string? SanitizeAuthorName(string? name) + { + if (name is not null) + { + const int MaxLength = 64; + + name = InvalidAuthorNameRegex().Replace(name, string.Empty); + if (name.Length == 0) + { + name = null; + } + else if (name.Length > MaxLength) + { + name = name.Substring(0, MaxLength); + } + } + + return name; + } + /// POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates. private sealed class FunctionCallInfo { @@ -756,4 +785,13 @@ private sealed class FunctionCallInfo public string? Name; public StringBuilder? Arguments; } + + private const string InvalidAuthorNamePattern = @"[^a-zA-Z0-9_]+"; +#if NET + [GeneratedRegex(InvalidAuthorNamePattern)] + private static partial Regex InvalidAuthorNameRegex(); +#else + private static Regex InvalidAuthorNameRegex() => _invalidAuthorNameRegex; + private static readonly Regex _invalidAuthorNameRegex = new(InvalidAuthorNamePattern, RegexOptions.Compiled); +#endif } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 402f3bf0b81..2720c7f761f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -413,7 +413,6 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt // Handle strongly-typed properties. result.MaxOutputTokenCount ??= options.MaxOutputTokens; - result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; result.PreviousResponseId ??= options.ConversationId; result.Temperature ??= options.Temperature; result.TopP ??= options.TopP; @@ -517,6 +516,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt } } + if (result.Tools.Count > 0) + { + result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; + } + if (result.ToolChoice is null && result.Tools.Count > 0) { switch (options.ToolMode) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 067539b9fe4..b7765f83949 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -152,6 +152,7 @@ public async Task BasicRequestResponse_NonStreaming() var response = await client.GetResponseAsync("hello", new() { + AllowMultipleToolCalls = false, MaxOutputTokens = 10, Temperature = 0.5f, }); @@ -658,15 +659,46 @@ public async Task StronglyTypedOptions_AllSent() { const string Input = """ { - "messages":[{"role":"user","content":"hello"}], - "model":"gpt-4o-mini", - "logprobs":true, - "top_logprobs":42, - "logit_bias":{"12":34}, - "parallel_tool_calls":false, - "user":"12345", - "metadata":{"something":"else"}, - "store":true + "metadata": { + "something": "else" + }, + "user": "12345", + "messages": [ + { + "role": "user", + "content": "hello" + } + ], + "model": "gpt-4o-mini", + "top_logprobs": 42, + "store": true, + "logit_bias": { + "12": 34 + }, + "logprobs": true, + "tools": [ + { + "type": "function", + "function": { + "description": "", + "name": "GetPersonAge", + "parameters": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + } + } + ], + "tool_choice": "auto", + "parallel_tool_calls": false } """; @@ -694,6 +726,7 @@ public async Task StronglyTypedOptions_AllSent() Assert.NotNull(await client.GetResponseAsync("hello", new() { AllowMultipleToolCalls = false, + Tools = [AIFunctionFactory.Create((string name) => 42, "GetPersonAge")], RawRepresentationFactory = (c) => { var openAIOptions = new ChatCompletionOptions diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 55c78cf0ea0..20b9c2b92f9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -159,7 +159,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) List messages = [ new(ChatRole.System, "You are a helpful assistant."), - new(ChatRole.User, "Hello"), + new(ChatRole.User, "Hello") { AuthorName = "Jane" }, new(ChatRole.Assistant, [ new TextContent("Hi there!"), @@ -168,9 +168,9 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) ["param1"] = "value1", ["param2"] = 42 }), - ]), + ]) { AuthorName = "!@#$%John Smith^*)" }, new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]), - new(ChatRole.Assistant, "The answer is 42."), + new(ChatRole.Assistant, "The answer is 42.") { AuthorName = "@#$#$@$" }, ]; ChatOptions? options = withOptions ? new ChatOptions { Instructions = "You talk like a parrot." } : null; @@ -196,6 +196,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) UserChatMessage m1 = Assert.IsType(convertedMessages[index + 1], exactMatch: false); Assert.Equal("Hello", Assert.Single(m1.Content).Text); + Assert.Equal("Jane", m1.ParticipantName); AssistantChatMessage m2 = Assert.IsType(convertedMessages[index + 2], exactMatch: false); Assert.Single(m2.Content); @@ -208,6 +209,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) ["param1"] = "value1", ["param2"] = 42 }), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span))); + Assert.Equal("JohnSmith", m2.ParticipantName); ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3], exactMatch: false); Assert.Equal("callid123", m3.ToolCallId); @@ -215,6 +217,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) AssistantChatMessage m4 = Assert.IsType(convertedMessages[index + 4], exactMatch: false); Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text); + Assert.Null(m4.ParticipantName); } [Fact] From b56aec451afe841d1865da4c9cb45fd5a379a519 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 24 Sep 2025 22:09:02 -0400 Subject: [PATCH 318/472] Disable really noisy analyzers, part 1 (#6837) * Disable really noisy analyzers, part 1 These are super annoying and add basically zero value. --- bench/.editorconfig | 5 -- eng/Tools/.editorconfig | 5 -- src/Analyzers/.editorconfig | 5 -- .../ApiLifecycle/ApiLifecycleAnalyzer.cs | 2 - .../ApiLifecycle/Json/JsonObjectExtensions.cs | 1 - .../DiagDescriptors.cs | 1 - src/Generators/.editorconfig | 5 -- .../Emission/Emitter.Method.cs | 1 - .../Model/LoggingMethodParameter.cs | 1 - .../Parsing/AttributeProcessors.cs | 1 - .../Parsing/Parser.LogProperties.cs | 1 - .../Parsing/Parser.Records.cs | 1 - src/LegacySupport/.editorconfig | 5 -- src/Libraries/.editorconfig | 65 +++++++++---------- .../TypeWrapper.cs | 2 - .../PerRequestLogBufferingOptions.cs | 2 - .../HttpLoggingRedactionInterceptor.cs | 2 - .../Logging/LoggingRedactionOptions.cs | 8 --- .../RequestHeadersLogEnricherOptions.cs | 2 - .../HeaderParser.cs | 2 - .../HeaderParsingFeature.cs | 1 - .../HeaderParsingOptions.cs | 2 - .../FakeSslCertificateFactory.cs | 1 - .../AdditionalPropertiesDictionary.cs | 4 -- .../AdditionalPropertiesDictionary{TValue}.cs | 5 +- .../ChatCompletion/ChatMessage.cs | 2 - .../ChatCompletion/ChatResponseExtensions.cs | 3 - .../ChatCompletion/ChatResponseFormat.cs | 1 - .../ChatCompletion/ChatResponseUpdate.cs | 2 - .../Contents/DataContent.cs | 3 - .../Contents/DataUriParser.cs | 6 -- .../Contents/FunctionCallContent.cs | 2 - .../EmbeddingGeneratorExtensions.cs | 3 - .../Functions/AIFunction.cs | 2 - .../Functions/AIFunctionArguments.cs | 12 +--- .../Functions/AIFunctionDeclaration.cs | 2 - .../Functions/AIFunctionFactory.cs | 4 -- .../Functions/DelegatingAIFunction.cs | 2 - .../DelegatingAIFunctionDeclaration.cs | 2 - ...cpServerToolRequireSpecificApprovalMode.cs | 3 - .../Image/ImageGenerationResponse.cs | 2 - ...icrosoft.Extensions.AI.Abstractions.csproj | 3 +- .../SpeechToText/SpeechToTextResponse.cs | 2 - .../SpeechToTextResponseUpdate.cs | 2 - .../SpeechToTextResponseUpdateExtensions.cs | 2 - .../Tools/AITool.cs | 2 - .../Utilities/AIJsonSchemaCreateOptions.cs | 2 - .../Utilities/AIJsonSchemaTransformCache.cs | 1 - .../Utilities/AIJsonSchemaTransformOptions.cs | 2 - .../AIJsonUtilities.Schema.Create.cs | 3 - .../Utilities/AIJsonUtilities.cs | 3 - .../AzureAIInferenceChatClient.cs | 10 --- .../AzureAIInferenceEmbeddingGenerator.cs | 2 - ...AzureAIInferenceImageEmbeddingGenerator.cs | 2 - ...soft.Extensions.AI.AzureAIInference.csproj | 4 +- .../BLEUEvaluatorContext.cs | 5 -- .../Common/SimpleWordTokenizer.cs | 2 - .../F1EvaluatorContext.cs | 5 -- .../GLEUEvaluatorContext.cs | 5 -- .../CoherenceEvaluator.cs | 4 -- .../CompletenessEvaluator.cs | 4 -- .../CompletenessEvaluatorContext.cs | 5 -- .../EquivalenceEvaluator.cs | 4 -- .../EquivalenceEvaluatorContext.cs | 5 -- .../FluencyEvaluator.cs | 4 -- .../GroundednessEvaluator.cs | 4 -- .../GroundednessEvaluatorContext.cs | 5 -- .../IntentResolutionEvaluator.cs | 2 - .../IntentResolutionRating.cs | 2 - .../RelevanceEvaluator.cs | 4 -- .../RelevanceTruthAndCompletenessEvaluator.cs | 2 - .../RelevanceTruthAndCompletenessRating.cs | 4 -- .../RetrievalEvaluator.cs | 4 -- .../TaskAdherenceEvaluator.cs | 2 - .../ToolCallAccuracyEvaluator.cs | 4 -- .../AzureStorageJsonUtilities.cs | 2 - ...sions.AI.Evaluation.Reporting.Azure.csproj | 2 - .../AzureStorageReportingConfiguration.cs | 2 - .../AzureStorageResponseCache.CacheEntry.cs | 5 -- .../Storage/AzureStorageResponseCache.cs | 5 -- .../AzureStorageResponseCacheProvider.cs | 5 -- .../CSharp/ChatDetails.cs | 6 -- .../CSharp/ChatTurnDetails.cs | 5 -- .../CSharp/Formats/Dataset.cs | 5 -- .../CSharp/JsonSerialization/JsonUtilities.cs | 2 - ....Extensions.AI.Evaluation.Reporting.csproj | 2 - .../CSharp/ReportingConfiguration.cs | 2 - .../CSharp/ScenarioRun.cs | 4 -- .../CSharp/ScenarioRunResult.cs | 17 ----- .../DiskBasedReportingConfiguration.cs | 2 - .../DiskBasedResponseCache.CacheEntry.cs | 5 -- .../CSharp/Storage/DiskBasedResponseCache.cs | 5 -- .../Storage/DiskBasedResponseCacheProvider.cs | 5 -- .../CSharp/Storage/DiskBasedResultStore.cs | 6 -- .../ContentSafetyChatClient.cs | 5 -- .../ContentSafetyChatOptions.cs | 5 -- .../ContentSafetyEvaluator.cs | 9 --- .../ContentSafetyService.UrlCacheKey.cs | 34 +++------- .../ContentSafetyService.cs | 7 -- .../ContentSafetyServiceConfiguration.cs | 5 -- .../ContentSafetyServicePayloadUtilities.cs | 4 -- .../GroundednessProEvaluatorContext.cs | 5 -- .../UngroundedAttributesEvaluatorContext.cs | 5 -- .../ChatConfiguration.cs | 5 -- .../CompositeEvaluator.cs | 4 -- .../EvaluationContext.cs | 8 --- .../EvaluationDiagnostic.cs | 5 -- .../EvaluationMetric.cs | 11 ---- .../EvaluationMetricInterpretation.cs | 5 -- .../EvaluationMetric{T}.cs | 5 -- .../EvaluationResult.cs | 6 -- .../Microsoft.Extensions.AI.OpenAI.csproj | 2 +- .../OpenAIAssistantsChatClient.cs | 6 -- .../OpenAIChatClient.cs | 5 -- .../OpenAIClientExtensions.cs | 1 - .../OpenAIEmbeddingGenerator.cs | 2 - .../OpenAIImageGenerator.cs | 3 - .../OpenAIResponsesChatClient.cs | 4 -- .../OpenAISpeechToTextClient.cs | 1 - .../AnonymousDelegatingChatClient.cs | 2 - .../ChatCompletion/CachingChatClient.cs | 3 - .../ChatCompletion/ChatResponse{T}.cs | 2 - .../DistributedCachingChatClient.cs | 2 - .../FunctionInvokingChatClient.cs | 6 -- .../ChatCompletion/OpenTelemetryChatClient.cs | 1 - .../OpenTelemetryImageGenerator.cs | 2 - .../ChatReduction/SummarizingChatReducer.cs | 1 - ...ionsEmbeddingGeneratorBuilderExtensions.cs | 2 - ...eOptionsImageGeneratorBuilderExtensions.cs | 2 - .../Image/LoggingImageGenerator.cs | 1 - .../Microsoft.Extensions.AI.csproj | 4 +- .../OpenTelemetryConsts.cs | 1 - ...ionsSpeechToTextClientBuilderExtensions.cs | 2 - .../TelemetryHelpers.cs | 8 +-- .../AsyncState.cs | 4 -- .../IAsyncContext.cs | 1 - .../IAsyncState.cs | 1 - .../Internal/DefaultHybridCache.L2.cs | 6 -- .../DefaultHybridCache.Serialization.cs | 2 - .../DefaultHybridCache.StampedeStateT.cs | 7 -- .../Internal/DefaultHybridCache.SyncLock.cs | 1 - .../DefaultHybridCache.TagInvalidation.cs | 1 - .../Internal/HybridCachePayload.cs | 5 -- .../Internal/ImmutableTypeCache.cs | 3 - .../RedactionStringBuilderExtensions.cs | 1 - .../HmacRedactor.cs | 2 - .../AutoActivationExtensions.cs | 1 - ...ummarizationServiceCollectionExtensions.cs | 1 - .../ResourceUsageThresholds.cs | 2 - .../TcpEndpointProbesOptions.cs | 2 - .../Linux/Disk/DiskStatsReader.cs | 4 -- .../Linux/Disk/LinuxSystemDiskMetrics.cs | 2 - .../Linux/LinuxUtilizationParserCgroupV1.cs | 5 -- .../Linux/LinuxUtilizationParserCgroupV2.cs | 7 -- .../Linux/Log.cs | 5 -- .../Log.cs | 1 - .../ResourceMonitorService.cs | 2 - .../ResourceMonitoringOptions.Windows.cs | 2 - .../Windows/Disk/WindowsDiskMetrics.cs | 2 - .../Windows/Interop/ProcessInfo.cs | 7 +- .../Windows/Log.cs | 4 -- .../WindowsContainerSnapshotProvider.cs | 2 - .../Windows/WindowsSnapshotProvider.cs | 2 - .../Logging/FakeLogCollectorOptions.cs | 2 - .../Logging/FakeLogRecord.cs | 3 - .../Logging/FakeLoggerProvider.cs | 1 - .../Logging/FakeLoggerT.cs | 1 - .../Metrics/MetricCollector.cs | 2 - .../DownstreamDependencyMetadataManager.cs | 4 -- .../Logging/Internal/HttpClientLogger.cs | 5 -- .../Logging/Internal/Log.cs | 2 - .../Logging/Internal/LogRecord.cs | 1 - .../Logging/LoggingOptions.cs | 12 ---- ...enceHttpClientBuilderExtensions.Hedging.cs | 1 - .../HttpRetryStrategyOptionsExtensions.cs | 1 - .../HttpResilienceContextExtensions.cs | 1 - .../Resilience/ResilienceHandler.cs | 2 - ...eHttpClientBuilderExtensions.Resilience.cs | 1 - .../Routing/OrderedGroupsRoutingOptions.cs | 2 - .../Routing/UriEndpointGroup.cs | 2 - .../Routing/WeightedGroupsRoutingOptions.cs | 2 - .../Internal/ContextualOptionsFactory.cs | 1 - .../Internal/ResilienceMetricsEnricher.cs | 1 - .../ResilienceServiceCollectionExtensions.cs | 1 - .../Buffering/LogBuffer.cs | 2 - .../Logging/LogPropertiesAttribute.cs | 1 - .../Logging/LogPropertyIgnoreAttribute.cs | 1 - .../LoggerMessageState.ClassifiedTag.cs | 1 - .../Logging/LoggerMessageState.cs | 1 - .../Sampling/LoggingSampler.cs | 2 - .../Buffering/GlobalLogBufferingOptions.cs | 2 - .../LogBufferingFilterRuleSelector.cs | 1 - .../Enrichment/ApplicationLogEnricher.cs | 1 - .../Http/HttpRouteFormatter.cs | 1 - .../Http/HttpRouteParser.cs | 1 - .../Http/IHttpRouteFormatter.cs | 1 - .../Http/IHttpRouteParser.cs | 1 - .../Http/TelemetryCommonExtensions.cs | 1 - .../Internal/LatencyConsoleExporter.cs | 1 - .../Internal/LatencyContextProvider.cs | 2 - .../Internal/LatencyContextTokenIssuer.cs | 1 - .../Logging/ExtendedLogger.ThreadLocals.cs | 1 - .../Logging/ExtendedLogger.cs | 6 -- .../Logging/ExtendedLoggerFactory.cs | 4 -- .../Import/LoggerFactoryScopeProvider.cs | 4 -- .../Logging/Import/LoggerInformation.cs | 3 - .../Logging/Import/LoggerRuleSelector.cs | 1 - .../Logging/Import/ProviderAliasUtilities.cs | 1 - .../Logging/LoggerConfig.cs | 2 - .../Sampling/LogSamplingRuleSelector.cs | 1 - ...domProbabilisticSamplerConfigureOptions.cs | 1 - .../RandomProbabilisticSamplerOptions.cs | 2 - src/Shared/Debugger/DebuggerExtensions.cs | 1 - .../JsonSchemaExporterContext.cs | 1 - .../JsonSchemaExporterOptions.cs | 1 - .../ChatCompletion/ChatMessageTests.cs | 1 - .../ChatResponseUpdateExtensionsTests.cs | 1 - .../Contents/AIAnnotationTests.cs | 1 - .../Image/ImageGeneratorTests.cs | 1 - .../BLEUEvaluatorTests.cs | 2 - .../F1EvaluatorTests.cs | 2 - .../GLEUEvaluatorTests.cs | 2 - .../IntegrationTestHelpers.cs | 1 - .../OpenTelemetryEmbeddingGeneratorTests.cs | 1 - .../Image/LoggingImageGeneratorTests.cs | 1 - .../Image/OpenTelemetryImageGeneratorTests.cs | 1 - .../AIChatWebSnapshotTests.cs | 2 - .../McpServerSnapshotTests.cs | 2 - 228 files changed, 55 insertions(+), 721 deletions(-) diff --git a/bench/.editorconfig b/bench/.editorconfig index f66bffda880..c86f7f34899 100644 --- a/bench/.editorconfig +++ b/bench/.editorconfig @@ -481,11 +481,6 @@ dotnet_diagnostic.CA1507.severity = warning # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 dotnet_diagnostic.CA1508.severity = warning -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - # Title : Invalid entry in code metrics rule specification file # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 diff --git a/eng/Tools/.editorconfig b/eng/Tools/.editorconfig index be77d5da2f3..8b26ad2938d 100644 --- a/eng/Tools/.editorconfig +++ b/eng/Tools/.editorconfig @@ -479,11 +479,6 @@ dotnet_diagnostic.CA1507.severity = warning # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 dotnet_diagnostic.CA1508.severity = warning -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - # Title : Invalid entry in code metrics rule specification file # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 diff --git a/src/Analyzers/.editorconfig b/src/Analyzers/.editorconfig index c46aa61b062..4f73565695b 100644 --- a/src/Analyzers/.editorconfig +++ b/src/Analyzers/.editorconfig @@ -482,11 +482,6 @@ dotnet_diagnostic.CA1507.severity = warning # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 dotnet_diagnostic.CA1508.severity = warning -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - # Title : Invalid entry in code metrics rule specification file # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 diff --git a/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/ApiLifecycleAnalyzer.cs b/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/ApiLifecycleAnalyzer.cs index 76973c7fc30..6424a114757 100644 --- a/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/ApiLifecycleAnalyzer.cs +++ b/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/ApiLifecycleAnalyzer.cs @@ -1,8 +1,6 @@ // 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.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; diff --git a/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/Json/JsonObjectExtensions.cs b/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/Json/JsonObjectExtensions.cs index 27a2e6a5493..0fa9fb224f3 100644 --- a/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/Json/JsonObjectExtensions.cs +++ b/src/Analyzers/Microsoft.Analyzers.Local/ApiLifecycle/Json/JsonObjectExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.Extensions.LocalAnalyzers.Json; namespace Microsoft.Extensions.LocalAnalyzers.Json; diff --git a/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs b/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs index 46af17c1e0b..ae718238cca 100644 --- a/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs +++ b/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs @@ -1,7 +1,6 @@ // 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.CodeAnalysis; [assembly: System.Resources.NeutralResourcesLanguage("en-us")] diff --git a/src/Generators/.editorconfig b/src/Generators/.editorconfig index c46aa61b062..4f73565695b 100644 --- a/src/Generators/.editorconfig +++ b/src/Generators/.editorconfig @@ -482,11 +482,6 @@ dotnet_diagnostic.CA1507.severity = warning # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 dotnet_diagnostic.CA1508.severity = warning -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - # Title : Invalid entry in code metrics rule specification file # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 diff --git a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs index c970caf330f..de0252657b7 100644 --- a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs +++ b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.Gen.Logging.Model; using Microsoft.Gen.Shared; diff --git a/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodParameter.cs b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodParameter.cs index 7f90823e9a4..b14ce4dc4e5 100644 --- a/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodParameter.cs +++ b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodParameter.cs @@ -1,7 +1,6 @@ // 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; diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/AttributeProcessors.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/AttributeProcessors.cs index f20dd59f52b..ab351819b57 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/AttributeProcessors.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/AttributeProcessors.cs @@ -1,7 +1,6 @@ // 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.CodeAnalysis; namespace Microsoft.Gen.Logging.Parsing; diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.LogProperties.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.LogProperties.cs index 3a8981c4918..23aebcdb00f 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.LogProperties.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.LogProperties.cs @@ -1,7 +1,6 @@ // 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.Linq; using System.Threading; diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.Records.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.Records.cs index c5f16008de7..484361d7347 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.Records.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.Records.cs @@ -1,7 +1,6 @@ // 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.Threading; using Microsoft.CodeAnalysis; diff --git a/src/LegacySupport/.editorconfig b/src/LegacySupport/.editorconfig index bc980461f04..757d371f53c 100644 --- a/src/LegacySupport/.editorconfig +++ b/src/LegacySupport/.editorconfig @@ -482,11 +482,6 @@ dotnet_diagnostic.CA1507.severity = warning # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 dotnet_diagnostic.CA1508.severity = warning -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - # Title : Invalid entry in code metrics rule specification file # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 diff --git a/src/Libraries/.editorconfig b/src/Libraries/.editorconfig index b74b979edaf..287246445ae 100644 --- a/src/Libraries/.editorconfig +++ b/src/Libraries/.editorconfig @@ -208,7 +208,7 @@ dotnet_code_quality.CA1030.api_surface = public # Title : Do not catch general exception types # Category : Design # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031 -dotnet_diagnostic.CA1031.severity = warning +dotnet_diagnostic.CA1031.severity = suggestion # Title : Implement standard exception constructors # Category : Design @@ -223,7 +223,7 @@ dotnet_diagnostic.CA1033.severity = warning # Title : Nested types should not be visible # Category : Design # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1034 -dotnet_diagnostic.CA1034.severity = warning +dotnet_diagnostic.CA1034.severity = suggestion # Title : Override methods on comparable types # Category : Design @@ -293,19 +293,19 @@ dotnet_code_quality.CA1052.api_surface = all # Title : URI-like parameters should not be strings # Category : Design # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1054 -dotnet_diagnostic.CA1054.severity = warning +dotnet_diagnostic.CA1054.severity = suggestion dotnet_code_quality.CA1054.api_surface = public # Title : URI-like return values should not be strings # Category : Design # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1055 -dotnet_diagnostic.CA1055.severity = warning +dotnet_diagnostic.CA1055.severity = suggestion dotnet_code_quality.CA1055.api_surface = public # Title : URI-like properties should not be strings # Category : Design # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1056 -dotnet_diagnostic.CA1056.severity = warning +dotnet_diagnostic.CA1056.severity = suggestion dotnet_code_quality.CA1056.api_surface = public # Title : Types should not extend certain base types @@ -497,12 +497,7 @@ dotnet_diagnostic.CA1507.severity = warning # Title : Avoid dead conditional code # Category : Maintainability # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning - -# Title : Avoid dead conditional code -# Category : Maintainability -# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 -dotnet_diagnostic.CA1508.severity = warning +dotnet_diagnostic.CA1508.severity = suggestion # Title : Invalid entry in code metrics rule specification file # Category : Maintainability @@ -578,7 +573,7 @@ dotnet_diagnostic.CA1715.severity = none # Title : Identifiers should not match keywords # Category : Naming # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1716 -dotnet_diagnostic.CA1716.severity = warning +dotnet_diagnostic.CA1716.severity = suggestion dotnet_code_quality.CA1716.api_surface = all dotnet_code_quality.CA1716.analyzed_symbol_kinds = all @@ -1137,7 +1132,7 @@ dotnet_diagnostic.CA2226.severity = none # Title : Collection properties should be read only # Category : Usage # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2227 -dotnet_diagnostic.CA2227.severity = warning +dotnet_diagnostic.CA2227.severity = suggestion # Title : Implement serialization constructors # Category : Usage @@ -1783,7 +1778,7 @@ dotnet_diagnostic.EA0001.severity = warning # Title : Use 'System.TimeProvider' to make the code easier to test # Category : Reliability # Help Link: https://aka.ms/dotnet-extensions-warnings/EA0002 -dotnet_diagnostic.EA0002.severity = warning +dotnet_diagnostic.EA0002.severity = suggestion # Title : Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' # Category : Performance @@ -1828,7 +1823,7 @@ dotnet_diagnostic.EA0010.severity = warning # Title : Consider removing unnecessary conditional access operator (?) # Category : Performance # Help Link: https://aka.ms/dotnet-extensions-warnings/EA0011 -dotnet_diagnostic.EA0011.severity = warning +dotnet_diagnostic.EA0011.severity = suggestion # Title : Consider removing unnecessary null coalescing assignment (??=) # Category : Performance @@ -2936,7 +2931,7 @@ dotnet_diagnostic.S101.severity = none # Title : Lines should not be too long # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-103 -dotnet_diagnostic.S103.severity = warning +dotnet_diagnostic.S103.severity = suggestion # Title : Files should not have too many lines of code # Category : Major Code Smell @@ -2968,12 +2963,12 @@ dotnet_diagnostic.S1066.severity = none # Title : Expressions should not be too complex # Category : Critical Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-1067 -dotnet_diagnostic.S1067.severity = warning +dotnet_diagnostic.S1067.severity = suggestion # Title : Methods should not have too many parameters # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-107 -dotnet_diagnostic.S107.severity = warning +dotnet_diagnostic.S107.severity = suggestion # Title : URIs should not be hardcoded # Category : Minor Code Smell @@ -2988,7 +2983,7 @@ dotnet_diagnostic.S108.severity = warning # Title : Magic numbers should not be used # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-109 -dotnet_diagnostic.S109.severity = warning +dotnet_diagnostic.S109.severity = suggestion # Title : Inheritance tree of classes should not be too deep # Category : Major Code Smell @@ -3039,7 +3034,7 @@ dotnet_diagnostic.S112.severity = none # Title : Assignments should not be made from within sub-expressions # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-1121 -dotnet_diagnostic.S1121.severity = warning +dotnet_diagnostic.S1121.severity = suggestion # Title : "Obsolete" attributes should include explanations # Category : Major Code Smell @@ -3288,12 +3283,12 @@ dotnet_diagnostic.S1656.severity = warning # Title : Multiple variables should not be declared on the same line # Category : Minor Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-1659 -dotnet_diagnostic.S1659.severity = warning +dotnet_diagnostic.S1659.severity = suggestion # Title : An abstract class should have both abstract and concrete methods # Category : Minor Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-1694 -dotnet_diagnostic.S1694.severity = warning +dotnet_diagnostic.S1694.severity = suggestion # Title : NullReferenceException should not be caught # Category : Major Code Smell @@ -3380,7 +3375,7 @@ dotnet_diagnostic.S1944.severity = warning # Title : "for" loop increment clauses should modify the loops' counters # Category : Critical Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-1994 -dotnet_diagnostic.S1994.severity = warning +dotnet_diagnostic.S1994.severity = suggestion # Title : Hashes should include an unpredictable salt # Category : Critical Vulnerability @@ -3617,7 +3612,7 @@ dotnet_diagnostic.S2330.severity = warning # Title : Redundant modifiers should not be used # Category : Minor Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-2333 -dotnet_diagnostic.S2333.severity = warning +dotnet_diagnostic.S2333.severity = suggestion # Title : Public constant members should not be used # Category : Critical Code Smell @@ -4021,7 +4016,7 @@ dotnet_diagnostic.S3251.severity = warning # Title : Constructor and destructor declarations should not be redundant # Category : Minor Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-3253 -dotnet_diagnostic.S3253.severity = warning +dotnet_diagnostic.S3253.severity = suggestion # Title : Default parameter values should not be passed as arguments # Category : Minor Code Smell @@ -4104,7 +4099,7 @@ dotnet_diagnostic.S3353.severity = warning # Title : Ternary operators should not be nested # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-3358 -dotnet_diagnostic.S3358.severity = warning +dotnet_diagnostic.S3358.severity = suggestion # Title : Date and time should not be used as a type for primary keys # Category : Minor Bug @@ -4270,7 +4265,7 @@ dotnet_diagnostic.S3603.severity = warning # Title : Member initializer values should not be redundant # Category : Minor Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-3604 -dotnet_diagnostic.S3604.severity = warning +dotnet_diagnostic.S3604.severity = suggestion # Title : Nullable type comparison should not be redundant # Category : Major Bug @@ -4543,7 +4538,7 @@ dotnet_diagnostic.S3995.severity = warning # Title : URI properties should not be strings # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-3996 -dotnet_diagnostic.S3996.severity = warning +dotnet_diagnostic.S3996.severity = suggestion # Title : String URI overloads should call "System.Uri" overloads # Category : Major Code Smell @@ -5201,7 +5196,7 @@ dotnet_diagnostic.S881.severity = suggestion # Title : "goto" statement should not be used # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-907 -dotnet_diagnostic.S907.severity = warning +dotnet_diagnostic.S907.severity = suggestion # Title : Parameter names should match base declaration and other partial definitions # Category : Critical Code Smell @@ -5648,12 +5643,12 @@ dotnet_diagnostic.SA1201.severity = none # Title : Elements should be ordered by access # Category : StyleCop.CSharp.OrderingRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1202.md -dotnet_diagnostic.SA1202.severity = warning +dotnet_diagnostic.SA1202.severity = suggestion # Title : Constants should appear before fields # Category : StyleCop.CSharp.OrderingRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1203.md -dotnet_diagnostic.SA1203.severity = warning +dotnet_diagnostic.SA1203.severity = suggestion # Title : Static elements should appear before instance elements # Category : StyleCop.CSharp.OrderingRules @@ -5812,7 +5807,7 @@ dotnet_diagnostic.SA1314.severity = none # Title : Tuple element names should use correct casing # Category : StyleCop.CSharp.NamingRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1316.md -dotnet_diagnostic.SA1316.severity = warning +dotnet_diagnostic.SA1316.severity = suggestion # Title : Access modifier should be declared # Category : StyleCop.CSharp.MaintainabilityRules @@ -6292,7 +6287,7 @@ dotnet_diagnostic.VSTHRD002.severity = warning # Title : Avoid awaiting foreign Tasks # Category : Usage # Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD003.md -dotnet_diagnostic.VSTHRD003.severity = warning +dotnet_diagnostic.VSTHRD003.severity = suggestion # Title : Await SwitchToMainThreadAsync # Category : Usage @@ -6402,3 +6397,7 @@ dotnet_diagnostic.VSTHRD114.severity = error # Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD200.md dotnet_diagnostic.VSTHRD200.severity = warning +# Title : Async method lacks 'await' operators and will run synchronously +# Category : Reliability +# Help Link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1998 +dotnet_diagnostic.CS1998.severity = suggestion diff --git a/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs b/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs index 83bd97fb515..876ace53876 100644 --- a/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs +++ b/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs @@ -14,8 +14,6 @@ namespace Microsoft.AspNetCore.AsyncState; /// Note that is not public, so nobody else can use it. /// /// The type of the value to store into . -#pragma warning disable S1694 // Convert this 'abstract' class to a concrete type with protected constructor. internal abstract class TypeWrapper -#pragma warning restore S1694 // Convert this 'abstract' class to a concrete type with protected constructor. { } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs index f675466ebe5..5e0cc99f779 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs @@ -58,7 +58,6 @@ public class PerRequestLogBufferingOptions [Range(MinimumPerRequestBufferSizeInBytes, MaximumPerRequestBufferSizeInBytes)] public int MaxPerRequestBufferSizeInBytes { get; set; } = DefaultPerRequestBufferSizeInBytes; -#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. /// @@ -71,6 +70,5 @@ public class PerRequestLogBufferingOptions /// If a log entry size is greater than , it will not be buffered and will be emitted normally. /// public IList Rules { get; set; } = []; -#pragma warning restore CA2227 } #endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs index 31a253f68db..f69b711a206 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/HttpLoggingRedactionInterceptor.cs @@ -160,7 +160,6 @@ public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext) { foreach (var enricher in _enrichers) { -#pragma warning disable CA1031 // Do not catch general exception types try { enricher.Enrich(loggerMessageState, context); @@ -169,7 +168,6 @@ public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext) { _logger.EnricherFailed(ex, enricher.GetType().Name); } -#pragma warning restore CA1031 // Do not catch general exception types } foreach (var pair in loggerMessageState) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs index d2cb34ac547..0310d59a0d7 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/LoggingRedactionOptions.cs @@ -55,9 +55,7 @@ public class LoggingRedactionOptions /// If you don't want a parameter to be redacted, mark it as . /// [Required] -#pragma warning disable CA2227 // Collection properties should be read only public IDictionary RouteParameterDataClasses { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA2227 // Collection properties should be read only /// /// Gets or sets a map between request headers to be logged and their data classification. @@ -66,9 +64,7 @@ public class LoggingRedactionOptions /// The default value is an empty dictionary, which means that no request header is logged by default. /// [Required] -#pragma warning disable CA2227 // Collection properties should be read only public IDictionary RequestHeadersDataClasses { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA2227 // Collection properties should be read only /// /// Gets or sets a map between response headers to be logged and their data classification. @@ -77,9 +73,7 @@ public class LoggingRedactionOptions /// The default value is an empty dictionary, which means that no response header is logged by default. /// [Required] -#pragma warning disable CA2227 // Collection properties should be read only public IDictionary ResponseHeadersDataClasses { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA2227 // Collection properties should be read only /// /// Gets or sets the set of HTTP paths that should be excluded from logging. @@ -97,9 +91,7 @@ public class LoggingRedactionOptions /// - "/probe/ready". /// [Required] -#pragma warning disable CA2227 // Collection properties should be read only public ISet ExcludePathStartsWith { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA2227 // Collection properties should be read only /// /// Gets or sets a value indicating whether to report unmatched routes. diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/RequestHeadersLogEnricherOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/RequestHeadersLogEnricherOptions.cs index 81483224810..e18822e7ad5 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/RequestHeadersLogEnricherOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Logging/RequestHeadersLogEnricherOptions.cs @@ -23,7 +23,5 @@ public class RequestHeadersLogEnricherOptions /// [Required] [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -#pragma warning disable CA2227 // Collection properties should be read only public IDictionary HeadersDataClasses { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA2227 // Collection properties should be read only } diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs index 34e78e3b37c..22297373047 100644 --- a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.HeaderParsing; /// Parses raw header value to a header type. /// /// The resulting strong type representing the header's value. -[SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Want abstract class for extensibility and perf")] public abstract class HeaderParser where T : notnull { @@ -21,6 +20,5 @@ public abstract class HeaderParser /// A resulting value. /// An error if parsing failed. /// Parsing result. - [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "There is no such keyword in C#.")] public abstract bool TryParse(StringValues values, [NotNullWhen(true)] out T? result, [NotNullWhen(false)] out string? error); } diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs index b3974603efb..915fbbee722 100644 --- a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs @@ -114,7 +114,6 @@ private enum BoxState NotFound = ParsingResult.NotFound, } - [SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Analyzer issue")] private abstract class Box { public abstract void Reset(); diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs index f8aa8680fb5..2c063d1aa8b 100644 --- a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs @@ -6,8 +6,6 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Primitives; -#pragma warning disable CA2227 // Collection properties should be read only - namespace Microsoft.AspNetCore.HeaderParsing; /// diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/FakeSslCertificateFactory.cs b/src/Libraries/Microsoft.AspNetCore.Testing/FakeSslCertificateFactory.cs index 1a3252df00a..136b6828537 100644 --- a/src/Libraries/Microsoft.AspNetCore.Testing/FakeSslCertificateFactory.cs +++ b/src/Libraries/Microsoft.AspNetCore.Testing/FakeSslCertificateFactory.cs @@ -19,7 +19,6 @@ internal static class FakeSslCertificateFactory /// Creates a self-signed instance for testing. /// /// An instance for testing. - [SuppressMessage("Reliability", "EA0002:Use System.TimeProvider when dealing with time in your code.", Justification = "declarations")] public static X509Certificate2 CreateSslCertificate() { var request = new CertificateRequest( diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs index dab50ff11ee..9bd7d266a6a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs @@ -1,10 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#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 - using System.Collections.Generic; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs index 14125e95b76..6b40a6c0d35 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs @@ -10,9 +10,6 @@ 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 #pragma warning disable S4039 // Interface methods should be callable by derived types #pragma warning disable CA1033 // Interface methods should be callable by derived types @@ -255,7 +252,9 @@ private sealed class DebugView(AdditionalPropertiesDictionary properties private readonly AdditionalPropertiesDictionary _properties = Throw.IfNull(properties); [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] +#pragma warning disable S2365 // Properties should not make collection or array copies public AdditionalProperty[] Items => (from p in _properties select new AdditionalProperty(p.Key, p.Value)).ToArray(); +#pragma warning restore S2365 [DebuggerDisplay("{Value}", Name = "[{Key}]")] public readonly struct AdditionalProperty(string key, TValue value) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs index 7cbbca4e822..4cdf3b30f0a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs @@ -7,8 +7,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -#pragma warning disable S3358 // Ternary operators should not be nested - namespace Microsoft.Extensions.AI; /// Represents a chat message used by an . diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 933c6412c20..81ac83cd59c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -11,9 +11,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable S109 // Magic numbers should not be used -#pragma warning disable S1121 // Assignments should not be made from within sub-expressions - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs index 088fc533d05..aaca11c4979 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs @@ -12,7 +12,6 @@ namespace Microsoft.Extensions.AI; #pragma warning disable CA1052 // Static holder types should be Static or NotInheritable -#pragma warning disable S2333 // gratuitous partial /// Represents the response format that is desired by the caller. [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index bea91f97ed9..6a4f11c3777 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -7,8 +7,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -#pragma warning disable S3358 // Ternary operators should not be nested - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs index 0c11973381b..4b8813c144e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs @@ -15,9 +15,6 @@ using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; -#pragma warning disable S3996 // URI properties should not be strings -#pragma warning disable CA1054 // URI-like parameters should not be strings -#pragma warning disable CA1056 // URI-like properties should not be strings #pragma warning disable CA1307 // Specify StringComparison for clarity namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataUriParser.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataUriParser.cs index cff25e9c30b..6afe1409e75 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataUriParser.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataUriParser.cs @@ -148,9 +148,7 @@ private static bool IsValidBase64Data(ReadOnlySpan value) #if NET8_0_OR_GREATER return Base64.IsValid(value) && !value.ContainsAny(" \t\r\n"); #else -#pragma warning disable S109 // Magic numbers should not be used if (value!.Length % 4 != 0) -#pragma warning restore S109 { return false; } @@ -171,9 +169,7 @@ private static bool IsValidBase64Data(ReadOnlySpan value) // Now traverse over characters for (var i = 0; i <= index; i++) { -#pragma warning disable S1067 // Expressions should not be too complex bool validChar = value[i] is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or (>= '0' and <= '9') or '+' or '/'; -#pragma warning restore S1067 if (!validChar) { return false; @@ -187,13 +183,11 @@ private static bool IsValidBase64Data(ReadOnlySpan value) /// Provides the parts of a parsed data URI. public sealed class DataUri(ReadOnlyMemory data, bool isBase64, string? mediaType) { -#pragma warning disable S3604 // False positive: Member initializer values should not be redundant public string? MediaType { get; } = mediaType; public ReadOnlyMemory Data { get; } = data; public bool IsBase64 { get; } = isBase64; -#pragma warning restore S3604 public byte[] ToByteArray() => IsBase64 ? Convert.FromBase64String(Data.ToString()) : diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index d19988b2b76..836d5a4110b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -83,7 +83,6 @@ public static FunctionCallContent CreateFromParsedArguments( IDictionary? arguments = null; Exception? parsingException = null; -#pragma warning disable CA1031 // Do not catch general exception types try { arguments = argumentParser(encodedArguments); @@ -92,7 +91,6 @@ public static FunctionCallContent CreateFromParsedArguments( { parsingException = new InvalidOperationException("Error parsing function call arguments.", ex); } -#pragma warning restore CA1031 // Do not catch general exception types return new FunctionCallContent(callId, name, arguments) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs index 895b7bf7ea7..d4503f57c2b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorExtensions.cs @@ -8,9 +8,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable S2302 // "nameof" should be used -#pragma warning disable S4136 // Method overloads should be grouped together - namespace Microsoft.Extensions.AI; /// Provides a collection of static methods for extending instances. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 88a224ab1c1..9af299013e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -6,8 +6,6 @@ using System.Threading; using System.Threading.Tasks; -#pragma warning disable SA1202 // Elements should be ordered by access - namespace Microsoft.Extensions.AI; /// Represents a function that can be described to an AI service and invoked. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs index 3238b88e532..4a7c9a555ce 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionArguments.cs @@ -9,8 +9,6 @@ #pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter #pragma warning disable SA1112 // Closing parenthesis should be on line of opening parenthesis #pragma warning disable SA1114 // Parameter list should follow declaration -#pragma warning disable S3358 // Extract this nested ternary operation into an independent statement. -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S4039 // Make 'AIFunctionArguments' sealed #pragma warning disable CA1033 // Make 'AIFunctionArguments' sealed #pragma warning disable CA1710 // Identifiers should have correct suffix @@ -79,14 +77,10 @@ public AIFunctionArguments(IEqualityComparer? comparer) /// public AIFunctionArguments(IDictionary? arguments, IEqualityComparer? comparer) { -#pragma warning disable S1698 // Consider using 'Equals' if value comparison is intended. _arguments = - arguments is null - ? new Dictionary(comparer) - : (arguments is Dictionary dc) && (comparer is null || dc.Comparer == comparer) - ? dc - : new Dictionary(arguments, comparer); -#pragma warning restore S1698 // Consider using 'Equals' if value comparison is intended. + arguments is null ? new(comparer) : + arguments is Dictionary dc && (comparer is null || ReferenceEquals(dc.Comparer, comparer)) ? dc : + new(arguments, comparer); } /// Gets or sets services optionally associated with these arguments. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs index e61deb65b82..203045f92b2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionDeclaration.cs @@ -4,8 +4,6 @@ using System.Text.Json; using System.Threading.Tasks; -#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods - namespace Microsoft.Extensions.AI; /// Represents a function that can be described to an AI service. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index aab33ec19c4..1b804e282dd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -22,11 +22,7 @@ using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; -#pragma warning disable CA1031 // Do not catch general exception types -#pragma warning disable S2333 // Redundant modifiers should not be used #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable SA1202 // Public members should come before private members -#pragma warning disable SA1203 // Constants should appear before fields namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs index ebb783d35b3..263d1ad6739 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs @@ -9,8 +9,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable SA1202 // Elements should be ordered by access - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs index d113e9539f7..38ebcf0ffd9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs @@ -6,8 +6,6 @@ using System.Text.Json; using Microsoft.Shared.Diagnostics; -#pragma warning disable SA1202 // Elements should be ordered by access - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs index 42a39f35c50..267b25334e1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs @@ -6,9 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -#pragma warning disable S109 // Magic numbers should not be used -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs index 53c33b22978..ba3abe8f1a3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs @@ -5,8 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators - namespace Microsoft.Extensions.AI; /// Represents the result of an image generation request. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index f5472854def..661f5d13af6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.AI @@ -14,7 +14,6 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA2227;CA1034;SA1316;S3253 $(NoWarn);MEAI001 true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs index 63c6c137411..a63d5cf2d63 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponse.cs @@ -7,8 +7,6 @@ using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators - namespace Microsoft.Extensions.AI; /// Represents the result of an speech to text request. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs index 24b7f079302..e65dd7dcbe7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdate.cs @@ -7,8 +7,6 @@ using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operators - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs index 0f83a7a8bee..683fdb24f80 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextResponseUpdateExtensions.cs @@ -7,8 +7,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable S1121 // Assignments should not be made from within sub-expressions - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs index 4ab1a1a456f..d8551fe6586 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs @@ -10,8 +10,6 @@ namespace Microsoft.Extensions.AI; -#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods - /// Represents a tool that can be specified to an AI service. [DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class AITool diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs index 667b8fee475..9da0d72e5a5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs @@ -6,8 +6,6 @@ using System.Text.Json.Nodes; using System.Threading; -#pragma warning disable S1067 // Expressions should not be too complex - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs index 2e3ac758c2f..438c05ce39b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformCache.cs @@ -1,7 +1,6 @@ // 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.Runtime.CompilerServices; using System.Text.Json; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs index 46e7476afcf..101cfa03168 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S1067 // Expressions should not be too complex - using System; using System.Text.Json.Nodes; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index 986f39d07ef..e905ce93859 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -18,10 +18,7 @@ using System.Threading; using Microsoft.Shared.Diagnostics; -#pragma warning disable S107 // Methods should not have too many parameters -#pragma warning disable S109 // Magic numbers should not be used #pragma warning disable S1075 // URIs should not be hardcoded -#pragma warning disable S1121 // Assignments should not be made from within sub-expressions #pragma warning disable S1199 // Nested block #pragma warning disable SA1118 // Parameter should not span multiple lines diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index 7e28a5983ee..ab66bf61317 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -17,9 +17,6 @@ #endif using Microsoft.Shared.Diagnostics; -#pragma warning disable S109 // Magic numbers should not be used -#pragma warning disable S1121 // Assignments should not be made from within sub-expressions - namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilities diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index b56604c027d..45081c0ab6c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -15,7 +15,6 @@ using Azure.AI.Inference; using Microsoft.Shared.Diagnostics; -#pragma warning disable S1135 // Track uses of "TODO" tags #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable SA1204 // Static elements should appear before instance elements @@ -183,13 +182,6 @@ public async IAsyncEnumerable GetStreamingResponseAsync( // Transfer over tool call updates. if (chatCompletionUpdate.ToolCallUpdate is { } toolCallUpdate) { - // TODO https://github.com/Azure/azure-sdk-for-net/issues/46830: Azure.AI.Inference - // has removed the Index property from ToolCallUpdate. It's now impossible via the - // exposed APIs to correctly handle multiple parallel tool calls, as the CallId is - // often null for anything other than the first update for a given call, and Index - // isn't available to correlate which updates are for which call. This is a temporary - // workaround to at least make a single tool call work and also make work multiple - // tool calls when their updates aren't interleaved. if (toolCallUpdate.Id is not null) { lastCallId = toolCallUpdate.Id; @@ -485,8 +477,6 @@ private static IEnumerable ToAzureAIInferenceChatMessages(IE } else if (input.Role == ChatRole.Assistant) { - // TODO: ChatRequestAssistantMessage only enables text content currently. - // Update it with other content types when it supports that. ChatRequestAssistantMessage message = new(string.Concat(input.Contents.Where(c => c is TextContent))); foreach (var content in input.Contents) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs index 95cea4e2a3b..04383a85b86 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs @@ -15,9 +15,7 @@ using Azure.AI.Inference; using Microsoft.Shared.Diagnostics; -#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable S109 // Magic numbers should not be used namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs index 91222722b2a..b04a7c73a39 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs @@ -11,9 +11,7 @@ using Azure.AI.Inference; using Microsoft.Shared.Diagnostics; -#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable S109 // Magic numbers should not be used namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index 06690c96eaf..3dc80561205 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.AI @@ -15,7 +15,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA1063;CA2227;SA1316;S1067;S1121;S3358 + $(NoWarn);CA1063 true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs index c1eaf699565..702b47948ed 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System.Collections.Generic; using System.Linq; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs index 322ea4cedd6..46ae984cb9d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Common/SimpleWordTokenizer.cs @@ -6,8 +6,6 @@ using System.Text; using Microsoft.Shared.Diagnostics; -#pragma warning disable S109 // Magic numbers should not be used - namespace Microsoft.Extensions.AI.Evaluation.NLP.Common; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs index d6dafcc3c6a..ff2091d9dc5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1EvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.NLP; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs index d98aac6811d..dd5a75d439d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System.Collections.Generic; using System.Linq; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs index 7bf68d887c9..7617c6ee2d0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CoherenceEvaluator.cs @@ -104,7 +104,6 @@ await TimingHelper.ExecuteWithTimingAsync(() => private static List GetEvaluationInstructions(ChatMessage? userRequest, ChatResponse modelResponse) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -114,14 +113,12 @@ private static List GetEvaluationInstructions(ChatMessage? userRequ - **Data**: Your input data include a QUERY and a RESPONSE. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; string renderedUserRequest = userRequest?.RenderText() ?? string.Empty; string renderedModelResponse = modelResponse.RenderText(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -194,7 +191,6 @@ private static List GetEvaluationInstructions(ChatMessage? userRequ ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs index 20a0b7b58b3..b398ca7b4aa 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluator.cs @@ -112,7 +112,6 @@ private static List GetEvaluationInstructions( ChatResponse modelResponse, CompletenessEvaluatorContext context) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -122,14 +121,12 @@ private static List GetEvaluationInstructions( - **Data**: Your input data include Response and Ground Truth. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; string renderedModelResponse = modelResponse.RenderText(); string groundTruth = context.GroundTruth; -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -186,7 +183,6 @@ private static List GetEvaluationInstructions( ## Please provide your answers between the tags: your chain of thoughts, your explanation, your score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluatorContext.cs index b9750da0a4d..8c7bf5d55e3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/CompletenessEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Quality; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs index e166f573573..e3f34d75982 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs @@ -114,12 +114,10 @@ private static List GetEvaluationInstructions( ChatResponse modelResponse, EquivalenceEvaluatorContext context) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ You are an AI assistant. You will be given the definition of an evaluation metric for assessing the quality of an answer in a question-answering task. Your job is to compute an accurate evaluation score using the provided evaluation metric. You should return a single integer value between 1 to 5 representing the evaluation metric. You will include no other text or information. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; @@ -127,7 +125,6 @@ private static List GetEvaluationInstructions( string renderedModelResponse = modelResponse.RenderText(); string groundTruth = context.GroundTruth; -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" Equivalence, as a metric, measures the similarity between the predicted answer and the correct answer. If the information and content in the predicted answer is similar or equivalent to the correct answer, then the value of the Equivalence metric should be high, else it should be low. Given the question, correct answer, and predicted answer, determine the value of Equivalence metric using the following rating scale: @@ -171,7 +168,6 @@ This rating value should always be an integer between 1 and 5. So the rating pro predicted answer: {{renderedModelResponse}} stars: """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluatorContext.cs index 31718bacfbf..b45b5b8bbe3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Quality; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs index 97a7a651427..41149bafe20 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/FluencyEvaluator.cs @@ -96,7 +96,6 @@ await TimingHelper.ExecuteWithTimingAsync(() => private static List GetEvaluationInstructions(ChatResponse modelResponse) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -106,13 +105,11 @@ private static List GetEvaluationInstructions(ChatResponse modelRes - **Data**: Your input data include a RESPONSE. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; string renderedModelResponse = modelResponse.RenderText(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -174,7 +171,6 @@ private static List GetEvaluationInstructions(ChatResponse modelRes ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs index 080fb9262f2..706655620e5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluator.cs @@ -114,7 +114,6 @@ private static List GetEvaluationInstructions( ChatResponse modelResponse, GroundednessEvaluatorContext context) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -124,14 +123,12 @@ private static List GetEvaluationInstructions( - **Data**: Your input data include CONTEXT, RESPONSE and an optional QUERY. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; string renderedModelResponse = modelResponse.RenderText(); string groundingContext = context.GroundingContext; -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt; if (userRequest is null) { @@ -296,7 +293,6 @@ private static List GetEvaluationInstructions( # Output """; } -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluatorContext.cs index a1c41989f44..d637c4d1ac9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/GroundednessEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Quality; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs index 5741adaff66..bce372e467c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionEvaluator.cs @@ -176,7 +176,6 @@ private static List GetEvaluationInstructions( string renderedModelResponse = modelResponse.RenderAsJson(); string? renderedToolDefinitions = context?.ToolDefinitions.RenderAsJson(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Goal @@ -313,7 +312,6 @@ Note that the QUERY can either be a string with a user request or an entire conv # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs index a1d9b0ef90e..c7eb8791f75 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/IntentResolutionRating.cs @@ -55,7 +55,6 @@ internal sealed class IntentResolutionRating public bool IsInconclusive => ResolutionScore < MinValue || ResolutionScore > MaxValue; [JsonConstructor] -#pragma warning disable S107 // Methods should not have too many parameters public IntentResolutionRating( int resolutionScore, string explanation, @@ -64,7 +63,6 @@ public IntentResolutionRating( bool conversationHasIntent, bool correctIntentDetected, bool intentResolved) -#pragma warning restore S107 { ResolutionScore = resolutionScore; Explanation = explanation; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs index 46f48d13ab8..31f7a68d510 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceEvaluator.cs @@ -109,7 +109,6 @@ await TimingHelper.ExecuteWithTimingAsync(() => private static List GetEvaluationInstructions(ChatMessage userRequest, ChatResponse modelResponse) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -119,14 +118,12 @@ private static List GetEvaluationInstructions(ChatMessage userReque - **Data**: Your input data include QUERY and RESPONSE. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; string renderedUserRequest = userRequest.RenderText(); string renderedModelResponse = modelResponse.RenderText(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -200,7 +197,6 @@ private static List GetEvaluationInstructions(ChatMessage userReque ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs index d175bfa7852..a41f7a92824 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessEvaluator.cs @@ -143,7 +143,6 @@ private static List GetEvaluationInstructions( string renderedModelResponse = modelResponse.RenderText(); string renderedConversationHistory = conversationHistory.RenderText(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" Read the History, User Query, and Model Response below and produce your response as a single JSON object. @@ -263,7 +262,6 @@ Step 3a. Record your response as the value of the "completeness" property in the ----- """; -#pragma warning restore S103 return [new ChatMessage(ChatRole.User, evaluationPrompt)]; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs index 83c76a1825e..8d4cd88fb5a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RelevanceTruthAndCompletenessRating.cs @@ -53,20 +53,16 @@ internal sealed class RelevanceTruthAndCompletenessRating private const int MinValue = 1; private const int MaxValue = 5; -#pragma warning disable S1067 // Expressions should not be too complex. public bool IsInconclusive => Relevance < MinValue || Relevance > MaxValue || Truth < MinValue || Truth > MaxValue || Completeness < MinValue || Completeness > MaxValue; -#pragma warning restore S1067 [JsonConstructor] -#pragma warning disable S107 // Methods should not have too many parameters. public RelevanceTruthAndCompletenessRating( int relevance, string relevanceReasoning, string[] relevanceReasons, int truth, string truthReasoning, string[] truthReasons, int completeness, string completenessReasoning, string[] completenessReasons) -#pragma warning restore S107 { (Relevance, RelevanceReasoning, RelevanceReasons, Truth, TruthReasoning, TruthReasons, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs index 557f66b0d21..7bd776f2db6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/RetrievalEvaluator.cs @@ -128,7 +128,6 @@ private static List GetEvaluationInstructions( ChatMessage userRequest, RetrievalEvaluatorContext context) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -138,7 +137,6 @@ private static List GetEvaluationInstructions( - **Data**: Your input data include QUERY and CONTEXT. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; @@ -158,7 +156,6 @@ private static List GetEvaluationInstructions( _ = builder.Append(']'); string renderedContext = builder.ToString(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -225,7 +222,6 @@ private static List GetEvaluationInstructions( ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs index c9e189af365..5f447138312 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/TaskAdherenceEvaluator.cs @@ -166,7 +166,6 @@ private static List GetEvaluationInstructions( string renderedModelResponse = modelResponse.RenderAsJson(); string? renderedToolDefinitions = context?.ToolDefinitions.RenderAsJson(); -#pragma warning disable S103 // Lines should not be too long string systemPrompt = $$""" # Instruction @@ -261,7 +260,6 @@ Response meets the core requirements but lacks precision or clarity. ## Please provide your answers between the tags: your chain of thoughts, your explanation, your score. # Output """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, systemPrompt)]; return evaluationInstructions; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs index 252b1254354..05dbf4bbc1d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/ToolCallAccuracyEvaluator.cs @@ -157,7 +157,6 @@ private static List GetEvaluationInstructions( ChatResponse modelResponse, ToolCallAccuracyEvaluatorContext context) { -#pragma warning disable S103 // Lines should not be too long const string SystemPrompt = """ # Instruction @@ -167,7 +166,6 @@ private static List GetEvaluationInstructions( - **Data**: Your input data include CONVERSATION , TOOL CALL and TOOL DEFINITION. - **Tasks**: To complete your evaluation you will be asked to evaluate the Data in different ways. """; -#pragma warning restore S103 List evaluationInstructions = [new ChatMessage(ChatRole.System, SystemPrompt)]; @@ -175,7 +173,6 @@ private static List GetEvaluationInstructions( string renderedToolCallsAndResults = modelResponse.RenderToolCallsAndResultsAsJson(); string renderedToolDefinitions = context.ToolDefinitions.RenderAsJson(); -#pragma warning disable S103 // Lines should not be too long string evaluationPrompt = $$""" # Definition @@ -218,7 +215,6 @@ 3. TOOL CALL has parameters that is present in TOOL DEFINITION. ## Please provide your answers between the tags: your chain of thoughts, your explanation, your Score. # Output """; -#pragma warning restore S103 evaluationInstructions.Add(new ChatMessage(ChatRole.User, evaluationPrompt)); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs index b36c8d8bd56..204ac68394b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -12,7 +11,6 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; internal static partial class AzureStorageJsonUtilities { - [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] internal static class Default { private static JsonSerializerOptions? _options; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj index 77ed508757c..ddc986f187a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Microsoft.Extensions.AI.Evaluation.Reporting.Azure.csproj @@ -4,8 +4,6 @@ A library that supports the Microsoft.Extensions.AI.Evaluation.Reporting library with an implementation for caching LLM responses and storing the evaluation results in an Azure Storage container. $(TargetFrameworks);netstandard2.0 Microsoft.Extensions.AI.Evaluation.Reporting - - $(NoWarn);EA0002 diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs index 3131fc1c5d9..197d795e742 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageReportingConfiguration.cs @@ -66,7 +66,6 @@ public static class AzureStorageReportingConfiguration /// (persisted to the cache using older versions of the library) will no longer be used - instead new responses /// will be fetched from the LLM and added to the cache for use in subsequent executions. /// -#pragma warning disable S107 // Methods should not have too many parameters public static ReportingConfiguration Create( DataLakeDirectoryClient client, IEnumerable evaluators, @@ -77,7 +76,6 @@ public static ReportingConfiguration Create( string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, IEnumerable? tags = null) -#pragma warning restore S107 { IEvaluationResponseCacheProvider? responseCacheProvider = chatConfiguration is not null && enableResponseCaching diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.CacheEntry.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.CacheEntry.cs index c40c4bafcf1..eedf15d75ba 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.CacheEntry.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.CacheEntry.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Globalization; using System.IO; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs index 5e83b456a0b..05f4f01eeaf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCache.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - #pragma warning disable CA1725 // CA1725: Parameter names should match base declaration. // All functions on 'IDistributedCache' use the parameter name 'token' in place of 'cancellationToken'. However, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs index 6c6d1431a1a..3507bd3769e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/Storage/AzureStorageResponseCacheProvider.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Threading; using System.Threading.Tasks; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetails.cs index 623485a8460..744a1098560 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetails.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatDetails.cs @@ -13,17 +13,11 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting; /// public sealed class ChatDetails { -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this type to be fully mutable for serialization purposes and for general - // convenience. - /// /// Gets or sets the for the LLM chat conversation turns recorded in this /// object. /// public IList TurnDetails { get; set; } -#pragma warning restore CA2227 /// /// Initializes a new instance of the class. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs index f9c93995224..2b1ad34e1ca 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ChatTurnDetails.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Text.Json.Serialization; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Formats/Dataset.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Formats/Dataset.cs index 1fb8b6c5ec9..146d3ae999b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Formats/Dataset.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Formats/Dataset.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs index 3a8c2af1ce2..fb514bd33c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,7 +12,6 @@ namespace Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization; internal static partial class JsonUtilities { - [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Default matches the generated source naming convention.")] internal static class Default { private static JsonSerializerOptions? _options; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj index 77a492d154f..8ee31bc2b1a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Microsoft.Extensions.AI.Evaluation.Reporting.csproj @@ -11,8 +11,6 @@ A library that contains support for caching LLM responses, storing the results of evaluations and generating reports from that data. $(TargetFrameworks);netstandard2.0 Microsoft.Extensions.AI.Evaluation.Reporting - - $(NoWarn);EA0002 diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs index 2f6613bbcbc..e74ca096f2c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ReportingConfiguration.cs @@ -132,7 +132,6 @@ public sealed class ReportingConfiguration /// A optional set of text tags applicable to all s created using this /// . /// -#pragma warning disable S107 // Methods should not have too many parameters public ReportingConfiguration( IEnumerable evaluators, IEvaluationResultStore resultStore, @@ -142,7 +141,6 @@ public ReportingConfiguration( string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, IEnumerable? tags = null) -#pragma warning restore S107 { Evaluators = [.. evaluators]; ResultStore = resultStore; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs index 5fa46e7e4ec..80ca411edb3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRun.cs @@ -100,7 +100,6 @@ public sealed class ScenarioRun : IAsyncDisposable private ScenarioRunResult? _result; -#pragma warning disable S107 // Methods should not have too many parameters internal ScenarioRun( string scenarioName, string iterationName, @@ -111,7 +110,6 @@ internal ScenarioRun( Func? evaluationMetricInterpreter = null, ChatDetails? chatDetails = null, IEnumerable? tags = null) -#pragma warning restore { ScenarioName = scenarioName; IterationName = iterationName; @@ -150,10 +148,8 @@ public async ValueTask EvaluateAsync( { if (_result is not null) { -#pragma warning disable S103 // Lines should not be too long throw new InvalidOperationException( $"The {nameof(ScenarioRun)} with {nameof(ScenarioName)}: {ScenarioName}, {nameof(IterationName)}: {IterationName} and {nameof(ExecutionName)}: {ExecutionName} has already been evaluated. Do not call {nameof(EvaluateAsync)} more than once on a given {nameof(ScenarioRun)}."); -#pragma warning restore S103 } EvaluationResult evaluationResult = diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunResult.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunResult.cs index af2c1d08a4c..e851a48dfdd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunResult.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ScenarioRunResult.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -126,17 +121,11 @@ public ScenarioRunResult( /// public DateTime CreationTime { get; set; } = creationTime; -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this type to be fully mutable for serialization purposes and for general - // convenience. - /// /// Gets or sets the conversation history including the request that produced the being /// evaluated in this . /// public IList Messages { get; set; } = messages; -#pragma warning restore CA2227 /// /// Gets or sets the response being evaluated in this . @@ -165,16 +154,10 @@ public ScenarioRunResult( /// public ChatDetails? ChatDetails { get; set; } = chatDetails; -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this type to be fully mutable for serialization purposes and for general - // convenience. - /// /// Gets or sets a set of text tags applicable to this . /// public IList? Tags { get; set; } = tags; -#pragma warning restore CA2227 /// /// Gets or sets the version of the format used to persist the current . diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs index 10350446229..ad28389aa30 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedReportingConfiguration.cs @@ -66,7 +66,6 @@ public static class DiskBasedReportingConfiguration /// (persisted to the cache using older versions of the library) will no longer be used - instead new responses /// will be fetched from the LLM and added to the cache for use in subsequent executions. /// -#pragma warning disable S107 // Methods should not have too many parameters public static ReportingConfiguration Create( string storageRootPath, IEnumerable evaluators, @@ -77,7 +76,6 @@ public static ReportingConfiguration Create( string executionName = Defaults.DefaultExecutionName, Func? evaluationMetricInterpreter = null, IEnumerable? tags = null) -#pragma warning restore S107 { storageRootPath = Path.GetFullPath(storageRootPath); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheEntry.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheEntry.cs index 6d5000c0395..12ac20923ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheEntry.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.CacheEntry.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Globalization; using System.IO; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs index d0a107d8710..3d2e53a9ff4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCache.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - #pragma warning disable CA1725 // CA1725: Parameter names should match base declaration. // All functions on 'IDistributedCache' use the parameter name 'token' in place of 'cancellationToken'. However, diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs index 8b60fe5a272..cfbdb207c0c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResponseCacheProvider.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Threading; using System.Threading.Tasks; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs index 4662857ec59..72bed04f2cd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/Storage/DiskBasedResultStore.cs @@ -177,11 +177,9 @@ public ValueTask DeleteResultsAsync( } /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. public async IAsyncEnumerable GetLatestExecutionNamesAsync( int? count = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) -#pragma warning restore CS1998 { if (count.HasValue && count <= 0) { @@ -204,11 +202,9 @@ public async IAsyncEnumerable GetLatestExecutionNamesAsync( } /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. public async IAsyncEnumerable GetScenarioNamesAsync( string executionName, [EnumeratorCancellation] CancellationToken cancellationToken = default) -#pragma warning restore CS1998 { IEnumerable executionDirs = EnumerateExecutionDirs(executionName, cancellationToken); @@ -224,12 +220,10 @@ public async IAsyncEnumerable GetScenarioNamesAsync( } /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. public async IAsyncEnumerable GetIterationNamesAsync( string executionName, string scenarioName, [EnumeratorCancellation] CancellationToken cancellationToken = default) -#pragma warning restore CS1998 { IEnumerable resultFiles = EnumerateResultFiles(executionName, scenarioName, cancellationToken: cancellationToken); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs index 1669bb7df6b..01618997069 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatClient.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs index 741bca9f790..2b95d3b5f39 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Safety; internal sealed class ContentSafetyChatOptions(string annotationTask, string evaluatorName) : ChatOptions diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs index 9c562c6f80c..d5414f47741 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyEvaluator.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Collections.Generic; using System.Linq; @@ -30,11 +25,9 @@ namespace Microsoft.Extensions.AI.Evaluation.Safety; /// AI Foundry Evaluation service, to the s of the s /// returned by this . /// -#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods public abstract class ContentSafetyEvaluator( string contentSafetyServiceAnnotationTask, IDictionary metricNames) : IEvaluator -#pragma warning restore S1694 { /// public IReadOnlyCollection EvaluationMetricNames { get; } = [.. metricNames.Values]; @@ -115,13 +108,11 @@ protected async ValueTask EvaluateContentSafetyAsync( { IReadOnlyList? relevantContext = FilterAdditionalContext(additionalContext); -#pragma warning disable S1067 // Expressions should not be too complex if (relevantContext is not null && relevantContext.Any() && relevantContext.SelectMany(c => c.Contents) is IEnumerable contents && contents.Any() && contents.OfType() is IEnumerable textContents && textContents.Any() && string.Join(Environment.NewLine, textContents.Select(c => c.Text)) is string contextString && !string.IsNullOrWhiteSpace(contextString)) -#pragma warning restore S1067 { // Currently we only support supplying a context for the last conversation turn (which is the main one // that is being evaluated). diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs index c8969a001c4..9454cb7f0e4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.UrlCacheKey.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; namespace Microsoft.Extensions.AI.Evaluation.Safety; @@ -18,27 +13,16 @@ private sealed class UrlCacheKey(ContentSafetyServiceConfiguration configuration internal ContentSafetyServiceConfiguration Configuration { get; } = configuration; internal string AnnotationTask { get; } = annotationTask; - public bool Equals(UrlCacheKey? other) - { - if (other is null) - { - return false; - } - else - { -#pragma warning disable S1067 // Expressions should not be too complex - return - other.Configuration.SubscriptionId == Configuration.SubscriptionId && - other.Configuration.ResourceGroupName == Configuration.ResourceGroupName && - other.Configuration.ProjectName == Configuration.ProjectName && - other.Configuration.Endpoint == Configuration.Endpoint && - other.AnnotationTask == AnnotationTask; -#pragma warning restore S1067 - } - } + public bool Equals(UrlCacheKey? other) => + other is not null && + other.Configuration.SubscriptionId == Configuration.SubscriptionId && + other.Configuration.ResourceGroupName == Configuration.ResourceGroupName && + other.Configuration.ProjectName == Configuration.ProjectName && + other.Configuration.Endpoint == Configuration.Endpoint && + other.AnnotationTask == AnnotationTask; - public override bool Equals(object? other) - => other is UrlCacheKey otherKey && Equals(otherKey); + public override bool Equals(object? other) => + other is UrlCacheKey otherKey && Equals(otherKey); public override int GetHashCode() => HashCode.Combine( diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs index ee9bdf2c926..ec3dc720bb0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Collections.Concurrent; using System.Diagnostics; @@ -392,9 +387,7 @@ await GetResponseAsync( } else { -#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test await Task.Delay(InitialDelayInMilliseconds * attempts, cancellationToken).ConfigureAwait(false); -#pragma warning restore EA0002 } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs index 56c6f3cc0f0..3e814f430e3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServiceConfiguration.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System; using System.Diagnostics.CodeAnalysis; using System.Net.Http; diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadUtilities.cs index feecec3be46..1d2f0768a1f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyServicePayloadUtilities.cs @@ -74,7 +74,6 @@ internal static (string payload, IReadOnlyList? diagnostic _ => throw new NotSupportedException($"The payload kind '{payloadFormat}' is not supported."), }; -#pragma warning disable S107 // Methods should not have too many parameters private static (string payload, IReadOnlyList? diagnostics) GetUserTextListPayloadWithEmbeddedXml( IEnumerable conversation, @@ -87,7 +86,6 @@ private static (string payload, IReadOnlyList? diagnostics string contextElementName = "Context", ContentSafetyServicePayloadStrategy strategy = ContentSafetyServicePayloadStrategy.AnnotateConversation, CancellationToken cancellationToken = default) -#pragma warning restore S107 { List> turns; List? normalizedPerTurnContext; @@ -162,7 +160,6 @@ private static (string payload, IReadOnlyList? diagnostics return (payload.ToJsonString(), diagnostics); } -#pragma warning disable S107 // Methods should not have too many parameters private static (string payload, IReadOnlyList? diagnostics) GetUserTextListPayloadWithEmbeddedJson( IEnumerable conversation, @@ -175,7 +172,6 @@ private static (string payload, IReadOnlyList? diagnostics string contextPropertyName = "context", ContentSafetyServicePayloadStrategy strategy = ContentSafetyServicePayloadStrategy.AnnotateLastTurn, CancellationToken cancellationToken = default) -#pragma warning restore S107 { if (strategy is ContentSafetyServicePayloadStrategy.AnnotateConversation) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluatorContext.cs index 677fd4154b3..56af247350d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/GroundednessProEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Safety; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluatorContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluatorContext.cs index b3273b93798..ef72729f6bb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluatorContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/UngroundedAttributesEvaluatorContext.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation.Safety; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/ChatConfiguration.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/ChatConfiguration.cs index 881816b198b..0d1db0ed487 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/ChatConfiguration.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/ChatConfiguration.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/CompositeEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/CompositeEvaluator.cs index e3d0fad4caf..07e2636d7f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/CompositeEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/CompositeEvaluator.cs @@ -50,10 +50,8 @@ public CompositeEvaluator(IEnumerable evaluators) { if (evaluator.EvaluationMetricNames.Count == 0) { -#pragma warning disable S103 // Lines should not be too long throw new InvalidOperationException( $"The '{nameof(evaluator.EvaluationMetricNames)}' property on '{evaluator.GetType().FullName}' returned an empty collection. An evaluator must advertise the names of the metrics that it supports."); -#pragma warning restore S103 } foreach (string metricName in evaluator.EvaluationMetricNames) @@ -149,10 +147,8 @@ async ValueTask EvaluateAsync(IEvaluator e) if (e.EvaluationMetricNames.Count == 0) { -#pragma warning disable S103 // Lines should not be too long throw new InvalidOperationException( $"The '{nameof(e.EvaluationMetricNames)}' property on '{e.GetType().FullName}' returned an empty collection. An evaluator must advertise the names of the metrics that it supports."); -#pragma warning restore S103 } foreach (string metricName in e.EvaluationMetricNames) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationContext.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationContext.cs index 203d86a84fa..05bdac6e68e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationContext.cs @@ -42,20 +42,13 @@ namespace Microsoft.Extensions.AI.Evaluation; /// contextual information that is modeled by the . /// /// -#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods public abstract class EvaluationContext -#pragma warning restore S1694 { /// /// Gets or sets the name for this . /// public string Name { get; set; } -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this property to be fully mutable for serialization purposes and for - // general convenience. - /// /// Gets or sets a list of objects that include all the information present in this /// . @@ -97,7 +90,6 @@ public abstract class EvaluationContext /// . /// public IList Contents { get; set; } -#pragma warning restore CA2227 /// /// Initializes a new instance of the class. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationDiagnostic.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationDiagnostic.cs index 67ec3b13ebb..64ec8e913b9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationDiagnostic.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationDiagnostic.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric.cs index 19d05c20bc4..112cd53d382 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - using System.Collections.Generic; using System.Text.Json.Serialization; @@ -43,11 +38,6 @@ public class EvaluationMetric(string name, string? reason = null) /// public EvaluationMetricInterpretation? Interpretation { get; set; } -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this type to be fully mutable for serialization purposes and for general - // convenience. - /// /// Gets or sets any s that were considered by the as part /// of the evaluation that produced the current . @@ -65,5 +55,4 @@ public class EvaluationMetric(string name, string? reason = null) /// . /// public IDictionary? Metadata { get; set; } -#pragma warning restore CA2227 } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricInterpretation.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricInterpretation.cs index 5206324edb6..b54f92e57c9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricInterpretation.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricInterpretation.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric{T}.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric{T}.cs index d2745069bc5..73965d9527d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric{T}.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetric{T}.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable S3604 -// S3604: Member initializer values should not be redundant. -// We disable this warning because it is a false positive arising from the analyzer's lack of support for C#'s primary -// constructor syntax. - namespace Microsoft.Extensions.AI.Evaluation; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationResult.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationResult.cs index 94ce86abfd9..a60ebd71e42 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationResult.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationResult.cs @@ -16,17 +16,11 @@ namespace Microsoft.Extensions.AI.Evaluation; /// public sealed class EvaluationResult { -#pragma warning disable CA2227 - // CA2227: Collection properties should be read only. - // We disable this warning because we want this type to be fully mutable for serialization purposes and for general - // convenience. - /// /// Gets or sets a collection of one or more s that represent the result of an /// evaluation. /// public IDictionary Metrics { get; set; } -#pragma warning restore CA2227 /// /// Initializes a new instance of the class. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index ce5a5fd89c0..666db46bfc1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002 + $(NoWarn);CA1063 $(NoWarn);OPENAI001;OPENAI002;MEAI001 true true diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 424185db887..ea682a11beb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -12,17 +12,11 @@ using Microsoft.Shared.Diagnostics; using OpenAI.Assistants; -#pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable SA1005 // Single line comments should begin with single space #pragma warning disable SA1204 // Static elements should appear before instance elements -#pragma warning disable S103 // Lines should not be too long #pragma warning disable S125 // Sections of code should not be commented out -#pragma warning disable S907 // "goto" statement should not be used -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S1751 // Loops with at most one iteration should be refactored #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable S4456 // Parameter validation in yielding methods should be wrapped -#pragma warning disable S4457 // Parameter validation in "async"/"await" methods should be wrapped namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 33f22dda420..1ce67c78a51 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -18,12 +18,7 @@ using OpenAI.Chat; #pragma warning disable CA1308 // Normalize strings to uppercase -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) -#pragma warning disable S1067 // Expressions should not be too complex -#pragma warning disable S2333 // Unnecessary partial #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable SA1202 // Elements should be ordered by access -#pragma warning disable SA1203 // Constants should appear before fields #pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 19fa835851f..0ebda69246c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -16,7 +16,6 @@ using OpenAI.Images; using OpenAI.Responses; -#pragma warning disable S103 // Lines should not be too long #pragma warning disable SA1515 // Single-line comment should be preceded by blank line namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index 9c3da231065..825937c9359 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -12,9 +12,7 @@ using Microsoft.Shared.Diagnostics; using OpenAI.Embeddings; -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs index 9281167d917..a51454d532c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -103,7 +102,6 @@ public async Task GenerateAsync(ImageGenerationRequest } /// -#pragma warning disable S1067 // Expressions should not be too complex public object? GetService(Type serviceType, object? serviceKey = null) => serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : serviceKey is not null ? null : @@ -111,7 +109,6 @@ public async Task GenerateAsync(ImageGenerationRequest serviceType == typeof(ImageClient) ? _imageClient : serviceType.IsInstanceOfType(this) ? this : null; -#pragma warning restore S1067 // Expressions should not be too complex /// void IDisposable.Dispose() diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 2720c7f761f..5da26a435ff 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -15,12 +15,8 @@ using Microsoft.Shared.Diagnostics; using OpenAI.Responses; -#pragma warning disable S907 // "goto" statement should not be used -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable S3254 // Default parameter values should not be passed as arguments -#pragma warning disable S3604 // Member initializer values should not be redundant -#pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1204 // Static elements should appear before instance elements namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 06051a54c26..fb0901eeb0d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -12,7 +12,6 @@ using OpenAI; using OpenAI.Audio; -#pragma warning disable S1067 // Expressions should not be too complex #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields #pragma warning disable SA1204 // Static elements should appear before instance elements diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/AnonymousDelegatingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/AnonymousDelegatingChatClient.cs index db256e94916..9a3fb9d4ad6 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/AnonymousDelegatingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/AnonymousDelegatingChatClient.cs @@ -12,8 +12,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that wraps an inner client with implementations provided by delegates. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs index 2923b0ad62d..cb5482ac213 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs @@ -8,9 +8,6 @@ using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; -#pragma warning disable S127 // "for" loop stop conditions should be invariant -#pragma warning disable SA1202 // Elements should be ordered by access - namespace Microsoft.Extensions.AI; /// diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs index 57c90307b67..a7a6c903834 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatResponse{T}.cs @@ -82,13 +82,11 @@ public bool TryGetResult([NotNullWhen(true)] out T? result) result = GetResultCore(out var failureReason); return failureReason is null; } -#pragma warning disable CA1031 // Do not catch general exception types catch { result = default; return false; } -#pragma warning restore CA1031 // Do not catch general exception types } private static T? DeserializeFirstTopLevelObject(string json, JsonTypeInfo typeInfo) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs index afaa12235ec..47984962598 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs @@ -11,8 +11,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Shared.Diagnostics; -#pragma warning disable S109 // Magic numbers should not be used -#pragma warning disable SA1202 // Elements should be ordered by access #pragma warning disable SA1502 // Element should not be on a single line namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 0137ac06c7f..4495d592adb 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -14,13 +14,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Diagnostics; -#pragma warning disable CA1508 // Avoid dead conditional code #pragma warning disable CA2213 // Disposable fields should be disposed -#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test -#pragma warning disable SA1202 // 'protected' members should come before 'private' members -#pragma warning disable S107 // Methods should not have too many parameters -#pragma warning disable S907 // "goto" statement should not be used -#pragma warning disable S1659 // Multiple variables should not be declared on the same line #pragma warning disable S3353 // Unchanged local variables should be "const" #pragma warning disable IDE0031 // Use null propagation, suppressed until repo updates to C# 14 #pragma warning disable IDE0032 // Use auto property, suppressed until repo updates to C# 14 diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 137392bf1ce..33c9306aa11 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -#pragma warning disable S3358 // Ternary operators should not be nested #pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter #pragma warning disable SA1113 // Comma should be on the same line as previous parameter diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs index 28be6407d19..b6ef8ea04e0 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryImageGenerator.cs @@ -7,13 +7,11 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Drawing; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -#pragma warning disable S3358 // Ternary operators should not be nested #pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter #pragma warning disable SA1113 // Comma should be on the same line as previous parameter diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs index ac57e919277..b79d1d18197 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -1,7 +1,6 @@ // 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; diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs index 73867e4b2f7..d2657bfdd1f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/ConfigureOptionsEmbeddingGeneratorBuilderExtensions.cs @@ -4,8 +4,6 @@ using System; using Microsoft.Shared.Diagnostics; -#pragma warning disable SA1629 // Documentation text should end with a period - namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs index 337c80951ef..52c953fba77 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Image/ConfigureOptionsImageGeneratorBuilderExtensions.cs @@ -5,8 +5,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.Diagnostics; -#pragma warning disable SA1629 // Documentation text should end with a period - namespace Microsoft.Extensions.AI; /// Provides extensions for configuring instances. diff --git a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs index 9ca0b8d06d2..f74701d766e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Image/LoggingImageGenerator.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -using static Microsoft.Extensions.AI.OpenTelemetryConsts.GenAI; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 0d77680cbc4..36e6bb00562 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.AI @@ -14,7 +14,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA2227;CA1034;SA1316;S1067;S1121;S1994;S3253;MEAI001 + $(NoWarn);MEAI001 + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b75b66eaea6..e4f5ea11353 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -194,17 +194,17 @@ - + https://github.com/dotnet/arcade - 5fe939db0a156be6f10e17c105b1842c0c8c8bdc + a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 - + https://github.com/dotnet/arcade - 5fe939db0a156be6f10e17c105b1842c0c8c8bdc + a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 - + https://github.com/dotnet/arcade - 5fe939db0a156be6f10e17c105b1842c0c8c8bdc + a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 diff --git a/eng/Versions.props b/eng/Versions.props index c704134fe48..91a09df773f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -83,7 +83,7 @@ 9.0.9 - 9.0.0-beta.25428.3 + 10.0.0-beta.25476.2 @@ -159,5 +159,9 @@ https://github.com/dotnet/arcade/blob/f5a7c5d5c56197b09715dece7541ca06beb94eb0/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnit/XUnit.targets#L9 --> 2.9.3 + + 2.8.2 diff --git a/eng/common/CIBuild.cmd b/eng/common/CIBuild.cmd index 56c2f25ac22..ac1f72bf94e 100644 --- a/eng/common/CIBuild.cmd +++ b/eng/common/CIBuild.cmd @@ -1,2 +1,2 @@ @echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" \ No newline at end of file +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index 792b60b49d4..9445c314325 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -157,7 +157,7 @@ if ($dotnet31Source -ne $null) { AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -pwd $Password } -$dotnetVersions = @('5','6','7','8','9') +$dotnetVersions = @('5','6','7','8','9','10') foreach ($dotnetVersion in $dotnetVersions) { $feedPrefix = "dotnet" + $dotnetVersion; diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index facb415ca6f..ddf4efc81a4 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -99,7 +99,7 @@ if [ "$?" == "0" ]; then PackageSources+=('dotnet3.1-internal-transport') fi -DotNetVersions=('5' '6' '7' '8' '9') +DotNetVersions=('5' '6' '7' '8' '9' '10') for DotNetVersion in ${DotNetVersions[@]} ; do FeedPrefix="dotnet${DotNetVersion}"; diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index 438f9920c43..8cfee107e7a 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -7,6 +7,7 @@ Param( [string] $msbuildEngine = $null, [bool] $warnAsError = $true, [bool] $nodeReuse = $true, + [switch] $buildCheck = $false, [switch][Alias('r')]$restore, [switch] $deployDeps, [switch][Alias('b')]$build, @@ -20,6 +21,7 @@ Param( [switch] $publish, [switch] $clean, [switch][Alias('pb')]$productBuild, + [switch]$fromVMR, [switch][Alias('bl')]$binaryLog, [switch][Alias('nobl')]$excludeCIBinarylog, [switch] $ci, @@ -71,6 +73,9 @@ function Print-Usage() { Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)" + Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" + Write-Host " -buildCheck Sets /check msbuild parameter" + Write-Host " -fromVMR Set when building from within the VMR" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." @@ -97,6 +102,7 @@ function Build { $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'Build.binlog') } else { '' } $platformArg = if ($platform) { "/p:Platform=$platform" } else { '' } + $check = if ($buildCheck) { '/check' } else { '' } if ($projects) { # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. @@ -113,6 +119,7 @@ function Build { MSBuild $toolsetBuildProj ` $bl ` $platformArg ` + $check ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` /p:Restore=$restore ` @@ -122,11 +129,13 @@ function Build { /p:Deploy=$deploy ` /p:Test=$test ` /p:Pack=$pack ` - /p:DotNetBuildRepo=$productBuild ` + /p:DotNetBuild=$productBuild ` + /p:DotNetBuildFromVMR=$fromVMR ` /p:IntegrationTest=$integrationTest ` /p:PerformanceTest=$performanceTest ` /p:Sign=$sign ` /p:Publish=$publish ` + /p:RestoreStaticGraphEnableBinaryLogger=$binaryLog ` @properties } diff --git a/eng/common/build.sh b/eng/common/build.sh index ac1ee8620cd..9767bb411a4 100755 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -42,6 +42,8 @@ usage() echo " --prepareMachine Prepare machine for CI run, clean up processes after build" echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + echo " --buildCheck Sets /check msbuild parameter" + echo " --fromVMR Set when building from within the VMR" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." @@ -63,6 +65,7 @@ restore=false build=false source_build=false product_build=false +from_vmr=false rebuild=false test=false integration_test=false @@ -76,6 +79,7 @@ clean=false warn_as_error=true node_reuse=true +build_check=false binary_log=false exclude_ci_binary_log=false pipelines_log=false @@ -87,7 +91,7 @@ verbosity='minimal' runtime_source_feed='' runtime_source_feed_key='' -properties='' +properties=() while [[ $# > 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in @@ -127,19 +131,22 @@ while [[ $# > 0 ]]; do -pack) pack=true ;; - -sourcebuild|-sb) + -sourcebuild|-source-build|-sb) build=true source_build=true product_build=true restore=true pack=true ;; - -productBuild|-pb) + -productbuild|-product-build|-pb) build=true product_build=true restore=true pack=true ;; + -fromvmr|-from-vmr) + from_vmr=true + ;; -test|-t) test=true ;; @@ -173,6 +180,9 @@ while [[ $# > 0 ]]; do node_reuse=$2 shift ;; + -buildcheck) + build_check=true + ;; -runtimesourcefeed) runtime_source_feed=$2 shift @@ -182,7 +192,7 @@ while [[ $# > 0 ]]; do shift ;; *) - properties="$properties $1" + properties+=("$1") ;; esac @@ -216,7 +226,7 @@ function Build { InitializeCustomToolset if [[ ! -z "$projects" ]]; then - properties="$properties /p:Projects=$projects" + properties+=("/p:Projects=$projects") fi local bl="" @@ -224,15 +234,21 @@ function Build { bl="/bl:\"$log_dir/Build.binlog\"" fi + local check="" + if [[ "$build_check" == true ]]; then + check="/check" + fi + MSBuild $_InitializeToolset \ $bl \ + $check \ /p:Configuration=$configuration \ /p:RepoRoot="$repo_root" \ /p:Restore=$restore \ /p:Build=$build \ - /p:DotNetBuildRepo=$product_build \ - /p:ArcadeBuildFromSource=$source_build \ + /p:DotNetBuild=$product_build \ /p:DotNetBuildSourceOnly=$source_build \ + /p:DotNetBuildFromVMR=$from_vmr \ /p:Rebuild=$rebuild \ /p:Test=$test \ /p:Pack=$pack \ @@ -240,7 +256,8 @@ function Build { /p:PerformanceTest=$performance_test \ /p:Sign=$sign \ /p:Publish=$publish \ - $properties + /p:RestoreStaticGraphEnableBinaryLogger=$binary_log \ + ${properties[@]+"${properties[@]}"} ExitWithExitCode 0 } diff --git a/eng/common/cibuild.sh b/eng/common/cibuild.sh index 1a02c0dec8f..66e3b0ac61c 100755 --- a/eng/common/cibuild.sh +++ b/eng/common/cibuild.sh @@ -13,4 +13,4 @@ while [[ -h $source ]]; do done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" -. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ \ No newline at end of file +. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index 8da43d3b583..5ce51840619 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,11 +19,11 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false + enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false enablePublishBuildAssets: false enablePublishTestResults: false - enablePublishUsingPipelines: false enableBuildRetry: false mergeTestResults: false testRunTitle: '' @@ -74,9 +74,6 @@ jobs: - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE value: '$(Build.Repository.Uri)' - - ${{ if eq(parameters.enableRichCodeNavigation, 'true') }}: - - name: EnableRichCodeNavigation - value: 'true' # Retry signature validation up to three times, waiting 2 seconds between attempts. # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY @@ -128,23 +125,12 @@ jobs: - ${{ preStep }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin - inputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea - ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca - env: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' + - template: /eng/common/core-templates/steps/install-microbuild.yml + parameters: + enableMicrobuild: ${{ parameters.enableMicrobuild }} + enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} + microbuildUseESRP: ${{ parameters.microbuildUseESRP }} continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}: - task: NuGetAuthenticate@1 @@ -160,27 +146,15 @@ jobs: - ${{ each step in parameters.steps }}: - ${{ step }} - - ${{ if eq(parameters.enableRichCodeNavigation, true) }}: - - task: RichCodeNavIndexer@0 - displayName: RichCodeNav Upload - inputs: - languages: ${{ coalesce(parameters.richCodeNavigationLanguage, 'csharp') }} - environment: ${{ coalesce(parameters.richCodeNavigationEnvironment, 'internal') }} - richNavLogOutputDirectory: $(System.DefaultWorkingDirectory)/artifacts/bin - uploadRichNavArtifacts: ${{ coalesce(parameters.richCodeNavigationUploadArtifacts, false) }} - continueOnError: true - - ${{ each step in parameters.componentGovernanceSteps }}: - ${{ step }} - - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - task: MicroBuildCleanup@1 - displayName: Execute Microbuild cleanup tasks - condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: /eng/common/core-templates/steps/cleanup-microbuild.yml + parameters: + enableMicrobuild: ${{ parameters.enableMicrobuild }} + enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} continueOnError: ${{ parameters.continueOnError }} - env: - TeamName: $(_TeamName) # Publish test results - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}: diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index edefa789d36..c5788829a87 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -4,7 +4,7 @@ parameters: # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool pool: '' - + CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) @@ -27,7 +27,7 @@ parameters: is1ESPipeline: '' jobs: - job: OneLocBuild${{ parameters.JobNameSuffix }} - + dependsOn: ${{ parameters.dependsOn }} displayName: OneLocBuild${{ parameters.JobNameSuffix }} @@ -86,8 +86,7 @@ jobs: isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} ${{ if eq(parameters.CreatePr, true) }}: isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} - ${{ if eq(parameters.RepoType, 'gitHub') }}: - isShouldReusePrSelected: ${{ parameters.ReusePr }} + isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: @@ -100,22 +99,20 @@ jobs: mirrorBranch: ${{ parameters.MirrorBranch }} condition: ${{ parameters.condition }} - - template: /eng/common/core-templates/steps/publish-build-artifacts.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} - args: - displayName: Publish Localization Files - pathToPublish: '$(Build.ArtifactStagingDirectory)/loc' - publishLocation: Container - artifactName: Loc - condition: ${{ parameters.condition }} + # Copy the locProject.json to the root of the Loc directory, then publish a pipeline artifact + - task: CopyFiles@2 + displayName: Copy LocProject.json + inputs: + SourceFolder: '$(System.DefaultWorkingDirectory)/eng/Localize/' + Contents: 'LocProject.json' + TargetFolder: '$(Build.ArtifactStagingDirectory)/loc' + condition: ${{ parameters.condition }} - - template: /eng/common/core-templates/steps/publish-build-artifacts.yml + - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: - displayName: Publish LocProject.json - pathToPublish: '$(System.DefaultWorkingDirectory)/eng/Localize/' - publishLocation: Container - artifactName: Loc - condition: ${{ parameters.condition }} \ No newline at end of file + targetPath: '$(Build.ArtifactStagingDirectory)/loc' + artifactName: 'Loc' + displayName: 'Publish Localization Files' + condition: ${{ parameters.condition }} diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index b103b7ee168..37dff559fc1 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -20,9 +20,6 @@ parameters: # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. runAsPublic: false - # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing - publishUsingPipelines: false - # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing publishAssetsImmediately: false @@ -32,8 +29,19 @@ parameters: is1ESPipeline: '' + # Optional: 🌤️ or not the build has assets it wants to publish to BAR + isAssetlessBuild: false + + # Optional, publishing version + publishingVersion: 3 + + # Optional: A minimatch pattern for the asset manifests to publish to BAR + assetManifestsPattern: '*/manifests/**/*.xml' + repositoryAlias: self + officialBuildId: '' + jobs: - job: Asset_Registry_Publish @@ -56,6 +64,11 @@ jobs: value: false # unconditional - needed for logs publishing (redactor tool version) - template: /eng/common/core-templates/post-build/common-variables.yml + - name: OfficialBuildId + ${{ if ne(parameters.officialBuildId, '') }}: + value: ${{ parameters.officialBuildId }} + ${{ else }}: + value: $(Build.BuildNumber) pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) @@ -77,15 +90,33 @@ jobs: - checkout: ${{ parameters.repositoryAlias }} fetchDepth: 3 clean: true - - - task: DownloadBuildArtifacts@0 - displayName: Download artifact - inputs: - artifactName: AssetManifests - downloadPath: '$(Build.StagingDirectory)/Download' - checkDownloadedFiles: true - condition: ${{ parameters.condition }} - continueOnError: ${{ parameters.continueOnError }} + + - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: + - ${{ if eq(parameters.publishingVersion, 3) }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Asset Manifests + inputs: + artifactName: AssetManifests + targetPath: '$(Build.StagingDirectory)/AssetManifests' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - ${{ if eq(parameters.publishingVersion, 4) }}: + - task: DownloadPipelineArtifact@2 + displayName: Download V4 asset manifests + inputs: + itemPattern: '*/manifests/**/*.xml' + targetPath: '$(Build.StagingDirectory)/AllAssetManifests' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: CopyFiles@2 + displayName: Copy V4 asset manifests to AssetManifests + inputs: + SourceFolder: '$(Build.StagingDirectory)/AllAssetManifests' + Contents: ${{ parameters.assetManifestsPattern }} + TargetFolder: '$(Build.StagingDirectory)/AssetManifests' + flattenFolders: true + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} - task: NuGetAuthenticate@1 @@ -97,10 +128,10 @@ jobs: scriptLocation: scriptPath scriptPath: $(System.DefaultWorkingDirectory)/eng/common/sdk-task.ps1 arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet - /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:ManifestsPath='$(Build.StagingDirectory)/AssetManifests' + /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} /p:MaestroApiEndpoint=https://maestro.dot.net - /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} - /p:OfficialBuildId=$(Build.BuildNumber) + /p:OfficialBuildId=$(OfficialBuildId) condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} @@ -122,6 +153,17 @@ jobs: Copy-Item -Path $symbolExclusionfile -Destination "$(Build.StagingDirectory)/ReleaseConfigs" } + - ${{ if eq(parameters.publishingVersion, 4) }}: + - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} + args: + targetPath: '$(Build.ArtifactStagingDirectory)/MergedManifest.xml' + artifactName: AssetManifests + displayName: 'Publish Merged Manifest' + retryCountOnTaskFailure: 10 # for any logs being locked + sbomEnabled: false # we don't need SBOM for logs + - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} @@ -131,7 +173,7 @@ jobs: publishLocation: Container artifactName: ReleaseConfigs - - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + - ${{ if or(eq(parameters.publishAssetsImmediately, 'true'), eq(parameters.isAssetlessBuild, 'true')) }}: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} @@ -152,6 +194,7 @@ jobs: -WaitPublishingFinish true -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' + -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - template: /eng/common/core-templates/steps/publish-logs.yml diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index 5baedac1e03..d805d5faeb9 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -12,9 +12,10 @@ parameters: # The name of the job. This is included in the job ID. # targetRID: '' # The name of the target RID to use, instead of the one auto-detected by Arcade. - # nonPortable: false + # portableBuild: false # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than - # linux-x64), and compiling against distro-provided packages rather than portable ones. + # linux-x64), and compiling against distro-provided packages rather than portable ones. The + # default is portable mode. # skipPublishValidation: false # Disables publishing validation. By default, a check is performed to ensure no packages are # published by source-build. @@ -33,9 +34,6 @@ parameters: # container and pool. platform: {} - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: '' # If set to true and running on a non-public project, @@ -96,4 +94,3 @@ jobs: parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} platform: ${{ parameters.platform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} diff --git a/eng/common/core-templates/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml index 662b9fcce15..30530359a5d 100644 --- a/eng/common/core-templates/job/source-index-stage1.yml +++ b/eng/common/core-templates/job/source-index-stage1.yml @@ -1,8 +1,5 @@ parameters: runAsPublic: false - sourceIndexUploadPackageVersion: 2.0.0-20250425.2 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20250425.2 - sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog @@ -16,12 +13,6 @@ jobs: dependsOn: ${{ parameters.dependsOn }} condition: ${{ parameters.condition }} variables: - - name: SourceIndexUploadPackageVersion - value: ${{ parameters.sourceIndexUploadPackageVersion }} - - name: SourceIndexProcessBinlogPackageVersion - value: ${{ parameters.sourceIndexProcessBinlogPackageVersion }} - - name: SourceIndexPackageSource - value: ${{ parameters.sourceIndexPackageSource }} - name: BinlogPath value: ${{ parameters.binlogPath }} - template: /eng/common/core-templates/variables/pool-providers.yml @@ -34,12 +25,10 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $(DncEngPublicBuildPool) - image: 1es-windows-2022-open - os: windows + image: windows.vs2022.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $(DncEngInternalBuildPool) - image: 1es-windows-2022 - os: windows + image: windows.vs2022.amd64 steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -47,35 +36,9 @@ jobs: - ${{ each preStep in parameters.preSteps }}: - ${{ preStep }} - - - task: UseDotNet@2 - displayName: Use .NET 8 SDK - inputs: - packageType: sdk - version: 8.0.x - installationPath: $(Agent.TempDirectory)/dotnet - workingDirectory: $(Agent.TempDirectory) - - - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version $(sourceIndexProcessBinlogPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version $(sourceIndexUploadPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools - displayName: Download Tools - # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. - workingDirectory: $(Agent.TempDirectory) - - script: ${{ parameters.sourceIndexBuildCommand }} displayName: Build Repository - - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output - displayName: Process Binlog into indexable sln - - - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - task: AzureCLI@2 - displayName: Log in to Azure and upload stage1 artifacts to source index - inputs: - azureSubscription: 'SourceDotNet Stage1 Publish' - addSpnToEnvironment: true - scriptType: 'ps' - scriptLocation: 'inlineScript' - inlineScript: | - $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1 + - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml + parameters: + binLogPath: ${{ parameters.binLogPath }} \ No newline at end of file diff --git a/eng/common/core-templates/jobs/codeql-build.yml b/eng/common/core-templates/jobs/codeql-build.yml index 4571a7864df..dbc14ac580a 100644 --- a/eng/common/core-templates/jobs/codeql-build.yml +++ b/eng/common/core-templates/jobs/codeql-build.yml @@ -15,7 +15,6 @@ jobs: enablePublishBuildArtifacts: false enablePublishTestResults: false enablePublishBuildAssets: false - enablePublishUsingPipelines: false enableTelemetry: true variables: diff --git a/eng/common/core-templates/jobs/jobs.yml b/eng/common/core-templates/jobs/jobs.yml index 3129670b338..01ada747665 100644 --- a/eng/common/core-templates/jobs/jobs.yml +++ b/eng/common/core-templates/jobs/jobs.yml @@ -5,9 +5,6 @@ parameters: # Optional: Include PublishBuildArtifacts task enablePublishBuildArtifacts: false - # Optional: Enable publishing using release pipelines - enablePublishUsingPipelines: false - # Optional: Enable running the source-build jobs to build repo from source enableSourceBuild: false @@ -30,6 +27,9 @@ parameters: # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage. publishAssetsImmediately: false + # Optional: 🌤️ or not the build has assets it wants to publish to BAR + isAssetlessBuild: false + # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml) artifactsPublishingAdditionalParameters: '' signingValidationAdditionalParameters: '' @@ -44,6 +44,7 @@ parameters: artifacts: {} is1ESPipeline: '' repositoryAlias: self + officialBuildId: '' # Internal resources (telemetry, microbuild) can only be accessed from non-public projects, # and some (Microbuild) should only be applied to non-PR cases for internal builds. @@ -84,7 +85,6 @@ jobs: - template: /eng/common/core-templates/jobs/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - allCompletedJobId: Source_Build_Complete ${{ each parameter in parameters.sourceBuildParameters }}: ${{ parameter.key }}: ${{ parameter.value }} @@ -97,7 +97,7 @@ jobs: ${{ parameter.key }}: ${{ parameter.value }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, '')) }}: + - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, ''), eq(parameters.isAssetlessBuild, true)) }}: - template: ../job/publish-build-assets.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} @@ -109,13 +109,12 @@ jobs: - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: - ${{ each job in parameters.jobs }}: - ${{ job.job }} - - ${{ if eq(parameters.enableSourceBuild, true) }}: - - Source_Build_Complete runAsPublic: ${{ parameters.runAsPublic }} - publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} - publishAssetsImmediately: ${{ parameters.publishAssetsImmediately }} + publishAssetsImmediately: ${{ or(parameters.publishAssetsImmediately, parameters.isAssetlessBuild) }} + isAssetlessBuild: ${{ parameters.isAssetlessBuild }} enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} repositoryAlias: ${{ parameters.repositoryAlias }} + officialBuildId: ${{ parameters.officialBuildId }} diff --git a/eng/common/core-templates/jobs/source-build.yml b/eng/common/core-templates/jobs/source-build.yml index 0b408a67bd5..d92860cba20 100644 --- a/eng/common/core-templates/jobs/source-build.yml +++ b/eng/common/core-templates/jobs/source-build.yml @@ -2,28 +2,19 @@ parameters: # This template adds arcade-powered source-build to CI. A job is created for each platform, as # well as an optional server job that completes when all platform jobs complete. - # The name of the "join" job for all source-build platforms. If set to empty string, the job is - # not included. Existing repo pipelines can use this job depend on all source-build jobs - # completing without maintaining a separate list of every single job ID: just depend on this one - # server job. By default, not included. Recommended name if used: 'Source_Build_Complete'. - allCompletedJobId: '' - # See /eng/common/core-templates/job/source-build.yml jobNamePrefix: 'Source_Build' # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream9' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-10-amd64' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, # one job runs on 'defaultManagedPlatform'. platforms: [] - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: '' # If set to true and running on a non-public project, @@ -34,23 +25,12 @@ parameters: jobs: -- ${{ if ne(parameters.allCompletedJobId, '') }}: - - job: ${{ parameters.allCompletedJobId }} - displayName: Source-Build Complete - pool: server - dependsOn: - - ${{ each platform in parameters.platforms }}: - - ${{ parameters.jobNamePrefix }}_${{ platform.name }} - - ${{ if eq(length(parameters.platforms), 0) }}: - - ${{ parameters.jobNamePrefix }}_${{ parameters.defaultManagedPlatform.name }} - - ${{ each platform in parameters.platforms }}: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ platform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} - ${{ if eq(length(parameters.platforms), 0) }}: @@ -59,5 +39,4 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ parameters.defaultManagedPlatform }} - componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 2ee8bbfff54..f6f87fe5c67 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -60,6 +60,11 @@ parameters: artifactNames: '' downloadArtifacts: true + - name: isAssetlessBuild + type: boolean + displayName: Is Assetless Build + default: false + # These parameters let the user customize the call to sdk-task.ps1 for publishing # symbols & general artifacts as well as for signing validation - name: symbolPublishingAdditionalParameters @@ -188,9 +193,6 @@ stages: buildId: $(AzDOBuildId) artifactName: PackageArtifacts checkDownloadedFiles: true - itemPattern: | - ** - !**/Microsoft.SourceBuild.Intermediate.*.nupkg # This is necessary whenever we want to publish/restore to an AzDO private feed # Since sdk-task.ps1 tries to restore packages we need to do this authentication here @@ -320,3 +322,4 @@ stages: -RequireDefaultChannels ${{ parameters.requireDefaultChannels }} -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' + -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' diff --git a/eng/common/core-templates/steps/cleanup-microbuild.yml b/eng/common/core-templates/steps/cleanup-microbuild.yml new file mode 100644 index 00000000000..c0fdcd3379d --- /dev/null +++ b/eng/common/core-templates/steps/cleanup-microbuild.yml @@ -0,0 +1,28 @@ +parameters: + # Enable cleanup tasks for MicroBuild + enableMicrobuild: false + # Enable cleanup tasks for MicroBuild on Mac and Linux + # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' + enableMicrobuildForMacAndLinux: false + continueOnError: false + +steps: + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and( + always(), + or( + and( + eq(variables['Agent.Os'], 'Windows_NT'), + in(variables['_SignType'], 'real', 'test') + ), + and( + ${{ eq(parameters.enableMicrobuildForMacAndLinux, true) }}, + ne(variables['Agent.Os'], 'Windows_NT'), + eq(variables['_SignType'], 'real') + ) + )) + continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index 7f5b84c4cb8..c05f6502797 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -5,7 +5,7 @@ # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: - PackageVersion: 9.0.0 + PackageVersion: 10.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom diff --git a/eng/common/core-templates/steps/get-delegation-sas.yml b/eng/common/core-templates/steps/get-delegation-sas.yml index 9db5617ea7d..d2901470a7f 100644 --- a/eng/common/core-templates/steps/get-delegation-sas.yml +++ b/eng/common/core-templates/steps/get-delegation-sas.yml @@ -31,16 +31,7 @@ steps: # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - # Temporarily work around a helix issue where SAS tokens with / in them will cause incorrect downloads - # of correlation payloads. https://github.com/dotnet/dnceng/issues/3484 - $sas = "" - do { - $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to generate SAS token." - exit 1 - } - } while($sas.IndexOf('/') -ne -1) + $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml new file mode 100644 index 00000000000..d6b9878f54d --- /dev/null +++ b/eng/common/core-templates/steps/install-microbuild.yml @@ -0,0 +1,91 @@ +parameters: + # Enable install tasks for MicroBuild + enableMicrobuild: false + # Enable install tasks for MicroBuild on Mac and Linux + # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' + enableMicrobuildForMacAndLinux: false + # Determines whether the ESRP service connection information should be passed to the signing plugin. + # This overlaps with _SignType to some degree. We only need the service connection for real signing. + # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. + # Doing so will cause the service connection to be authorized for the pipeline, which isn't allowed and won't work for non-prod. + # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The + # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough. + microbuildUseESRP: true + # Location of the MicroBuild output folder + # NOTE: There's something that relies on this being in the "default" source directory for tasks such as Signing to work properly. + microBuildOutputFolder: '$(Build.SourcesDirectory)' + + continueOnError: false + +steps: + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, 'true') }}: + # Needed to download the MicroBuild plugin nupkgs on Mac and Linux when nuget.exe is unavailable + - task: UseDotNet@2 + displayName: Install .NET 8.0 SDK for MicroBuild Plugin + inputs: + packageType: sdk + version: 8.0.x + installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet + workingDirectory: ${{ parameters.microBuildOutputFolder }} + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) + + - script: | + REM Check if ESRP is disabled while SignType is real + if /I "${{ parameters.microbuildUseESRP }}"=="false" if /I "$(_SignType)"=="real" ( + echo Error: ESRP must be enabled when SignType is real. + exit /b 1 + ) + displayName: 'Validate ESRP usage (Windows)' + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT')) + - script: | + # Check if ESRP is disabled while SignType is real + if [ "${{ parameters.microbuildUseESRP }}" = "false" ] && [ "$(_SignType)" = "real" ]; then + echo "Error: ESRP must be enabled when SignType is real." + exit 1 + fi + displayName: 'Validate ESRP usage (Non-Windows)' + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) + + # Two different MB install steps. This is due to not being able to use the agent OS during + # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, + # we can avoid including the MB install step if not enabled at all. This avoids a bunch of + # extra pipeline authorizations, since most pipelines do not sign on non-Windows. + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (Windows) + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) + + - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (non-Windows) + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 + ${{ else }}: + ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 0623ac6e112..10f825e270a 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -34,7 +34,9 @@ steps: '$(akams-client-id)' '$(microsoft-symbol-server-pat)' '$(symweb-symbol-server-pat)' + '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' + '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} continueOnError: true condition: always() @@ -45,6 +47,7 @@ steps: SourceFolder: '$(System.DefaultWorkingDirectory)/PostBuildLogs' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/PostBuildLogs' + condition: always() - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index 0718e4ba902..acf16ed3496 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -11,10 +11,6 @@ parameters: # for details. The entire object is described in the 'job' template for simplicity, even though # the usage of the properties on this object is split between the 'job' and 'steps' templates. platform: {} - - # Optional list of directories to ignore for component governance scans. - componentGovernanceIgnoreDirectories: [] - is1ESPipeline: false steps: @@ -23,25 +19,12 @@ steps: set -x df -h - # If file changes are detected, set CopyWipIntoInnerSourceBuildRepo to copy the WIP changes into the inner source build repo. - internalRestoreArgs= - if ! git diff --quiet; then - internalRestoreArgs='/p:CopyWipIntoInnerSourceBuildRepo=true' - # The 'Copy WIP' feature of source build uses git stash to apply changes from the original repo. - # This only works if there is a username/email configured, which won't be the case in most CI runs. - git config --get user.email - if [ $? -ne 0 ]; then - git config user.email dn-bot@microsoft.com - git config user.name dn-bot - fi - fi - # If building on the internal project, the internal storage variable may be available (usually only if needed) # In that case, add variables to allow the download of internal runtimes if the specified versions are not found # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release @@ -50,88 +33,33 @@ steps: buildConfig='$(_BuildConfig)' fi - officialBuildArgs= - if [ '${{ and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}' = 'True' ]; then - officialBuildArgs='/p:DotNetPublishUsingPipelines=true /p:OfficialBuildId=$(BUILD.BUILDNUMBER)' - fi - targetRidArgs= if [ '${{ parameters.platform.targetRID }}' != '' ]; then targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' fi - runtimeOsArgs= - if [ '${{ parameters.platform.runtimeOS }}' != '' ]; then - runtimeOsArgs='/p:RuntimeOS=${{ parameters.platform.runtimeOS }}' - fi - - baseOsArgs= - if [ '${{ parameters.platform.baseOS }}' != '' ]; then - baseOsArgs='/p:BaseOS=${{ parameters.platform.baseOS }}' - fi - - publishArgs= - if [ '${{ parameters.platform.skipPublishValidation }}' != 'true' ]; then - publishArgs='--publish' - fi - - assetManifestFileName=SourceBuild_RidSpecific.xml - if [ '${{ parameters.platform.name }}' != '' ]; then - assetManifestFileName=SourceBuild_${{ parameters.platform.name }}.xml + portableBuildArgs= + if [ '${{ parameters.platform.portableBuild }}' != '' ]; then + portableBuildArgs='/p:PortableBuild=${{ parameters.platform.portableBuild }}' fi ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ --configuration $buildConfig \ - --restore --build --pack $publishArgs -bl \ + --restore --build --pack -bl \ + --source-build \ ${{ parameters.platform.buildArguments }} \ - $officialBuildArgs \ $internalRuntimeDownloadArgs \ - $internalRestoreArgs \ $targetRidArgs \ - $runtimeOsArgs \ - $baseOsArgs \ - /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ - /p:ArcadeBuildFromSource=true \ - /p:DotNetBuildSourceOnly=true \ - /p:DotNetBuildRepo=true \ - /p:AssetManifestFileName=$assetManifestFileName + $portableBuildArgs \ displayName: Build -# Upload build logs for diagnosis. -- task: CopyFiles@2 - displayName: Prepare BuildLogs staging directory - inputs: - SourceFolder: '$(System.DefaultWorkingDirectory)' - Contents: | - **/*.log - **/*.binlog - artifacts/sb/prebuilt-report/** - TargetFolder: '$(Build.StagingDirectory)/BuildLogs' - CleanTargetFolder: true - continueOnError: true - condition: succeededOrFailed() - - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish BuildLogs - targetPath: '$(Build.StagingDirectory)/BuildLogs' + targetPath: artifacts/log/${{ coalesce(variables._BuildConfig, 'Release') }} artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt) continueOnError: true condition: succeededOrFailed() sbomEnabled: false # we don't need SBOM for logs - -# Manually inject component detection so that we can ignore the source build upstream cache, which contains -# a nupkg cache of input packages (a local feed). -# This path must match the upstream cache path in property 'CurrentRepoSourceBuiltNupkgCacheDir' -# in src\Microsoft.DotNet.Arcade.Sdk\tools\SourceBuild\SourceBuildArcade.targets -- template: /eng/common/core-templates/steps/component-governance.yml - parameters: - displayName: Component Detection (Exclude upstream cache) - is1ESPipeline: ${{ parameters.is1ESPipeline }} - ${{ if eq(length(parameters.componentGovernanceIgnoreDirectories), 0) }}: - componentGovernanceIgnoreDirectories: '$(System.DefaultWorkingDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' - ${{ else }}: - componentGovernanceIgnoreDirectories: ${{ join(',', parameters.componentGovernanceIgnoreDirectories) }} - disableComponentGovernance: ${{ eq(variables['System.TeamProject'], 'public') }} diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml new file mode 100644 index 00000000000..e9a694afa58 --- /dev/null +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -0,0 +1,35 @@ +parameters: + sourceIndexUploadPackageVersion: 2.0.0-20250818.1 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250818.1 + sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json + binlogPath: artifacts/log/Debug/Build.binlog + +steps: +- task: UseDotNet@2 + displayName: "Source Index: Use .NET 9 SDK" + inputs: + packageType: sdk + version: 9.0.x + installationPath: $(Agent.TempDirectory)/dotnet + workingDirectory: $(Agent.TempDirectory) + +- script: | + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + displayName: "Source Index: Download netsourceindex Tools" + # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. + workingDirectory: $(Agent.TempDirectory) + +- script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i ${{parameters.BinlogPath}} -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output + displayName: "Source Index: Process Binlog into indexable sln" + +- ${{ if and(ne(parameters.runAsPublic, 'true'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: AzureCLI@2 + displayName: "Source Index: Upload Source Index stage1 artifacts to Azure" + inputs: + azureSubscription: 'SourceDotNet Stage1 Publish' + addSpnToEnvironment: true + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1 diff --git a/eng/common/cross/arm64/tizen/tizen.patch b/eng/common/cross/arm64/tizen/tizen.patch index af7c8be0590..2cebc547382 100644 --- a/eng/common/cross/arm64/tizen/tizen.patch +++ b/eng/common/cross/arm64/tizen/tizen.patch @@ -5,5 +5,5 @@ diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf64-littleaarch64) --GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-aarch64.so.1 ) ) +-GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib64/ld-linux-aarch64.so.1 ) ) +GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux-aarch64.so.1 ) ) diff --git a/eng/common/cross/armel/armel.jessie.patch b/eng/common/cross/armel/armel.jessie.patch deleted file mode 100644 index 2d261561935..00000000000 --- a/eng/common/cross/armel/armel.jessie.patch +++ /dev/null @@ -1,43 +0,0 @@ -diff -u -r a/usr/include/urcu/uatomic/generic.h b/usr/include/urcu/uatomic/generic.h ---- a/usr/include/urcu/uatomic/generic.h 2014-10-22 15:00:58.000000000 -0700 -+++ b/usr/include/urcu/uatomic/generic.h 2020-10-30 21:38:28.550000000 -0700 -@@ -69,10 +69,10 @@ - #endif - #ifdef UATOMIC_HAS_ATOMIC_SHORT - case 2: -- return __sync_val_compare_and_swap_2(addr, old, _new); -+ return __sync_val_compare_and_swap_2((uint16_t*) addr, old, _new); - #endif - case 4: -- return __sync_val_compare_and_swap_4(addr, old, _new); -+ return __sync_val_compare_and_swap_4((uint32_t*) addr, old, _new); - #if (CAA_BITS_PER_LONG == 64) - case 8: - return __sync_val_compare_and_swap_8(addr, old, _new); -@@ -109,7 +109,7 @@ - return; - #endif - case 4: -- __sync_and_and_fetch_4(addr, val); -+ __sync_and_and_fetch_4((uint32_t*) addr, val); - return; - #if (CAA_BITS_PER_LONG == 64) - case 8: -@@ -148,7 +148,7 @@ - return; - #endif - case 4: -- __sync_or_and_fetch_4(addr, val); -+ __sync_or_and_fetch_4((uint32_t*) addr, val); - return; - #if (CAA_BITS_PER_LONG == 64) - case 8: -@@ -187,7 +187,7 @@ - return __sync_add_and_fetch_2(addr, val); - #endif - case 4: -- return __sync_add_and_fetch_4(addr, val); -+ return __sync_add_and_fetch_4((uint32_t*) addr, val); - #if (CAA_BITS_PER_LONG == 64) - case 8: - return __sync_add_and_fetch_8(addr, val); diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh index 7e9ba2b75ed..fbd8d80848a 100755 --- a/eng/common/cross/build-android-rootfs.sh +++ b/eng/common/cross/build-android-rootfs.sh @@ -6,10 +6,11 @@ usage() { echo "Creates a toolchain and sysroot used for cross-compiling for Android." echo - echo "Usage: $0 [BuildArch] [ApiLevel]" + echo "Usage: $0 [BuildArch] [ApiLevel] [--ndk NDKVersion]" echo echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" + echo "NDKVersion is the version of Android NDK. The default is r21. See https://developer.android.com/ndk/downloads/revision_history" echo echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" echo "by setting the TOOLCHAIN_DIR environment variable" @@ -25,10 +26,15 @@ __BuildArch=arm64 __AndroidArch=aarch64 __AndroidToolchain=aarch64-linux-android -for i in "$@" - do - lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" - case $lowerI in +while :; do + if [[ "$#" -le 0 ]]; then + break + fi + + i=$1 + + lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" + case $lowerI in -?|-h|--help) usage exit 1 @@ -43,6 +49,10 @@ for i in "$@" __AndroidArch=arm __AndroidToolchain=arm-linux-androideabi ;; + --ndk) + shift + __NDK_Version=$1 + ;; *[0-9]) __ApiLevel=$i ;; @@ -50,8 +60,17 @@ for i in "$@" __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" ;; esac + shift done +if [[ "$__NDK_Version" == "r21" ]] || [[ "$__NDK_Version" == "r22" ]]; then + __NDK_File_Arch_Spec=-x86_64 + __SysRoot=sysroot +else + __NDK_File_Arch_Spec= + __SysRoot=toolchains/llvm/prebuilt/linux-x86_64/sysroot +fi + # Obtain the location of the bash script to figure out where the root of the repo is. __ScriptBaseDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -78,6 +97,7 @@ fi echo "Target API level: $__ApiLevel" echo "Target architecture: $__BuildArch" +echo "NDK version: $__NDK_Version" echo "NDK location: $__NDK_Dir" echo "Target Toolchain location: $__ToolchainDir" @@ -85,8 +105,8 @@ echo "Target Toolchain location: $__ToolchainDir" if [ ! -d $__NDK_Dir ]; then echo Downloading the NDK into $__NDK_Dir mkdir -p $__NDK_Dir - wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux-x86_64.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip - unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip -d $__CrossDir + wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux$__NDK_File_Arch_Spec.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux.zip + unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux.zip -d $__CrossDir fi if [ ! -d $__lldb_Dir ]; then @@ -116,16 +136,11 @@ for path in $(wget -qO- https://packages.termux.dev/termux-main-21/dists/stable/ fi done -cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/sysroot/usr/" +cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/$__SysRoot/usr/" # Generate platform file for build.sh script to assign to __DistroRid echo "Generating platform file..." -echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/sysroot/android_platform - -echo "Now to build coreclr, libraries and installers; run:" -echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ - --subsetCategory coreclr -echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ - --subsetCategory libraries -echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ - --subsetCategory installer +echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/$__SysRoot/android_platform + +echo "Now to build coreclr, libraries and host; run:" +echo ROOTFS_DIR=$(realpath $__ToolchainDir/$__SysRoot) ./build.sh clr+libs+host --cross --arch $__BuildArch diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 4b5e8d7166b..8abfb71f727 100755 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -5,7 +5,7 @@ set -e usage() { echo "Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir ]" - echo "BuildArch can be: arm(default), arm64, armel, armv6, ppc64le, riscv64, s390x, x64, x86" + echo "BuildArch can be: arm(default), arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64, x86" echo "CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine" echo " for alpine can be specified with version: alpineX.YY or alpineedge" echo " for FreeBSD can be: freebsd13, freebsd14" @@ -15,6 +15,7 @@ usage() echo "llvmx[.y] - optional, LLVM version for LLVM related packages." echo "--skipunmount - optional, will skip the unmount of rootfs folder." echo "--skipsigcheck - optional, will skip package signature checks (allowing untrusted packages)." + echo "--skipemulation - optional, will skip qemu and debootstrap requirement when building environment for debian based systems." echo "--use-mirror - optional, use mirror URL to fetch resources, when available." echo "--jobs N - optional, restrict to N jobs." exit 1 @@ -52,28 +53,27 @@ __UbuntuPackages+=" symlinks" __UbuntuPackages+=" libicu-dev" __UbuntuPackages+=" liblttng-ust-dev" __UbuntuPackages+=" libunwind8-dev" -__UbuntuPackages+=" libnuma-dev" __AlpinePackages+=" gettext-dev" __AlpinePackages+=" icu-dev" __AlpinePackages+=" libunwind-dev" __AlpinePackages+=" lttng-ust-dev" __AlpinePackages+=" compiler-rt" -__AlpinePackages+=" numactl-dev" # runtime libraries' dependencies __UbuntuPackages+=" libcurl4-openssl-dev" __UbuntuPackages+=" libkrb5-dev" __UbuntuPackages+=" libssl-dev" __UbuntuPackages+=" zlib1g-dev" +__UbuntuPackages+=" libbrotli-dev" __AlpinePackages+=" curl-dev" __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" -__FreeBSDBase="13.3-RELEASE" -__FreeBSDPkg="1.17.0" +__FreeBSDBase="13.4-RELEASE" +__FreeBSDPkg="1.21.3" __FreeBSDABI="13" __FreeBSDPackages="libunwind" __FreeBSDPackages+=" icu" @@ -91,18 +91,18 @@ __HaikuPackages="gcc_syslibs" __HaikuPackages+=" gcc_syslibs_devel" __HaikuPackages+=" gmp" __HaikuPackages+=" gmp_devel" -__HaikuPackages+=" icu66" -__HaikuPackages+=" icu66_devel" +__HaikuPackages+=" icu[0-9]+" +__HaikuPackages+=" icu[0-9]*_devel" __HaikuPackages+=" krb5" __HaikuPackages+=" krb5_devel" __HaikuPackages+=" libiconv" __HaikuPackages+=" libiconv_devel" -__HaikuPackages+=" llvm12_libunwind" -__HaikuPackages+=" llvm12_libunwind_devel" +__HaikuPackages+=" llvm[0-9]*_libunwind" +__HaikuPackages+=" llvm[0-9]*_libunwind_devel" __HaikuPackages+=" mpfr" __HaikuPackages+=" mpfr_devel" -__HaikuPackages+=" openssl" -__HaikuPackages+=" openssl_devel" +__HaikuPackages+=" openssl3" +__HaikuPackages+=" openssl3_devel" __HaikuPackages+=" zlib" __HaikuPackages+=" zlib_devel" @@ -128,10 +128,12 @@ __AlpineKeys=' 616adfeb:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0BFD1D4lIxQcsqEpQzU\npNCYM3aP1V/fxxVdT4DWvSI53JHTwHQamKdMWtEXetWVbP5zSROniYKFXd/xrD9X\n0jiGHey3lEtylXRIPxe5s+wXoCmNLcJVnvTcDtwx/ne2NLHxp76lyc25At+6RgE6\nADjLVuoD7M4IFDkAsd8UQ8zM0Dww9SylIk/wgV3ZkifecvgUQRagrNUdUjR56EBZ\nraQrev4hhzOgwelT0kXCu3snbUuNY/lU53CoTzfBJ5UfEJ5pMw1ij6X0r5S9IVsy\nKLWH1hiO0NzU2c8ViUYCly4Fe9xMTFc6u2dy/dxf6FwERfGzETQxqZvSfrRX+GLj\n/QZAXiPg5178hT/m0Y3z5IGenIC/80Z9NCi+byF1WuJlzKjDcF/TU72zk0+PNM/H\nKuppf3JT4DyjiVzNC5YoWJT2QRMS9KLP5iKCSThwVceEEg5HfhQBRT9M6KIcFLSs\nmFjx9kNEEmc1E8hl5IR3+3Ry8G5/bTIIruz14jgeY9u5jhL8Vyyvo41jgt9sLHR1\n/J1TxKfkgksYev7PoX6/ZzJ1ksWKZY5NFoDXTNYUgzFUTOoEaOg3BAQKadb3Qbbq\nXIrxmPBdgrn9QI7NCgfnAY3Tb4EEjs3ON/BNyEhUENcXOH6I1NbcuBQ7g9P73kE4\nVORdoc8MdJ5eoKBpO8Ww8HECAwEAAQ== 616ae350:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyduVzi1mWm+lYo2Tqt/0\nXkCIWrDNP1QBMVPrE0/ZlU2bCGSoo2Z9FHQKz/mTyMRlhNqTfhJ5qU3U9XlyGOPJ\npiM+b91g26pnpXJ2Q2kOypSgOMOPA4cQ42PkHBEqhuzssfj9t7x47ppS94bboh46\nxLSDRff/NAbtwTpvhStV3URYkxFG++cKGGa5MPXBrxIp+iZf9GnuxVdST5PGiVGP\nODL/b69sPJQNbJHVquqUTOh5Ry8uuD2WZuXfKf7/C0jC/ie9m2+0CttNu9tMciGM\nEyKG1/Xhk5iIWO43m4SrrT2WkFlcZ1z2JSf9Pjm4C2+HovYpihwwdM/OdP8Xmsnr\nDzVB4YvQiW+IHBjStHVuyiZWc+JsgEPJzisNY0Wyc/kNyNtqVKpX6dRhMLanLmy+\nf53cCSI05KPQAcGj6tdL+D60uKDkt+FsDa0BTAobZ31OsFVid0vCXtsbplNhW1IF\nHwsGXBTVcfXg44RLyL8Lk/2dQxDHNHzAUslJXzPxaHBLmt++2COa2EI1iWlvtznk\nOk9WP8SOAIj+xdqoiHcC4j72BOVVgiITIJNHrbppZCq6qPR+fgXmXa+sDcGh30m6\n9Wpbr28kLMSHiENCWTdsFij+NQTd5S47H7XTROHnalYDuF1RpS+DpQidT5tUimaT\nJZDr++FjKrnnijbyNF8b98UCAwEAAQ== 616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\n8CM1S15HxV78s9dFntEqIokCAwEAAQ== +66ba20fe:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfB12w4ZgqsXWZDfUAV/\n6Y4aHUKIu3q4SXrNZ7CXF9nXoAVYrS7NAxJdAodsY3vPCN0g5O8DFXR+390LdOuQ\n+HsGKCc1k5tX5ZXld37EZNTNSbR0k+NKhd9h6X3u6wqPOx7SIKxwAQR8qeeFq4pP\nrt9GAGlxtuYgzIIcKJPwE0dZlcBCg+GnptCUZXp/38BP1eYC+xTXSL6Muq1etYfg\nodXdb7Yl+2h1IHuOwo5rjgY5kpY7GcAs8AjGk3lDD/av60OTYccknH0NCVSmPoXK\nvrxDBOn0LQRNBLcAfnTKgHrzy0Q5h4TNkkyTgxkoQw5ObDk9nnabTxql732yy9BY\ns+hM9+dSFO1HKeVXreYSA2n1ndF18YAvAumzgyqzB7I4pMHXq1kC/8bONMJxwSkS\nYm6CoXKyavp7RqGMyeVpRC7tV+blkrrUml0BwNkxE+XnwDRB3xDV6hqgWe0XrifD\nYTfvd9ScZQP83ip0r4IKlq4GMv/R5shcCRJSkSZ6QSGshH40JYSoiwJf5FHbj9ND\n7do0UAqebWo4yNx63j/wb2ULorW3AClv0BCFSdPsIrCStiGdpgJDBR2P2NZOCob3\nG9uMj+wJD6JJg2nWqNJxkANXX37Qf8plgzssrhrgOvB0fjjS7GYhfkfmZTJ0wPOw\nA8+KzFseBh4UFGgue78KwgkCAwEAAQ== ' __Keyring= __KeyringFile="/usr/share/keyrings/ubuntu-archive-keyring.gpg" __SkipSigCheck=0 +__SkipEmulation=0 __UseMirror=0 __UnprocessedBuildArgs= @@ -162,9 +164,13 @@ while :; do armel) __BuildArch=armel __UbuntuArch=armel - __UbuntuRepo="http://ftp.debian.org/debian/" - __CodeName=jessie + __UbuntuRepo="http://archive.debian.org/debian/" + __CodeName=buster __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" + __LLDB_Package="liblldb-6.0-dev" + __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" + __UbuntuPackages="${__UbuntuPackages// libomp5/}" + __UbuntuSuites= ;; armv6) __BuildArch=armv6 @@ -180,6 +186,18 @@ while :; do __Keyring="--keyring $__KeyringFile" fi ;; + loongarch64) + __BuildArch=loongarch64 + __AlpineArch=loongarch64 + __QEMUArch=loongarch64 + __UbuntuArch=loong64 + __UbuntuSuites=unreleased + __LLDB_Package="liblldb-19-dev" + + if [[ "$__CodeName" == "sid" ]]; then + __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" + fi + ;; riscv64) __BuildArch=riscv64 __AlpineArch=riscv64 @@ -264,44 +282,21 @@ while :; do ;; xenial) # Ubuntu 16.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=xenial - fi - ;; - zesty) # Ubuntu 17.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=zesty - fi + __CodeName=xenial ;; bionic) # Ubuntu 18.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=bionic - fi + __CodeName=bionic ;; focal) # Ubuntu 20.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=focal - fi + __CodeName=focal ;; jammy) # Ubuntu 22.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=jammy - fi + __CodeName=jammy ;; noble) # Ubuntu 24.04 - if [[ "$__CodeName" != "jessie" ]]; then - __CodeName=noble - fi - if [[ -n "$__LLDB_Package" ]]; then - __LLDB_Package="liblldb-18-dev" - fi - ;; - jessie) # Debian 8 - __CodeName=jessie - __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" - - if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __CodeName=noble + if [[ -z "$__LLDB_Package" ]]; then + __LLDB_Package="liblldb-19-dev" fi ;; stretch) # Debian 9 @@ -319,7 +314,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __UbuntuRepo="http://archive.debian.org/debian/" fi ;; bullseye) # Debian 11 @@ -340,10 +335,28 @@ while :; do ;; sid) # Debian sid __CodeName=sid - __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" + __UbuntuSuites= - if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + # Debian-Ports architectures need different values + case "$__UbuntuArch" in + amd64|arm64|armel|armhf|i386|mips64el|ppc64el|riscv64|s390x) + __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + *) + __KeyringFile="/usr/share/keyrings/debian-ports-archive-keyring.gpg" + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" + fi + ;; + esac + + if [[ -e "$__KeyringFile" ]]; then + __Keyring="--keyring $__KeyringFile" fi ;; tizen) @@ -370,7 +383,7 @@ while :; do ;; freebsd14) __CodeName=freebsd - __FreeBSDBase="14.0-RELEASE" + __FreeBSDBase="14.2-RELEASE" __FreeBSDABI="14" __SkipUnmount=1 ;; @@ -388,6 +401,9 @@ while :; do --skipsigcheck) __SkipSigCheck=1 ;; + --skipemulation) + __SkipEmulation=1 + ;; --rootfsdir|-rootfsdir) shift __RootfsDir="$1" @@ -420,16 +436,15 @@ case "$__AlpineVersion" in elif [[ "$__AlpineArch" == "x86" ]]; then __AlpineVersion=3.17 # minimum version that supports lldb-dev __AlpinePackages+=" llvm15-libs" - elif [[ "$__AlpineArch" == "riscv64" ]]; then + elif [[ "$__AlpineArch" == "riscv64" || "$__AlpineArch" == "loongarch64" ]]; then + __AlpineVersion=3.21 # minimum version that supports lldb-dev + __AlpinePackages+=" llvm19-libs" + elif [[ -n "$__AlpineMajorVersion" ]]; then + # use whichever alpine version is provided and select the latest toolchain libs __AlpineLlvmLibsLookup=1 - __AlpineVersion=edge # minimum version with APKINDEX.tar.gz (packages archive) else __AlpineVersion=3.13 # 3.13 to maximize compatibility __AlpinePackages+=" llvm10-libs" - - if [[ "$__AlpineArch" == "armv7" ]]; then - __AlpinePackages="${__AlpinePackages//numactl-dev/}" - fi fi esac @@ -439,15 +454,6 @@ if [[ "$__AlpineVersion" =~ 3\.1[345] ]]; then __AlpinePackages="${__AlpinePackages/compiler-rt/compiler-rt-static}" fi -if [[ "$__BuildArch" == "armel" ]]; then - __LLDB_Package="lldb-3.5-dev" -fi - -if [[ "$__CodeName" == "xenial" && "$__UbuntuArch" == "armhf" ]]; then - # libnuma-dev is not available on armhf for xenial - __UbuntuPackages="${__UbuntuPackages//libnuma-dev/}" -fi - __UbuntuPackages+=" ${__LLDB_Package:-}" if [[ -z "$__UbuntuRepo" ]]; then @@ -496,7 +502,7 @@ if [[ "$__CodeName" == "alpine" ]]; then arch="$(uname -m)" ensureDownloadTool - + if [[ "$__hasWget" == 1 ]]; then wget -P "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static" else @@ -512,11 +518,6 @@ if [[ "$__CodeName" == "alpine" ]]; then echo "$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static" | sha512sum -c chmod +x "$__ApkToolsDir/apk.static" - if [[ -f "/usr/bin/qemu-$__QEMUArch-static" ]]; then - mkdir -p "$__RootfsDir"/usr/bin - cp -v "/usr/bin/qemu-$__QEMUArch-static" "$__RootfsDir/usr/bin" - fi - if [[ "$__AlpineVersion" == "edge" ]]; then version=edge else @@ -536,6 +537,10 @@ if [[ "$__CodeName" == "alpine" ]]; then __ApkSignatureArg="--keys-dir $__ApkKeysDir" fi + if [[ "$__SkipEmulation" == "1" ]]; then + __NoEmulationArg="--no-scripts" + fi + # initialize DB # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ @@ -557,7 +562,7 @@ if [[ "$__CodeName" == "alpine" ]]; then "$__ApkToolsDir/apk.static" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ - -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ + -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" $__NoEmulationArg \ add $__AlpinePackages rm -r "$__ApkToolsDir" @@ -573,7 +578,7 @@ elif [[ "$__CodeName" == "freebsd" ]]; then curl -SL "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version fi echo "ABI = \"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\"; FINGERPRINTS = \"${__RootfsDir}/usr/share/keys\"; REPOS_DIR = [\"${__RootfsDir}/etc/pkg\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;" > "${__RootfsDir}"/usr/local/etc/pkg.conf - echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"${__RootfsDir}/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf + echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf mkdir -p "$__RootfsDir"/tmp # get and build package manager if [[ "$__hasWget" == 1 ]]; then @@ -681,7 +686,7 @@ elif [[ "$__CodeName" == "haiku" ]]; then ensureDownloadTool - echo "Downloading Haiku package tool" + echo "Downloading Haiku package tools" git clone https://github.com/haiku/haiku-toolchains-ubuntu --depth 1 "$__RootfsDir/tmp/script" if [[ "$__hasWget" == 1 ]]; then wget -O "$__RootfsDir/tmp/download/hosttools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --hosttools)" @@ -691,34 +696,42 @@ elif [[ "$__CodeName" == "haiku" ]]; then unzip -o "$__RootfsDir/tmp/download/hosttools.zip" -d "$__RootfsDir/tmp/bin" - DepotBaseUrl="https://depot.haiku-os.org/__api/v2/pkg/get-pkg" - HpkgBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" + HaikuBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" + HaikuPortsBaseUrl="https://eu.hpkg.haiku-os.org/haikuports/master/$__HaikuArch/current" + + echo "Downloading HaikuPorts package repository index..." + if [[ "$__hasWget" == 1 ]]; then + wget -P "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" + else + curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" + fi - # Download Haiku packages echo "Downloading Haiku packages" read -ra array <<<"$__HaikuPackages" for package in "${array[@]}"; do echo "Downloading $package..." - # API documented here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L60 - # The schema here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L598 + hpkgFilename="$(LD_LIBRARY_PATH="$__RootfsDir/tmp/bin" "$__RootfsDir/tmp/bin/package_repo" list -f "$__RootfsDir/tmp/download/repo" | + grep -E "${package}-" | sort -V | tail -n 1 | xargs)" + if [ -z "$hpkgFilename" ]; then + >&2 echo "ERROR: package $package missing." + exit 1 + fi + echo "Resolved filename: $hpkgFilename..." + hpkgDownloadUrl="$HaikuPortsBaseUrl/packages/$hpkgFilename" if [[ "$__hasWget" == 1 ]]; then - hpkgDownloadUrl="$(wget -qO- --post-data '{"name":"'"$package"'","repositorySourceCode":"haikuports_'$__HaikuArch'","versionType":"LATEST","naturalLanguageCode":"en"}' \ - --header 'Content-Type:application/json' "$DepotBaseUrl" | jq -r '.result.versions[].hpkgDownloadURL')" wget -P "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" else - hpkgDownloadUrl="$(curl -sSL -XPOST --data '{"name":"'"$package"'","repositorySourceCode":"haikuports_'$__HaikuArch'","versionType":"LATEST","naturalLanguageCode":"en"}' \ - --header 'Content-Type:application/json' "$DepotBaseUrl" | jq -r '.result.versions[].hpkgDownloadURL')" curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" fi done for package in haiku haiku_devel; do echo "Downloading $package..." if [[ "$__hasWget" == 1 ]]; then - hpkgVersion="$(wget -qO- "$HpkgBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" - wget -P "$__RootfsDir/tmp/download" "$HpkgBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" + hpkgVersion="$(wget -qO- "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" + wget -P "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" else - hpkgVersion="$(curl -sSL "$HpkgBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" - curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HpkgBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" + hpkgVersion="$(curl -sSL "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" + curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" fi done @@ -744,25 +757,67 @@ elif [[ "$__CodeName" == "haiku" ]]; then popd rm -rf "$__RootfsDir/tmp" elif [[ -n "$__CodeName" ]]; then + __Suites="$__CodeName $(for suite in $__UbuntuSuites; do echo -n "$__CodeName-$suite "; done)" + + if [[ "$__SkipEmulation" == "1" ]]; then + if [[ -z "$AR" ]]; then + if command -v ar &>/dev/null; then + AR="$(command -v ar)" + elif command -v llvm-ar &>/dev/null; then + AR="$(command -v llvm-ar)" + else + echo "Unable to find ar or llvm-ar on PATH, add them to PATH or set AR environment variable pointing to the available AR tool" + exit 1 + fi + fi + + PYTHON=${PYTHON_EXECUTABLE:-python3} + + # shellcheck disable=SC2086,SC2046 + echo running "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ + $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ + $__UbuntuPackages + + # shellcheck disable=SC2086,SC2046 + "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ + $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ + $__UbuntuPackages + exit 0 + fi + + __UpdateOptions= if [[ "$__SkipSigCheck" == "0" ]]; then __Keyring="$__Keyring --force-check-gpg" + else + __Keyring= + __UpdateOptions="--allow-unauthenticated --allow-insecure-repositories" fi # shellcheck disable=SC2086 echo running debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" - debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" + # shellcheck disable=SC2086 + if ! debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo"; then + echo "debootstrap failed! dumping debootstrap.log" + cat "$__RootfsDir/debootstrap/debootstrap.log" + exit 1 + fi + + rm -rf "$__RootfsDir"/etc/apt/*.{sources,list} "$__RootfsDir"/etc/apt/sources.list.d mkdir -p "$__RootfsDir/etc/apt/sources.list.d/" + + # shellcheck disable=SC2086 cat > "$__RootfsDir/etc/apt/sources.list.d/$__CodeName.sources" < token2) - (token1 < token2) + else: + return -1 if isinstance(token1, str) else 1 + + return len(tokens1) - len(tokens2) + +def compare_debian_versions(version1, version2): + """Compare two Debian package versions.""" + epoch1, upstream1, revision1 = parse_debian_version(version1) + epoch2, upstream2, revision2 = parse_debian_version(version2) + + if epoch1 != epoch2: + return epoch1 - epoch2 + + result = compare_upstream_version(upstream1, upstream2) + if result != 0: + return result + + return compare_upstream_version(revision1, revision2) + +def resolve_dependencies(packages, aliases, desired_packages): + """Recursively resolves dependencies for the desired packages.""" + resolved = [] + to_process = deque(desired_packages) + + while to_process: + current = to_process.popleft() + resolved_package = current if current in packages else aliases.get(current, [None])[0] + + if not resolved_package: + print(f"Error: Package '{current}' was not found in the available packages.") + sys.exit(1) + + if resolved_package not in resolved: + resolved.append(resolved_package) + + deps = packages.get(resolved_package, {}).get("Depends", "") + if deps: + deps = [dep.split(' ')[0] for dep in deps.split(', ') if dep] + for dep in deps: + if dep not in resolved and dep not in to_process and dep in packages: + to_process.append(dep) + + return resolved + +def parse_package_index(content): + """Parses the Packages.gz file and returns package information.""" + packages = {} + aliases = {} + entries = re.split(r'\n\n+', content) + + for entry in entries: + fields = dict(re.findall(r'^(\S+): (.+)$', entry, re.MULTILINE)) + if "Package" in fields: + package_name = fields["Package"] + version = fields.get("Version") + filename = fields.get("Filename") + depends = fields.get("Depends") + provides = fields.get("Provides", None) + + # Only update if package_name is not in packages or if the new version is higher + if package_name not in packages or compare_debian_versions(version, packages[package_name]["Version"]) > 0: + packages[package_name] = { + "Version": version, + "Filename": filename, + "Depends": depends + } + + # Update aliases if package provides any alternatives + if provides: + provides_list = [x.strip() for x in provides.split(",")] + for alias in provides_list: + # Strip version specifiers + alias_name = re.sub(r'\s*\(=.*\)', '', alias) + if alias_name not in aliases: + aliases[alias_name] = [] + if package_name not in aliases[alias_name]: + aliases[alias_name].append(package_name) + + return packages, aliases + +def install_packages(mirror, packages_info, aliases, tmp_dir, extract_dir, ar_tool, desired_packages): + """Downloads .deb files and extracts them.""" + resolved_packages = resolve_dependencies(packages_info, aliases, desired_packages) + print(f"Resolved packages (including dependencies): {resolved_packages}") + + packages_to_download = {} + + for pkg in resolved_packages: + if pkg in packages_info: + packages_to_download[pkg] = packages_info[pkg] + + if pkg in aliases: + for alias in aliases[pkg]: + if alias in packages_info: + packages_to_download[alias] = packages_info[alias] + + asyncio.run(download_deb_files_parallel(mirror, packages_to_download, tmp_dir)) + + package_to_deb_file_map = {} + for pkg in resolved_packages: + pkg_info = packages_info.get(pkg) + if pkg_info: + deb_filename = pkg_info.get("Filename") + if deb_filename: + deb_file_path = os.path.join(tmp_dir, os.path.basename(deb_filename)) + package_to_deb_file_map[pkg] = deb_file_path + + for pkg in reversed(resolved_packages): + deb_file = package_to_deb_file_map.get(pkg) + if deb_file and os.path.exists(deb_file): + extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool) + + print("All done!") + +def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): + """Extract .deb file contents""" + + os.makedirs(extract_dir, exist_ok=True) + + with tempfile.TemporaryDirectory(dir=tmp_dir) as tmp_subdir: + result = subprocess.run(f"{ar_tool} t {os.path.abspath(deb_file)}", cwd=tmp_subdir, check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + tar_filename = None + for line in result.stdout.decode().splitlines(): + if line.startswith("data.tar"): + tar_filename = line.strip() + break + + if not tar_filename: + raise FileNotFoundError(f"Could not find 'data.tar.*' in {deb_file}.") + + tar_file_path = os.path.join(tmp_subdir, tar_filename) + print(f"Extracting {tar_filename} from {deb_file}..") + + subprocess.run(f"{ar_tool} p {os.path.abspath(deb_file)} {tar_filename} > {tar_file_path}", check=True, shell=True) + + file_extension = os.path.splitext(tar_file_path)[1].lower() + + if file_extension == ".xz": + mode = "r:xz" + elif file_extension == ".gz": + mode = "r:gz" + elif file_extension == ".zst": + # zstd is not supported by standard library yet + decompressed_tar_path = tar_file_path.replace(".zst", "") + with open(tar_file_path, "rb") as zst_file, open(decompressed_tar_path, "wb") as decompressed_file: + dctx = zstandard.ZstdDecompressor() + dctx.copy_stream(zst_file, decompressed_file) + + tar_file_path = decompressed_tar_path + mode = "r" + else: + raise ValueError(f"Unsupported compression format: {file_extension}") + + with tarfile.open(tar_file_path, mode) as tar: + tar.extractall(path=extract_dir, filter='fully_trusted') + +def finalize_setup(rootfsdir): + lib_dir = os.path.join(rootfsdir, 'lib') + usr_lib_dir = os.path.join(rootfsdir, 'usr', 'lib') + + if os.path.exists(lib_dir): + if os.path.islink(lib_dir): + os.remove(lib_dir) + else: + os.makedirs(usr_lib_dir, exist_ok=True) + + for item in os.listdir(lib_dir): + src = os.path.join(lib_dir, item) + dest = os.path.join(usr_lib_dir, item) + + if os.path.isdir(src): + shutil.copytree(src, dest, dirs_exist_ok=True) + else: + shutil.copy2(src, dest) + + shutil.rmtree(lib_dir) + + os.symlink(usr_lib_dir, lib_dir) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate rootfs for .NET runtime on Debian-like OS") + parser.add_argument("--distro", required=False, help="Distro name (e.g., debian, ubuntu, etc.)") + parser.add_argument("--arch", required=True, help="Architecture (e.g., amd64, loong64, etc.)") + parser.add_argument("--rootfsdir", required=True, help="Destination directory.") + parser.add_argument('--suite', required=True, action='append', help='Specify one or more repository suites to collect index data.') + parser.add_argument("--mirror", required=False, help="Mirror (e.g., http://ftp.debian.org/debian-ports etc.)") + parser.add_argument("--artool", required=False, default="ar", help="ar tool to extract debs (e.g., ar, llvm-ar etc.)") + parser.add_argument("packages", nargs="+", help="List of package names to be installed.") + + args = parser.parse_args() + + if args.mirror is None: + if args.distro == "ubuntu": + args.mirror = "http://archive.ubuntu.com/ubuntu" if args.arch in ["amd64", "i386"] else "http://ports.ubuntu.com/ubuntu-ports" + elif args.distro == "debian": + args.mirror = "http://ftp.debian.org/debian-ports" + else: + raise Exception("Unsupported distro") + + DESIRED_PACKAGES = args.packages + [ # base packages + "dpkg", + "busybox", + "libc-bin", + "base-files", + "base-passwd", + "debianutils" + ] + + print(f"Creating rootfs. rootfsdir: {args.rootfsdir}, distro: {args.distro}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}") + + package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite)) + + packages_info, aliases = parse_package_index(package_index_content) + + with tempfile.TemporaryDirectory() as tmp_dir: + install_packages(args.mirror, packages_info, aliases, tmp_dir, args.rootfsdir, args.artool, DESIRED_PACKAGES) + + finalize_setup(args.rootfsdir) diff --git a/eng/common/cross/tizen-fetch.sh b/eng/common/cross/tizen-fetch.sh index 28936ceef3a..37c3a61f1de 100755 --- a/eng/common/cross/tizen-fetch.sh +++ b/eng/common/cross/tizen-fetch.sh @@ -156,13 +156,8 @@ fetch_tizen_pkgs() done } -if [ "$TIZEN_ARCH" == "riscv64" ]; then - BASE="Tizen-Base-RISCV" - UNIFIED="Tizen-Unified-RISCV" -else - BASE="Tizen-Base" - UNIFIED="Tizen-Unified" -fi +BASE="Tizen-Base" +UNIFIED="Tizen-Unified" Inform "Initialize ${TIZEN_ARCH} base" fetch_tizen_pkgs_init standard $BASE diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index 9a7ecfbd42c..0ff85cf0367 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -67,6 +67,13 @@ elseif(TARGET_ARCH_NAME STREQUAL "armv6") else() set(TOOLCHAIN "arm-linux-gnueabihf") endif() +elseif(TARGET_ARCH_NAME STREQUAL "loongarch64") + set(CMAKE_SYSTEM_PROCESSOR "loongarch64") + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/loongarch64-alpine-linux-musl) + set(TOOLCHAIN "loongarch64-alpine-linux-musl") + else() + set(TOOLCHAIN "loongarch64-linux-gnu") + endif() elseif(TARGET_ARCH_NAME STREQUAL "ppc64le") set(CMAKE_SYSTEM_PROCESSOR ppc64le) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl) @@ -118,7 +125,7 @@ elseif(TARGET_ARCH_NAME STREQUAL "x86") set(TIZEN_TOOLCHAIN "i586-tizen-linux-gnu") endif() else() - message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, ppc64le, riscv64, s390x, x64 and x86 are supported!") + message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64 and x86 are supported!") endif() if(DEFINED ENV{TOOLCHAIN}) @@ -148,6 +155,25 @@ if(TIZEN) include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++/${TIZEN_TOOLCHAIN}) endif() +function(locate_toolchain_exec exec var) + set(TOOLSET_PREFIX ${TOOLCHAIN}-) + string(TOUPPER ${exec} EXEC_UPPERCASE) + if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") + set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) + return() + endif() + + find_program(EXEC_LOCATION_${exec} + NAMES + "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" + "${TOOLSET_PREFIX}${exec}") + + if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") + message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") + endif() + set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) +endfunction() + if(ANDROID) if(TARGET_ARCH_NAME STREQUAL "arm") set(ANDROID_ABI armeabi-v7a) @@ -178,66 +204,24 @@ elseif(FREEBSD) set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld") elseif(ILLUMOS) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") + set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") include_directories(SYSTEM ${CROSS_ROOTFS}/include) - set(TOOLSET_PREFIX ${TOOLCHAIN}-) - function(locate_toolchain_exec exec var) - string(TOUPPER ${exec} EXEC_UPPERCASE) - if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") - set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) - return() - endif() - - find_program(EXEC_LOCATION_${exec} - NAMES - "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" - "${TOOLSET_PREFIX}${exec}") - - if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") - message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") - endif() - set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) - endfunction() - - set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") - locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) - - set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") - set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") elseif(HAIKU) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_PROGRAM_PATH "${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin") - - set(TOOLSET_PREFIX ${TOOLCHAIN}-) - function(locate_toolchain_exec exec var) - string(TOUPPER ${exec} EXEC_UPPERCASE) - if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") - set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) - return() - endif() - - find_program(EXEC_LOCATION_${exec} - NAMES - "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" - "${TOOLSET_PREFIX}${exec}") - - if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") - message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") - endif() - set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) - endfunction() - set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) - set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") - set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") - # let CMake set up the correct search paths include(Platform/Haiku) else() @@ -307,7 +291,7 @@ endif() # Specify compile options -if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) +if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index 36dbd45e1ce..e889f439b8d 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -68,7 +68,7 @@ function InstallDarcCli { fi fi - local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" + local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." diff --git a/eng/common/dotnet.cmd b/eng/common/dotnet.cmd new file mode 100644 index 00000000000..527fa4bb38f --- /dev/null +++ b/eng/common/dotnet.cmd @@ -0,0 +1,7 @@ +@echo off + +:: This script is used to install the .NET SDK. +:: It will also invoke the SDK with any provided arguments. + +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet.ps1""" %*" +exit /b %ErrorLevel% diff --git a/eng/common/dotnet.ps1 b/eng/common/dotnet.ps1 new file mode 100644 index 00000000000..45e5676c9eb --- /dev/null +++ b/eng/common/dotnet.ps1 @@ -0,0 +1,11 @@ +# This script is used to install the .NET SDK. +# It will also invoke the SDK with any provided arguments. + +. $PSScriptRoot\tools.ps1 +$dotnetRoot = InitializeDotNetCli -install:$true + +# Invoke acquired SDK with args if they are provided +if ($args.count -gt 0) { + $env:DOTNET_NOLOGO=1 + & "$dotnetRoot\dotnet.exe" $args +} diff --git a/eng/common/dotnet.sh b/eng/common/dotnet.sh new file mode 100644 index 00000000000..2ef68235675 --- /dev/null +++ b/eng/common/dotnet.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# This script is used to install the .NET SDK. +# It will also invoke the SDK with any provided arguments. + +source="${BASH_SOURCE[0]}" +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +source $scriptroot/tools.sh +InitializeDotNetCli true # install + +# Invoke acquired SDK with args if they are provided +if [[ $# > 0 ]]; then + __dotnetDir=${_InitializeDotNetCli} + dotnetPath=${__dotnetDir}/dotnet + ${dotnetPath} "$@" +fi diff --git a/eng/common/generate-locproject.ps1 b/eng/common/generate-locproject.ps1 index 524aaa57f2b..fa1cdc2b300 100644 --- a/eng/common/generate-locproject.ps1 +++ b/eng/common/generate-locproject.ps1 @@ -33,15 +33,27 @@ $jsonTemplateFiles | ForEach-Object { $jsonWinformsTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\\strings\.json" } # current winforms pattern +$wxlFilesV3 = @() +$wxlFilesV5 = @() $wxlFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\.+\.wxl" -And -Not( $_.Directory.Name -Match "\d{4}" ) } # localized files live in four digit lang ID directories; this excludes them if (-not $wxlFiles) { $wxlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\1033\\.+\.wxl" } # pick up en files (1033 = en) specifically so we can copy them to use as the neutral xlf files if ($wxlEnFiles) { - $wxlFiles = @() - $wxlEnFiles | ForEach-Object { - $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" - $wxlFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru - } + $wxlFiles = @() + $wxlEnFiles | ForEach-Object { + $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" + $content = Get-Content $_.FullName -Raw + + # Split files on schema to select different parser settings in the generated project. + if ($content -like "*http://wixtoolset.org/schemas/v4/wxl*") + { + $wxlFilesV5 += Copy-Item $_.FullName -Destination $destinationFile -PassThru + } + elseif ($content -like "*http://schemas.microsoft.com/wix/2006/localization*") + { + $wxlFilesV3 += Copy-Item $_.FullName -Destination $destinationFile -PassThru + } + } } } @@ -114,7 +126,32 @@ $locJson = @{ CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "wxl_loc.lss" ) LocItems = @( - $wxlFiles | ForEach-Object { + $wxlFilesV3 | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } + } + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if ($continue) + { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } + } + } + ) + }, + @{ + LanguageSet = $LanguageSet + CloneLanguageSet = "WiX_CloneLanguages" + LssFiles = @( "P210WxlSchemaV4.lss" ) + LocItems = @( + $wxlFilesV5 | ForEach-Object { $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh new file mode 100644 index 00000000000..477a44f335b --- /dev/null +++ b/eng/common/native/install-dependencies.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +set -e + +# This is a simple script primarily used for CI to install necessary dependencies +# +# Usage: +# +# ./install-dependencies.sh + +os="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + +if [ -z "$os" ]; then + . "$(dirname "$0")"/init-os-and-arch.sh +fi + +case "$os" in + linux) + if [ -e /etc/os-release ]; then + . /etc/os-release + fi + + if [ "$ID" = "debian" ] || [ "$ID_LIKE" = "debian" ]; then + apt update + + apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \ + libssl-dev libkrb5-dev pigz cpio + + localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 + elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then + pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" + $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio + elif [ "$ID" = "alpine" ]; then + apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio + else + echo "Unsupported distro. distro: $ID" + exit 1 + fi + ;; + + osx|maccatalyst|ios|iossimulator|tvos|tvossimulator) + echo "Installed xcode version: $(xcode-select -p)" + + export HOMEBREW_NO_INSTALL_CLEANUP=1 + export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 + # Skip brew update for now, see https://github.com/actions/setup-python/issues/577 + # brew update --preinstall + brew bundle --no-upgrade --file=- < Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host " -excludeCIBinaryLog When running on CI, allow no binary log (short: -nobl)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." } @@ -34,10 +37,11 @@ function Print-Usage() { function Build([string]$target) { $logSuffix = if ($target -eq 'Execute') { '' } else { ".$target" } $log = Join-Path $LogDir "$task$logSuffix.binlog" + $binaryLogArg = if ($binaryLog) { "/bl:$log" } else { "" } $outputPath = Join-Path $ToolsetDir "$task\" MSBuild $taskProject ` - /bl:$log ` + $binaryLogArg ` /t:$target ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` @@ -64,7 +68,7 @@ try { $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty } if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { - $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.12.0" -MemberType NoteProperty + $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.13.0" -MemberType NoteProperty } if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true diff --git a/eng/common/sdk-task.sh b/eng/common/sdk-task.sh new file mode 100644 index 00000000000..3270f83fa9a --- /dev/null +++ b/eng/common/sdk-task.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +show_usage() { + echo "Common settings:" + echo " --task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" + echo " --restore Restore dependencies" + echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" + echo " --help Print help and exit" + echo "" + + echo "Advanced settings:" + echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" + echo " --noWarnAsError Do not warn as error" + echo "" + echo "Command line arguments not listed above are passed thru to msbuild." +} + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +Build() { + local target=$1 + local log_suffix="" + [[ "$target" != "Execute" ]] && log_suffix=".$target" + local log="$log_dir/$task$log_suffix.binlog" + local binaryLogArg="" + [[ $binary_log == true ]] && binaryLogArg="/bl:$log" + local output_path="$toolset_dir/$task/" + + MSBuild "$taskProject" \ + $binaryLogArg \ + /t:"$target" \ + /p:Configuration="$configuration" \ + /p:RepoRoot="$repo_root" \ + /p:BaseIntermediateOutputPath="$output_path" \ + /v:"$verbosity" \ + $properties +} + +binary_log=true +configuration="Debug" +verbosity="minimal" +exclude_ci_binary_log=false +restore=false +help=false +properties='' +warnAsError=true + +while (($# > 0)); do + lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" + case $lowerI in + --task) + task=$2 + shift 2 + ;; + --restore) + restore=true + shift 1 + ;; + --verbosity) + verbosity=$2 + shift 2 + ;; + --excludecibinarylog|--nobl) + binary_log=false + exclude_ci_binary_log=true + shift 1 + ;; + --noWarnAsError) + warnAsError=false + shift 1 + ;; + --help) + help=true + shift 1 + ;; + *) + properties="$properties $1" + shift 1 + ;; + esac +done + +ci=true + +if $help; then + show_usage + exit 0 +fi + +. "$scriptroot/tools.sh" +InitializeToolset + +if [[ -z "$task" ]]; then + Write-PipelineTelemetryError -Category 'Task' -Name 'MissingTask' -Message "Missing required parameter '-task '" + ExitWithExitCode 1 +fi + +taskProject=$(GetSdkTaskProject "$task") +if [[ ! -e "$taskProject" ]]; then + Write-PipelineTelemetryError -Category 'Task' -Name 'UnknownTask' -Message "Unknown task: $task" + ExitWithExitCode 1 +fi + +if $restore; then + Build "Restore" +fi + +Build "Execute" + + +ExitWithExitCode 0 diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config index 4585cfd6bba..e5f543ea68c 100644 --- a/eng/common/sdl/packages.config +++ b/eng/common/sdl/packages.config @@ -1,4 +1,4 @@ - + diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml index 81ea7a261f2..92a0664f564 100644 --- a/eng/common/templates-official/job/job.yml +++ b/eng/common/templates-official/job/job.yml @@ -31,6 +31,7 @@ jobs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} condition: always() + retryCountOnTaskFailure: 10 # for any logs being locked continueOnError: true - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - output: pipelineArtifact @@ -39,6 +40,7 @@ jobs: displayName: 'Publish logs' continueOnError: true condition: always() + retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if eq(parameters.enablePublishBuildArtifacts, true) }}: @@ -46,7 +48,7 @@ jobs: displayName: Publish Logs PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container - ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} + ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} continueOnError: true condition: always() sbomEnabled: false # we don't need SBOM for logs diff --git a/eng/common/templates-official/steps/publish-build-artifacts.yml b/eng/common/templates-official/steps/publish-build-artifacts.yml index 100a3fc9849..fcf6637b2eb 100644 --- a/eng/common/templates-official/steps/publish-build-artifacts.yml +++ b/eng/common/templates-official/steps/publish-build-artifacts.yml @@ -24,6 +24,10 @@ parameters: - name: is1ESPipeline type: boolean default: true + +- name: retryCountOnTaskFailure + type: string + default: 10 steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: @@ -38,4 +42,5 @@ steps: PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: ArtifactName: ${{ parameters.artifactName }} - + ${{ if parameters.retryCountOnTaskFailure }}: + retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} diff --git a/eng/common/templates-official/steps/source-index-stage1-publish.yml b/eng/common/templates-official/steps/source-index-stage1-publish.yml new file mode 100644 index 00000000000..9b8b80942b5 --- /dev/null +++ b/eng/common/templates-official/steps/source-index-stage1-publish.yml @@ -0,0 +1,7 @@ +steps: +- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml + parameters: + is1ESPipeline: true + + ${{ each parameter in parameters }}: + ${{ parameter.key }}: ${{ parameter.value }} diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index 5bdd3dd85fd..238fa0818f7 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -46,6 +46,7 @@ jobs: artifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} continueOnError: true condition: always() + retryCountOnTaskFailure: 10 # for any logs being locked - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: @@ -56,6 +57,7 @@ jobs: displayName: 'Publish logs' continueOnError: true condition: always() + retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}: @@ -66,7 +68,7 @@ jobs: displayName: Publish Logs pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container - artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} + artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} continueOnError: true condition: always() diff --git a/eng/common/templates/steps/publish-build-artifacts.yml b/eng/common/templates/steps/publish-build-artifacts.yml index 6428a98dfef..605e602e94d 100644 --- a/eng/common/templates/steps/publish-build-artifacts.yml +++ b/eng/common/templates/steps/publish-build-artifacts.yml @@ -25,6 +25,10 @@ parameters: type: string default: 'Container' +- name: retryCountOnTaskFailure + type: string + default: 10 + steps: - ${{ if eq(parameters.is1ESPipeline, true) }}: - 'eng/common/templates cannot be referenced from a 1ES managed template': error @@ -37,4 +41,6 @@ steps: PublishLocation: ${{ parameters.publishLocation }} PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: - ArtifactName: ${{ parameters.artifactName }} \ No newline at end of file + ArtifactName: ${{ parameters.artifactName }} + ${{ if parameters.retryCountOnTaskFailure }}: + retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} diff --git a/eng/common/templates/steps/source-index-stage1-publish.yml b/eng/common/templates/steps/source-index-stage1-publish.yml new file mode 100644 index 00000000000..182cec33a7b --- /dev/null +++ b/eng/common/templates/steps/source-index-stage1-publish.yml @@ -0,0 +1,7 @@ +steps: +- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml + parameters: + is1ESPipeline: false + + ${{ each parameter in parameters }}: + ${{ parameter.key }}: ${{ parameter.value }} diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml new file mode 100644 index 00000000000..599afb6186b --- /dev/null +++ b/eng/common/templates/steps/vmr-sync.yml @@ -0,0 +1,207 @@ +### These steps synchronize new code from product repositories into the VMR (https://github.com/dotnet/dotnet). +### They initialize the darc CLI and pull the new updates. +### Changes are applied locally onto the already cloned VMR (located in $vmrPath). + +parameters: +- name: targetRef + displayName: Target revision in dotnet/ to synchronize + type: string + default: $(Build.SourceVersion) + +- name: vmrPath + displayName: Path where the dotnet/dotnet is checked out to + type: string + default: $(Agent.BuildDirectory)/vmr + +- name: additionalSyncs + displayName: Optional list of package names whose repo's source will also be synchronized in the local VMR, e.g. NuGet.Protocol + type: object + default: [] + +steps: +- checkout: vmr + displayName: Clone dotnet/dotnet + path: vmr + clean: true + +- checkout: self + displayName: Clone $(Build.Repository.Name) + path: repo + fetchDepth: 0 + +# This step is needed so that when we get a detached HEAD / shallow clone, +# we still pull the commit into the temporary repo clone to use it during the sync. +# Also unshallow the clone so that forwardflow command would work. +- script: | + git branch repo-head + git rev-parse HEAD + displayName: Label PR commit + workingDirectory: $(Agent.BuildDirectory)/repo + +- script: | + vmr_sha=$(grep -oP '(?<=Sha=")[^"]*' $(Agent.BuildDirectory)/repo/eng/Version.Details.xml) + echo "##vso[task.setvariable variable=vmr_sha]$vmr_sha" + displayName: Obtain the vmr sha from Version.Details.xml (Unix) + condition: ne(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo + +- powershell: | + [xml]$xml = Get-Content -Path $(Agent.BuildDirectory)/repo/eng/Version.Details.xml + $vmr_sha = $xml.SelectSingleNode("//Source").Sha + Write-Output "##vso[task.setvariable variable=vmr_sha]$vmr_sha" + displayName: Obtain the vmr sha from Version.Details.xml (Windows) + condition: eq(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo + +- script: | + git fetch --all + git checkout $(vmr_sha) + displayName: Checkout VMR at correct sha for repo flow + workingDirectory: ${{ parameters.vmrPath }} + +- script: | + git config --global user.name "dotnet-maestro[bot]" + git config --global user.email "dotnet-maestro[bot]@users.noreply.github.com" + displayName: Set git author to dotnet-maestro[bot] + workingDirectory: ${{ parameters.vmrPath }} + +- script: | + ./eng/common/vmr-sync.sh \ + --vmr ${{ parameters.vmrPath }} \ + --tmp $(Agent.TempDirectory) \ + --azdev-pat '$(dn-bot-all-orgs-code-r)' \ + --ci \ + --debug + + if [ "$?" -ne 0 ]; then + echo "##vso[task.logissue type=error]Failed to synchronize the VMR" + exit 1 + fi + displayName: Sync repo into VMR (Unix) + condition: ne(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo + +- script: | + git config --global diff.astextplain.textconv echo + git config --system core.longpaths true + displayName: Configure Windows git (longpaths, astextplain) + condition: eq(variables['Agent.OS'], 'Windows_NT') + +- powershell: | + ./eng/common/vmr-sync.ps1 ` + -vmr ${{ parameters.vmrPath }} ` + -tmp $(Agent.TempDirectory) ` + -azdevPat '$(dn-bot-all-orgs-code-r)' ` + -ci ` + -debugOutput + + if ($LASTEXITCODE -ne 0) { + echo "##vso[task.logissue type=error]Failed to synchronize the VMR" + exit 1 + } + displayName: Sync repo into VMR (Windows) + condition: eq(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo + +- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + - task: CopyFiles@2 + displayName: Collect failed patches + condition: failed() + inputs: + SourceFolder: '$(Agent.TempDirectory)' + Contents: '*.patch' + TargetFolder: '$(Build.ArtifactStagingDirectory)/FailedPatches' + + - publish: '$(Build.ArtifactStagingDirectory)/FailedPatches' + artifact: $(System.JobDisplayName)_FailedPatches + displayName: Upload failed patches + condition: failed() + +- ${{ each assetName in parameters.additionalSyncs }}: + # The vmr-sync script ends up staging files in the local VMR so we have to commit those + - script: + git commit --allow-empty -am "Forward-flow $(Build.Repository.Name)" + displayName: Commit local VMR changes + workingDirectory: ${{ parameters.vmrPath }} + + - script: | + set -ex + + echo "Searching for details of asset ${{ assetName }}..." + + # Use darc to get dependencies information + dependencies=$(./.dotnet/dotnet darc get-dependencies --name '${{ assetName }}' --ci) + + # Extract repository URL and commit hash + repository=$(echo "$dependencies" | grep 'Repo:' | sed 's/Repo:[[:space:]]*//' | head -1) + + if [ -z "$repository" ]; then + echo "##vso[task.logissue type=error]Asset ${{ assetName }} not found in the dependency list" + exit 1 + fi + + commit=$(echo "$dependencies" | grep 'Commit:' | sed 's/Commit:[[:space:]]*//' | head -1) + + echo "Updating the VMR from $repository / $commit..." + cd .. + git clone $repository ${{ assetName }} + cd ${{ assetName }} + git checkout $commit + git branch "sync/$commit" + + ./eng/common/vmr-sync.sh \ + --vmr ${{ parameters.vmrPath }} \ + --tmp $(Agent.TempDirectory) \ + --azdev-pat '$(dn-bot-all-orgs-code-r)' \ + --ci \ + --debug + + if [ "$?" -ne 0 ]; then + echo "##vso[task.logissue type=error]Failed to synchronize the VMR" + exit 1 + fi + displayName: Sync ${{ assetName }} into (Unix) + condition: ne(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo + + - powershell: | + $ErrorActionPreference = 'Stop' + + Write-Host "Searching for details of asset ${{ assetName }}..." + + $dependencies = .\.dotnet\dotnet darc get-dependencies --name '${{ assetName }}' --ci + + $repository = $dependencies | Select-String -Pattern 'Repo:\s+([^\s]+)' | Select-Object -First 1 + $repository -match 'Repo:\s+([^\s]+)' | Out-Null + $repository = $matches[1] + + if ($repository -eq $null) { + Write-Error "Asset ${{ assetName }} not found in the dependency list" + exit 1 + } + + $commit = $dependencies | Select-String -Pattern 'Commit:\s+([^\s]+)' | Select-Object -First 1 + $commit -match 'Commit:\s+([^\s]+)' | Out-Null + $commit = $matches[1] + + Write-Host "Updating the VMR from $repository / $commit..." + cd .. + git clone $repository ${{ assetName }} + cd ${{ assetName }} + git checkout $commit + git branch "sync/$commit" + + .\eng\common\vmr-sync.ps1 ` + -vmr ${{ parameters.vmrPath }} ` + -tmp $(Agent.TempDirectory) ` + -azdevPat '$(dn-bot-all-orgs-code-r)' ` + -ci ` + -debugOutput + + if ($LASTEXITCODE -ne 0) { + echo "##vso[task.logissue type=error]Failed to synchronize the VMR" + exit 1 + } + displayName: Sync ${{ assetName }} into (Windows) + condition: ne(variables['Agent.OS'], 'Windows_NT') + workingDirectory: $(Agent.BuildDirectory)/repo diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml new file mode 100644 index 00000000000..ce3c29a62fa --- /dev/null +++ b/eng/common/templates/vmr-build-pr.yml @@ -0,0 +1,42 @@ +# This pipeline is used for running the VMR verification of the PR changes in repo-level PRs. +# +# It will run a full set of verification jobs defined in: +# https://github.com/dotnet/dotnet/blob/10060d128e3f470e77265f8490f5e4f72dae738e/eng/pipelines/templates/stages/vmr-build.yml#L27-L38 +# +# For repos that do not need to run the full set, you would do the following: +# +# 1. Copy this YML file to a repo-specific location, i.e. outside of eng/common. +# +# 2. Add `verifications` parameter to VMR template reference +# +# Examples: +# - For source-build stage 1 verification, add the following: +# verifications: [ "source-build-stage1" ] +# +# - For Windows only verifications, add the following: +# verifications: [ "unified-build-windows-x64", "unified-build-windows-x86" ] + +trigger: none +pr: none + +variables: +- template: /eng/common/templates/variables/pool-providers.yml@self + +- name: skipComponentGovernanceDetection # we run CG on internal builds only + value: true + +- name: Codeql.Enabled # we run CodeQL on internal builds only + value: false + +resources: + repositories: + - repository: vmr + type: github + name: dotnet/dotnet + endpoint: dotnet + +stages: +- template: /eng/pipelines/templates/stages/vmr-build.yml@vmr + parameters: + isBuiltFromVmr: false + scope: lite diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 9b3ad8840fd..06b44de7870 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -65,10 +65,8 @@ $ErrorActionPreference = 'Stop' # Base-64 encoded SAS token that has permission to storage container described by $runtimeSourceFeed [string]$runtimeSourceFeedKey = if (Test-Path variable:runtimeSourceFeedKey) { $runtimeSourceFeedKey } else { $null } -# True if the build is a product build -[bool]$productBuild = if (Test-Path variable:productBuild) { $productBuild } else { $false } - -[String[]]$properties = if (Test-Path variable:properties) { $properties } else { @() } +# True when the build is running within the VMR. +[bool]$fromVMR = if (Test-Path variable:fromVMR) { $fromVMR } else { $false } function Create-Directory ([string[]] $path) { New-Item -Path $path -Force -ItemType 'Directory' | Out-Null @@ -259,7 +257,20 @@ function Retry($downloadBlock, $maxRetries = 5) { function GetDotNetInstallScript([string] $dotnetRoot) { $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1' + $shouldDownload = $false + if (!(Test-Path $installScript)) { + $shouldDownload = $true + } else { + # Check if the script is older than 30 days + $fileAge = (Get-Date) - (Get-Item $installScript).LastWriteTime + if ($fileAge.Days -gt 30) { + Write-Host "Existing install script is too old, re-downloading..." + $shouldDownload = $true + } + } + + if ($shouldDownload) { Create-Directory $dotnetRoot $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit $uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" @@ -383,8 +394,8 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # If the version of msbuild is going to be xcopied, # use this version. Version matches a package here: - # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.12.0 - $defaultXCopyMSBuildVersion = '17.12.0' + # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.13.0 + $defaultXCopyMSBuildVersion = '17.13.0' if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { @@ -533,7 +544,8 @@ function LocateVisualStudio([object]$vsRequirements = $null){ if (Get-Member -InputObject $GlobalJson.tools -Name 'vswhere') { $vswhereVersion = $GlobalJson.tools.vswhere } else { - $vswhereVersion = '2.5.2' + # keep this in sync with the VSWhereVersion in DefaultVersions.props + $vswhereVersion = '3.1.7' } $vsWhereDir = Join-Path $ToolsDir "vswhere\$vswhereVersion" @@ -541,7 +553,8 @@ function LocateVisualStudio([object]$vsRequirements = $null){ if (!(Test-Path $vsWhereExe)) { Create-Directory $vsWhereDir - Write-Host 'Downloading vswhere' + Write-Host "Downloading vswhere $vswhereVersion" + $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit Retry({ Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -OutFile $vswhereExe }) @@ -604,14 +617,7 @@ function InitializeBuildTool() { } $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') - # Use override if it exists - commonly set by source-build - if ($null -eq $env:_OverrideArcadeInitializeBuildToolFramework) { - $initializeBuildToolFramework="net9.0" - } else { - $initializeBuildToolFramework=$env:_OverrideArcadeInitializeBuildToolFramework - } - - $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = $initializeBuildToolFramework } + $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'net' } } elseif ($msbuildEngine -eq "vs") { try { $msbuildPath = InitializeVisualStudioMSBuild -install:$restore @@ -620,7 +626,7 @@ function InitializeBuildTool() { ExitWithExitCode 1 } - $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "net472"; ExcludePrereleaseVS = $excludePrereleaseVS } + $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "netframework"; ExcludePrereleaseVS = $excludePrereleaseVS } } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unexpected value of -msbuildEngine: '$msbuildEngine'." ExitWithExitCode 1 @@ -653,7 +659,6 @@ function GetNuGetPackageCachePath() { $env:NUGET_PACKAGES = Join-Path $env:UserProfile '.nuget\packages\' } else { $env:NUGET_PACKAGES = Join-Path $RepoRoot '.packages\' - $env:RESTORENOHTTPCACHE = $true } } @@ -775,26 +780,13 @@ function MSBuild() { $toolsetBuildProject = InitializeToolset $basePath = Split-Path -parent $toolsetBuildProject - $possiblePaths = @( - # new scripts need to work with old packages, so we need to look for the old names/versions - (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll')), - (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.Arcade.Sdk.dll')), - (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.ArcadeLogging.dll')), - (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.Arcade.Sdk.dll')), - (Join-Path $basePath (Join-Path net8.0 'Microsoft.DotNet.ArcadeLogging.dll')), - (Join-Path $basePath (Join-Path net8.0 'Microsoft.DotNet.Arcade.Sdk.dll')) - ) - $selectedPath = $null - foreach ($path in $possiblePaths) { - if (Test-Path $path -PathType Leaf) { - $selectedPath = $path - break - } - } + $selectedPath = Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll') + if (-not $selectedPath) { - Write-PipelineTelemetryError -Category 'Build' -Message 'Unable to find arcade sdk logger assembly.' + Write-PipelineTelemetryError -Category 'Build' -Message "Unable to find arcade sdk logger assembly: $selectedPath" ExitWithExitCode 1 } + $args += "/logger:$selectedPath" } @@ -857,8 +849,8 @@ function MSBuild-Core() { } # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR orchestrator build. - if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$productBuild -and -not($properties -like "*DotNetBuildRepo=true*")) { + # Skip this when the build is a child of the VMR build. + if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) { Write-PipelineSetResult -Result "Failed" -Message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 01b09b65796..c1841c9dfd0 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -5,6 +5,9 @@ # CI mode - set to true on CI server for PR validation build or official build. ci=${ci:-false} +# Build mode +source_build=${source_build:-false} + # Set to true to use the pipelines logger which will enable Azure logging output. # https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md # This flag is meant as a temporary opt-opt for the feature while validate it across @@ -58,7 +61,8 @@ use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} # True to use global NuGet cache instead of restoring packages to repository-local directory. -if [[ "$ci" == true ]]; then +# Keep in sync with NuGetPackageroot in Arcade SDK's RepositoryLayout.props. +if [[ "$ci" == true || "$source_build" == true ]]; then use_global_nuget_cache=${use_global_nuget_cache:-false} else use_global_nuget_cache=${use_global_nuget_cache:-true} @@ -68,8 +72,8 @@ fi runtime_source_feed=${runtime_source_feed:-''} runtime_source_feed_key=${runtime_source_feed_key:-''} -# True if the build is a product build -product_build=${product_build:-false} +# True when the build is running within the VMR. +from_vmr=${from_vmr:-false} # Resolve any symlinks in the given path. function ResolvePath { @@ -296,8 +300,29 @@ function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" local install_script_url="https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" + local timestamp_file="$root/.dotnet-install.timestamp" + local should_download=false if [[ ! -a "$install_script" ]]; then + should_download=true + elif [[ -f "$timestamp_file" ]]; then + # Check if the script is older than 30 days using timestamp file + local download_time=$(cat "$timestamp_file" 2>/dev/null || echo "0") + local current_time=$(date +%s) + local age_seconds=$((current_time - download_time)) + + # 30 days = 30 * 24 * 60 * 60 = 2592000 seconds + if [[ $age_seconds -gt 2592000 ]]; then + echo "Existing install script is too old, re-downloading..." + should_download=true + fi + else + # No timestamp file exists, assume script is old and re-download + echo "No timestamp found for existing install script, re-downloading..." + should_download=true + fi + + if [[ "$should_download" == true ]]; then mkdir -p "$root" echo "Downloading '$install_script_url'" @@ -324,6 +349,9 @@ function GetDotNetInstallScript { ExitWithExitCode $exit_code } fi + + # Create timestamp file to track download time in seconds from epoch + date +%s > "$timestamp_file" fi # return value _GetDotNetInstallScript="$install_script" @@ -339,22 +367,14 @@ function InitializeBuildTool { # return values _InitializeBuildTool="$_InitializeDotNetCli/dotnet" _InitializeBuildToolCommand="msbuild" - # use override if it exists - commonly set by source-build - if [[ "${_OverrideArcadeInitializeBuildToolFramework:-x}" == "x" ]]; then - _InitializeBuildToolFramework="net9.0" - else - _InitializeBuildToolFramework="${_OverrideArcadeInitializeBuildToolFramework}" - fi } -# Set RestoreNoHttpCache as a workaround for https://github.com/NuGet/Home/issues/3116 function GetNuGetPackageCachePath { if [[ -z ${NUGET_PACKAGES:-} ]]; then if [[ "$use_global_nuget_cache" == true ]]; then export NUGET_PACKAGES="$HOME/.nuget/packages/" else export NUGET_PACKAGES="$repo_root/.packages/" - export RESTORENOHTTPCACHE=true fi fi @@ -451,25 +471,13 @@ function MSBuild { fi local toolset_dir="${_InitializeToolset%/*}" - # new scripts need to work with old packages, so we need to look for the old names/versions - local selectedPath= - local possiblePaths=() - possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.ArcadeLogging.dll" ) - possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" ) - possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.ArcadeLogging.dll" ) - possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.Arcade.Sdk.dll" ) - possiblePaths+=( "$toolset_dir/net8.0/Microsoft.DotNet.ArcadeLogging.dll" ) - possiblePaths+=( "$toolset_dir/net8.0/Microsoft.DotNet.Arcade.Sdk.dll" ) - for path in "${possiblePaths[@]}"; do - if [[ -f $path ]]; then - selectedPath=$path - break - fi - done + local selectedPath="$toolset_dir/net/Microsoft.DotNet.ArcadeLogging.dll" + if [[ -z "$selectedPath" ]]; then - Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly." + Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly: $selectedPath" ExitWithExitCode 1 fi + args+=( "-logger:$selectedPath" ) fi @@ -506,8 +514,8 @@ function MSBuild-Core { echo "Build failed with exit code $exit_code. Check errors above." # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR orchestrator build. - if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$product_build" != true && "$properties" != *"DotNetBuildRepo=true"* ]]; then + # Skip this when the build is a child of the VMR build. + if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true ]]; then Write-PipelineSetResult -result "Failed" -message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error @@ -530,6 +538,13 @@ function GetDarc { fi "$eng_root/common/darc-init.sh" --toolpath "$darc_path" $version + darc_tool="$darc_path/darc" +} + +# Returns a full path to an Arcade SDK task project file. +function GetSdkTaskProject { + taskName=$1 + echo "$(dirname $_InitializeToolset)/SdkTasks/$taskName.proj" } ResolvePath "${BASH_SOURCE[0]}" diff --git a/eng/common/vmr-sync.ps1 b/eng/common/vmr-sync.ps1 new file mode 100644 index 00000000000..97302f3205b --- /dev/null +++ b/eng/common/vmr-sync.ps1 @@ -0,0 +1,138 @@ +<# +.SYNOPSIS + +This script is used for synchronizing the current repository into a local VMR. +It pulls the current repository's code into the specified VMR directory for local testing or +Source-Build validation. + +.DESCRIPTION + +The tooling used for synchronization will clone the VMR repository into a temporary folder if +it does not already exist. These clones can be reused in future synchronizations, so it is +recommended to dedicate a folder for this to speed up re-runs. + +.EXAMPLE + Synchronize current repository into a local VMR: + ./vmr-sync.ps1 -vmrDir "$HOME/repos/dotnet" -tmpDir "$HOME/repos/tmp" + +.PARAMETER tmpDir +Required. Path to the temporary folder where repositories will be cloned + +.PARAMETER vmrBranch +Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch + +.PARAMETER azdevPat +Optional. Azure DevOps PAT to use for cloning private repositories. + +.PARAMETER vmrDir +Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder + +.PARAMETER debugOutput +Optional. Enables debug logging in the darc vmr command. + +.PARAMETER ci +Optional. Denotes that the script is running in a CI environment. +#> +param ( + [Parameter(Mandatory=$true, HelpMessage="Path to the temporary folder where repositories will be cloned")] + [string][Alias('t', 'tmp')]$tmpDir, + [string][Alias('b', 'branch')]$vmrBranch, + [string]$remote, + [string]$azdevPat, + [string][Alias('v', 'vmr')]$vmrDir, + [switch]$ci, + [switch]$debugOutput +) + +function Fail { + Write-Host "> $($args[0])" -ForegroundColor 'Red' +} + +function Highlight { + Write-Host "> $($args[0])" -ForegroundColor 'Cyan' +} + +$verbosity = 'verbose' +if ($debugOutput) { + $verbosity = 'debug' +} +# Validation + +if (-not $tmpDir) { + Fail "Missing -tmpDir argument. Please specify the path to the temporary folder where the repositories will be cloned" + exit 1 +} + +# Sanitize the input + +if (-not $vmrDir) { + $vmrDir = Join-Path $tmpDir 'dotnet' +} + +if (-not (Test-Path -Path $tmpDir -PathType Container)) { + New-Item -ItemType Directory -Path $tmpDir | Out-Null +} + +# Prepare the VMR + +if (-not (Test-Path -Path $vmrDir -PathType Container)) { + Highlight "Cloning 'dotnet/dotnet' into $vmrDir.." + git clone https://github.com/dotnet/dotnet $vmrDir + + if ($vmrBranch) { + git -C $vmrDir switch -c $vmrBranch + } +} +else { + if ((git -C $vmrDir diff --quiet) -eq $false) { + Fail "There are changes in the working tree of $vmrDir. Please commit or stash your changes" + exit 1 + } + + if ($vmrBranch) { + Highlight "Preparing $vmrDir" + git -C $vmrDir checkout $vmrBranch + git -C $vmrDir pull + } +} + +Set-StrictMode -Version Latest + +# Prepare darc + +Highlight 'Installing .NET, preparing the tooling..' +. .\eng\common\tools.ps1 +$dotnetRoot = InitializeDotNetCli -install:$true +$darc = Get-Darc +$dotnet = "$dotnetRoot\dotnet.exe" + +Highlight "Starting the synchronization of VMR.." + +# Synchronize the VMR +$darcArgs = ( + "vmr", "forwardflow", + "--tmp", $tmpDir, + "--$verbosity", + $vmrDir +) + +if ($ci) { + $darcArgs += ("--ci") +} + +if ($azdevPat) { + $darcArgs += ("--azdev-pat", $azdevPat) +} + +& "$darc" $darcArgs + +if ($LASTEXITCODE -eq 0) { + Highlight "Synchronization succeeded" +} +else { + Fail "Synchronization of repo to VMR failed!" + Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." + Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." + Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." + exit 1 +} diff --git a/eng/common/vmr-sync.sh b/eng/common/vmr-sync.sh new file mode 100644 index 00000000000..44239e331c0 --- /dev/null +++ b/eng/common/vmr-sync.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +### This script is used for synchronizing the current repository into a local VMR. +### It pulls the current repository's code into the specified VMR directory for local testing or +### Source-Build validation. +### +### The tooling used for synchronization will clone the VMR repository into a temporary folder if +### it does not already exist. These clones can be reused in future synchronizations, so it is +### recommended to dedicate a folder for this to speed up re-runs. +### +### USAGE: +### Synchronize current repository into a local VMR: +### ./vmr-sync.sh --tmp "$HOME/repos/tmp" "$HOME/repos/dotnet" +### +### Options: +### -t, --tmp, --tmp-dir PATH +### Required. Path to the temporary folder where repositories will be cloned +### +### -b, --branch, --vmr-branch BRANCH_NAME +### Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch +### +### --debug +### Optional. Turns on the most verbose logging for the VMR tooling +### +### --remote name:URI +### Optional. Additional remote to use during the synchronization +### This can be used to synchronize to a commit from a fork of the repository +### Example: 'runtime:https://github.com/yourfork/runtime' +### +### --azdev-pat +### Optional. Azure DevOps PAT to use for cloning private repositories. +### +### -v, --vmr, --vmr-dir PATH +### Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +function print_help () { + sed -n '/^### /,/^$/p' "$source" | cut -b 5- +} + +COLOR_RED=$(tput setaf 1 2>/dev/null || true) +COLOR_CYAN=$(tput setaf 6 2>/dev/null || true) +COLOR_CLEAR=$(tput sgr0 2>/dev/null || true) +COLOR_RESET=uniquesearchablestring +FAILURE_PREFIX='> ' + +function fail () { + echo "${COLOR_RED}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_RED}}${COLOR_CLEAR}" >&2 +} + +function highlight () { + echo "${COLOR_CYAN}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_CYAN}}${COLOR_CLEAR}" +} + +tmp_dir='' +vmr_dir='' +vmr_branch='' +additional_remotes='' +verbosity=verbose +azdev_pat='' +ci=false + +while [[ $# -gt 0 ]]; do + opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -t|--tmp|--tmp-dir) + tmp_dir=$2 + shift + ;; + -v|--vmr|--vmr-dir) + vmr_dir=$2 + shift + ;; + -b|--branch|--vmr-branch) + vmr_branch=$2 + shift + ;; + --remote) + additional_remotes="$additional_remotes $2" + shift + ;; + --azdev-pat) + azdev_pat=$2 + shift + ;; + --ci) + ci=true + ;; + -d|--debug) + verbosity=debug + ;; + -h|--help) + print_help + exit 0 + ;; + *) + fail "Invalid argument: $1" + print_help + exit 1 + ;; + esac + + shift +done + +# Validation + +if [[ -z "$tmp_dir" ]]; then + fail "Missing --tmp-dir argument. Please specify the path to the temporary folder where the repositories will be cloned" + exit 1 +fi + +# Sanitize the input + +if [[ -z "$vmr_dir" ]]; then + vmr_dir="$tmp_dir/dotnet" +fi + +if [[ ! -d "$tmp_dir" ]]; then + mkdir -p "$tmp_dir" +fi + +if [[ "$verbosity" == "debug" ]]; then + set -x +fi + +# Prepare the VMR + +if [[ ! -d "$vmr_dir" ]]; then + highlight "Cloning 'dotnet/dotnet' into $vmr_dir.." + git clone https://github.com/dotnet/dotnet "$vmr_dir" + + if [[ -n "$vmr_branch" ]]; then + git -C "$vmr_dir" switch -c "$vmr_branch" + fi +else + if ! git -C "$vmr_dir" diff --quiet; then + fail "There are changes in the working tree of $vmr_dir. Please commit or stash your changes" + exit 1 + fi + + if [[ -n "$vmr_branch" ]]; then + highlight "Preparing $vmr_dir" + git -C "$vmr_dir" checkout "$vmr_branch" + git -C "$vmr_dir" pull + fi +fi + +set -e + +# Prepare darc + +highlight 'Installing .NET, preparing the tooling..' +source "./eng/common/tools.sh" +InitializeDotNetCli true +GetDarc +dotnetDir=$( cd ./.dotnet/; pwd -P ) +dotnet=$dotnetDir/dotnet + +highlight "Starting the synchronization of VMR.." +set +e + +if [[ -n "$additional_remotes" ]]; then + additional_remotes="--additional-remotes $additional_remotes" +fi + +if [[ -n "$azdev_pat" ]]; then + azdev_pat="--azdev-pat $azdev_pat" +fi + +ci_arg='' +if [[ "$ci" == "true" ]]; then + ci_arg="--ci" +fi + +# Synchronize the VMR + +export DOTNET_ROOT="$dotnetDir" + +"$darc_tool" vmr forwardflow \ + --tmp "$tmp_dir" \ + $azdev_pat \ + --$verbosity \ + $ci_arg \ + $additional_remotes \ + "$vmr_dir" + +if [[ $? == 0 ]]; then + highlight "Synchronization succeeded" +else + fail "Synchronization of repo to VMR failed!" + fail "'$vmr_dir' is left in its last state (re-run of this script will reset it)." + fail "Please inspect the logs which contain path to the failing patch file (use --debug to get all the details)." + fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." + exit 1 +fi diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml index bd008ead075..bd760e84f3c 100644 --- a/eng/pipelines/templates/BuildAndTest.yml +++ b/eng/pipelines/templates/BuildAndTest.yml @@ -169,11 +169,12 @@ steps: displayName: Build Azure DevOps plugin - script: ${{ parameters.buildScript }} + -restore -sign $(_SignArgs) -publish $(_PublishArgs) -configuration ${{ parameters.buildConfig }} -warnAsError 1 /bl:${{ parameters.repoLogPath }}/publish.binlog - /p:Restore=false /p:Build=false + /p:Build=false $(_OfficialBuildIdArgs) displayName: Sign and publish diff --git a/global.json b/global.json index bb42ae14f2c..fd61e068f3a 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "9.0.109" + "version": "10.0.100-rc.1.25451.107" }, "tools": { - "dotnet": "9.0.109", + "dotnet": "10.0.100-rc.1.25451.107", "runtimes": { "dotnet": [ "8.0.0", @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25428.3", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25428.3" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25476.2", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25476.2" } } diff --git a/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs index ee610c0c032..0354528f810 100644 --- a/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs +++ b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs @@ -6,6 +6,7 @@ using Microsoft.Gen.Metrics.Model; namespace Microsoft.Gen.MetricsReports; + internal static class MetricsReportsHelpers { internal static ReportedMetricClass[] MapToCommonModel(IReadOnlyList meteringClasses, string? rootNamespace) diff --git a/src/Generators/Shared/RoslynExtensions.cs b/src/Generators/Shared/RoslynExtensions.cs index 82860a09f59..ef4f7e07911 100644 --- a/src/Generators/Shared/RoslynExtensions.cs +++ b/src/Generators/Shared/RoslynExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -103,33 +102,6 @@ internal static class RoslynExtensions ? throw new ArgumentException("The input type must correspond to a named type symbol.") : GetBestTypeByMetadataName(compilation, type.FullName); - public static ImmutableArray ToImmutableArray(this ReadOnlySpan span) - { -#pragma warning disable S109 // Magic numbers should not be used - switch (span.Length) - { - case 0: - return ImmutableArray.Empty; - case 1: - return ImmutableArray.Create(span[0]); - case 2: - return ImmutableArray.Create(span[0], span[1]); - case 3: - return ImmutableArray.Create(span[0], span[1], span[2]); - case 4: - return ImmutableArray.Create(span[0], span[1], span[2], span[3]); - default: - var builder = ImmutableArray.CreateBuilder(span.Length); - foreach (var item in span) - { - builder.Add(item); - } - - return builder.MoveToImmutable(); - } -#pragma warning restore S109 // Magic numbers should not be used - } - public static SimpleNameSyntax GetUnqualifiedName(this NameSyntax name) => name switch { diff --git a/src/Libraries/.editorconfig b/src/Libraries/.editorconfig index 287246445ae..d3164bd9547 100644 --- a/src/Libraries/.editorconfig +++ b/src/Libraries/.editorconfig @@ -2012,7 +2012,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = warning +dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj index 15e0e9d9225..2d6e518f1b5 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj @@ -11,6 +11,8 @@ $(NetCoreTargetFrameworks) true + + $(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration true true false @@ -39,7 +41,6 @@ - diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs index 915fbbee722..ca5e38f3651 100644 --- a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs @@ -15,7 +15,6 @@ namespace Microsoft.AspNetCore.HeaderParsing; /// public sealed partial class HeaderParsingFeature { - private readonly IHeaderRegistry _registry; private readonly ILogger _logger; private readonly HeaderParsingMetrics _metrics; @@ -26,11 +25,10 @@ public sealed partial class HeaderParsingFeature internal HttpContext? Context { get; set; } - internal HeaderParsingFeature(IHeaderRegistry registry, ILogger logger, HeaderParsingMetrics metrics) + internal HeaderParsingFeature(ILogger logger, HeaderParsingMetrics metrics) { _logger = logger; _metrics = metrics; - _registry = registry; } /// @@ -91,12 +89,11 @@ internal sealed class PoolHelper : IDisposable public PoolHelper( ObjectPool pool, - IHeaderRegistry registry, ILogger logger, HeaderParsingMetrics metrics) { _pool = pool; - Feature = new HeaderParsingFeature(registry, logger, metrics); + Feature = new HeaderParsingFeature(logger, metrics); } public void Dispose() diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj index adbae73bd9e..32020fa29f9 100644 --- a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj @@ -10,6 +10,8 @@ $(NetCoreTargetFrameworks) true + + $(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration true true true @@ -30,10 +32,6 @@ - - - - diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj index 53564605660..c6d52244f87 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/Microsoft.Extensions.AI.Evaluation.NLP.csproj @@ -26,7 +26,6 @@ - diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs index ec3dc720bb0..18d9f52d2c4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs @@ -445,9 +445,8 @@ private async ValueTask AddHeadersAsync( httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); - if (httpRequestMessage.Content is not null) - { - httpRequestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - } +#pragma warning disable IDE0058 // Temporary workaround for Roslyn analyzer issue (see https://github.com/dotnet/roslyn/issues/80499). + httpRequestMessage.Content?.Headers.ContentType = new MediaTypeHeaderValue("application/json"); +#pragma warning restore IDE0058 } } diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs index 21f33e05a2d..2593136a736 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs @@ -54,5 +54,5 @@ public interface IAsyncState /// Registers new async context with the state. /// /// Token that gives access to the reserved context. - public AsyncStateToken RegisterAsyncContext(); + AsyncStateToken RegisterAsyncContext(); } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs index f499ba485b3..537968113c2 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultJsonSerializerFactory.cs @@ -126,10 +126,9 @@ static void PrepareStateForDepth(Type type, ref Dictionary? state, bool result) { var value = result ? FieldOnlyResult.FieldOnly : FieldOnlyResult.NotFieldOnly; - if (state is not null) - { - state[type] = value; - } +#pragma warning disable IDE0058 // Temporary workaround for Roslyn analyzer issue (see https://github.com/dotnet/roslyn/issues/80499). + state?[type] = value; +#pragma warning restore IDE0058 return value; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizationBuilder.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizationBuilder.cs index a7a08eb4d3e..fe7d146c579 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizationBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizationBuilder.cs @@ -14,7 +14,7 @@ public interface IExceptionSummarizationBuilder /// /// Gets the service collection into which the summary provider instances are registered. /// - public IServiceCollection Services { get; } + IServiceCollection Services { get; } /// /// Adds a summary provider to the builder. diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizer.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizer.cs index c9c4c4ef48a..3087bded812 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizer.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummarizer.cs @@ -15,5 +15,5 @@ public interface IExceptionSummarizer /// /// The exception to summarize. /// The summary of the given . - public ExceptionSummary Summarize(Exception exception); + ExceptionSummary Summarize(Exception exception); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummaryProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummaryProvider.cs index 24cfd0d079e..000e34ac0c7 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummaryProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/IExceptionSummaryProvider.cs @@ -26,15 +26,15 @@ public interface IExceptionSummaryProvider /// This method should only get invoked with an exception which is type compatible with a type /// described by . /// - public int Describe(Exception exception, out string? additionalDetails); + int Describe(Exception exception, out string? additionalDetails); /// /// Gets the set of supported exception types that can be handled by this provider. /// - public IEnumerable SupportedExceptionTypes { get; } + IEnumerable SupportedExceptionTypes { get; } /// /// Gets the set of description strings exposed by this provider. /// - public IReadOnlyList Descriptions { get; } + IReadOnlyList Descriptions { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs index 6772a33f0d3..e8164908150 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs @@ -13,7 +13,7 @@ public interface IManualHealthCheck : IDisposable /// /// Gets or sets the health status. /// - public HealthCheckResult Result { get; set; } + HealthCheckResult Result { get; set; } } /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs index daef766c1f2..bed838f31e7 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs @@ -13,15 +13,15 @@ public interface IDownstreamDependencyMetadata /// /// Gets the name of the dependent service. /// - public string DependencyName { get; } + string DependencyName { get; } /// /// Gets the list of host name suffixes that can uniquely identify a host as this dependency. /// - public ISet UniqueHostNameSuffixes { get; } + ISet UniqueHostNameSuffixes { get; } /// /// Gets the list of all metadata for all routes to the dependency service. /// - public ISet RequestMetadata { get; } + ISet RequestMetadata { get; } } diff --git a/src/Shared/.editorconfig b/src/Shared/.editorconfig index bc980461f04..59e9e2090d9 100644 --- a/src/Shared/.editorconfig +++ b/src/Shared/.editorconfig @@ -1991,7 +1991,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = warning +dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name diff --git a/src/Shared/Debugger/IDebuggerState.cs b/src/Shared/Debugger/IDebuggerState.cs index 9e5eb7ac0be..bcbe3d13187 100644 --- a/src/Shared/Debugger/IDebuggerState.cs +++ b/src/Shared/Debugger/IDebuggerState.cs @@ -13,5 +13,5 @@ internal interface IDebuggerState /// /// Gets a value indicating whether a debugger is attached or not. /// - public bool IsAttached { get; } + bool IsAttached { get; } } diff --git a/test/.editorconfig b/test/.editorconfig index e98d1472b6a..ca9523939db 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -1981,7 +1981,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = warning +dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs index 013f8d85956..3e0e8683ba5 100644 --- a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs @@ -196,10 +196,10 @@ public MyDerivedClass(double privateFieldValue) internal interface IMyInterface { - public int IntProperty { get; set; } + int IntProperty { get; set; } [LogProperties] - public LeafTransitiveBaseClass? TransitiveProp { get; set; } + LeafTransitiveBaseClass? TransitiveProp { get; set; } } internal sealed class MyInterfaceImpl : IMyInterface diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs index ce23cb2dfb0..7bc5ddc95f2 100644 --- a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs @@ -48,7 +48,7 @@ public void Parses_header() var key = Registry.Register(CommonHeaders.Date); Context.Request.Headers["Date"] = date; - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; Assert.True(feature.TryGetHeaderValue(key, out var value, out var _)); Assert.Equal(date, value.ToString("R", CultureInfo.InvariantCulture)); @@ -67,7 +67,7 @@ public void Parses_multiple_headers() Context.Request.Headers["Date"] = currentDate; Context.Request.Headers["Test"] = futureDate; - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; Assert.True(feature.TryGetHeaderValue(key, out var value, out var result)); Assert.Equal(currentDate, value.ToString("R", CultureInfo.InvariantCulture)); @@ -89,7 +89,7 @@ public void Parses_with_late_binding() Context.Request.Headers["Date"] = date; - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; var key = Registry.Register(CommonHeaders.Date); @@ -103,7 +103,7 @@ public void TryParse_returns_false_on_header_not_found() { using var meter = new Meter(nameof(TryParse_returns_false_on_header_not_found)); var metrics = GetMockedMetrics(meter); - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; var key = Registry.Register(CommonHeaders.Date); Assert.False(feature.TryGetHeaderValue(key, out var value, out var _)); @@ -120,7 +120,7 @@ public void TryParse_returns_default_on_header_not_found() var date = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); _options.Value.DefaultValues.Add("Date", date); - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; var key = Registry.Register(CommonHeaders.Date); Assert.True(feature.TryGetHeaderValue(key, out var value, out var result)); @@ -138,7 +138,7 @@ public void TryParse_returns_false_on_error() using var metricCollector = new MetricCollector(meter, "aspnetcore.header_parsing.parse_errors"); Context.Request.Headers["Date"] = "Not a date."; - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; var key = Registry.Register(CommonHeaders.Date); Assert.False(feature.TryGetHeaderValue(key, out var value, out var result)); @@ -161,7 +161,7 @@ public void Dispose_resets_state_and_returns_to_pool() var metrics = GetMockedMetrics(meter); var pool = new Mock>(MockBehavior.Strict); - var helper = new HeaderParsingFeature.PoolHelper(pool.Object, Registry, _logger, metrics); + var helper = new HeaderParsingFeature.PoolHelper(pool.Object, _logger, metrics); helper.Feature.Context = Context; pool.Setup(x => x.Return(helper)); @@ -195,8 +195,8 @@ public void CachingWorks() Context.Request.Headers[HeaderNames.CacheControl] = "max-age=604800"; - var feature = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; - var feature2 = new HeaderParsingFeature(Registry, _logger, metrics) { Context = Context }; + var feature = new HeaderParsingFeature(_logger, metrics) { Context = Context }; + var feature2 = new HeaderParsingFeature(_logger, metrics) { Context = Context }; var key = Registry.Register(CommonHeaders.CacheControl); Assert.True(feature.TryGetHeaderValue(key, out var value1, out var error1)); diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs index 0932aab6ac0..1a826f086c6 100644 --- a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs @@ -146,8 +146,8 @@ public void ContentDisposition_ReturnsParsedValue() { var sv = new StringValues("attachment; filename=\"cool.html\""); Assert.True(ContentDispositionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); - Assert.Equal("cool.html", result.FileName); - Assert.Equal("attachment", result.DispositionType); + Assert.Equal("cool.html", result.FileName.ToString()); + Assert.Equal("attachment", result.DispositionType.ToString()); Assert.Null(error); } @@ -174,8 +174,8 @@ public void MediaType_ReturnsParsedValue() { var sv = new StringValues("text/html; charset=UTF-8"); Assert.True(MediaTypeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); - Assert.Equal("text/html", result.MediaType); - Assert.Equal("UTF-8", result.Charset); + Assert.Equal("text/html", result.MediaType.ToString()); + Assert.Equal("UTF-8", result.Charset.ToString()); Assert.Null(error); } @@ -203,8 +203,8 @@ public void MediaTypes_ReturnsParsedValue() var sv = new StringValues("text/html; charset=UTF-8"); Assert.True(MediaTypeHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); Assert.Single(result); - Assert.Equal("text/html", result[0].MediaType); - Assert.Equal("UTF-8", result[0].Charset); + Assert.Equal("text/html", result[0].MediaType.ToString()); + Assert.Equal("UTF-8", result[0].Charset.ToString()); Assert.Null(error); } @@ -223,7 +223,7 @@ public void EntityTag_ReturnsParsedValue() var sv = new StringValues("\"HelloWorld\""); Assert.True(EntityTagHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); Assert.Single(result!); - Assert.Equal("\"HelloWorld\"", result[0].Tag); + Assert.Equal("\"HelloWorld\"", result[0].Tag.ToString()); Assert.Null(error); } @@ -242,7 +242,7 @@ public void StringQuality_ReturnsParsedValue() var sv = new StringValues("en-US"); Assert.True(StringWithQualityHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); Assert.Single(result!); - Assert.Equal("en-US", result[0].Value); + Assert.Equal("en-US", result[0].Value.ToString()); Assert.Null(error); } @@ -252,8 +252,8 @@ public void StringQuality_Multi() var sv = new StringValues("en-US,en;q=0.5"); Assert.True(StringWithQualityHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); Assert.Equal(2, result.Count); - Assert.Equal("en-US", result[0].Value); - Assert.Equal("en", result[1].Value); + Assert.Equal("en-US", result[0].Value.ToString()); + Assert.Equal("en", result[1].Value.ToString()); Assert.Equal(0.5, result[1].Quality); Assert.Null(error); } @@ -300,7 +300,7 @@ public void Range_ReturnsParsedValue() { var sv = new StringValues("bytes=200-1000"); Assert.True(RangeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); - Assert.Equal("bytes", result!.Unit); + Assert.Equal("bytes", result!.Unit.ToString()); Assert.Single(result.Ranges); Assert.Equal(200, result.Ranges.Single().From); Assert.Equal(1000, result.Ranges.Single().To); @@ -341,7 +341,7 @@ public void RangeCondition_ReturnsParsedValue() sv = new StringValues("\"67ab43\""); Assert.True(RangeConditionHeaderValueParser.Instance.TryParse(sv, out result, out error)); - Assert.Equal("\"67ab43\"", result!.EntityTag!.Tag); + Assert.Equal("\"67ab43\"", result!.EntityTag!.Tag.ToString()); Assert.Null(error); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj index d2ae2802123..4c275e54993 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj @@ -29,7 +29,6 @@ - diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs index 1ad60e9ad4f..0fcdd415fd1 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs @@ -7,6 +7,7 @@ using Xunit; namespace Microsoft.Extensions.AsyncState.Test; + public class AsyncContextTests { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs index 06a1c30e5b2..ef0a5023cbe 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs @@ -4,6 +4,7 @@ using Xunit; namespace Microsoft.Extensions.AsyncState.Test; + public class AsyncStateTokenTests { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs index 909389acb95..5abd3e3d7cd 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs @@ -6,6 +6,7 @@ using Xunit; namespace Microsoft.Extensions.AsyncState.Test; + public class FeaturesPooledPolicyTests { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ExpirationTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ExpirationTests.cs index c33bcb50e98..562ba8ae98f 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ExpirationTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ExpirationTests.cs @@ -10,6 +10,7 @@ using static Microsoft.Extensions.Caching.Hybrid.Tests.L2Tests; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class ExpirationTests(ITestOutputHelper log) { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/FunctionalTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/FunctionalTests.cs index 7b8396eb50d..730013dbe4f 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/FunctionalTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/FunctionalTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class FunctionalTests : IClassFixture { private static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action? config = null) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/L2Tests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/L2Tests.cs index 5c9cc2a41c5..f5be5b5277d 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/L2Tests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/L2Tests.cs @@ -11,6 +11,7 @@ using Xunit.Abstractions; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class L2Tests(ITestOutputHelper log) : IClassFixture { private static string CreateString(bool work = false) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/LocalInvalidationTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/LocalInvalidationTests.cs index 310f7d5cdce..6efc4b14d45 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/LocalInvalidationTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/LocalInvalidationTests.cs @@ -9,6 +9,7 @@ using static Microsoft.Extensions.Caching.Hybrid.Tests.L2Tests; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class LocalInvalidationTests(ITestOutputHelper log) : IClassFixture { private static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action? config = null) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/PayloadTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/PayloadTests.cs index 1f125336dae..a4a9c470551 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/PayloadTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/PayloadTests.cs @@ -12,6 +12,7 @@ using static Microsoft.Extensions.Caching.Hybrid.Tests.L2Tests; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class PayloadTests(ITestOutputHelper log) : IClassFixture { private static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action? config = null) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TagSetTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TagSetTests.cs index 1c63ff5e5c2..818dac7b45c 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TagSetTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TagSetTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Hybrid.Internal; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class TagSetTests { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TypeTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TypeTests.cs index c2ab242a6b0..d0176ab4e49 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TypeTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/TypeTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Caching.Hybrid.Internal; namespace Microsoft.Extensions.Caching.Hybrid.Tests; + public class TypeTests { [Theory] diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactionAbstractionsExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactionAbstractionsExtensionsTest.cs index 52211d96e62..a890b5396c9 100644 --- a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactionAbstractionsExtensionsTest.cs +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactionAbstractionsExtensionsTest.cs @@ -24,12 +24,7 @@ public static void When_Passed_Null_Value_String_Builder_Extensions_Does_Not_App var sb = new StringBuilder(); var redactor = NullRedactor.Instance; - sb.AppendRedacted(NullRedactor.Instance, -#if NETCOREAPP3_1_OR_GREATER - null); -#else - (string?)null); -#endif + sb.AppendRedacted(NullRedactor.Instance, null); Assert.Equal(0, sb.Length); } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs index 61fcf232461..9e3227e92d2 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs @@ -5,5 +5,5 @@ namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; public interface IAnotherFakeServiceCounter { - public int Counter { get; set; } + int Counter { get; set; } } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs index 592a617d32f..a3d7b92cbb2 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs @@ -5,5 +5,5 @@ namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; public interface IFactoryServiceCounter { - public int Counter { get; set; } + int Counter { get; set; } } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs index 772b0be54df..dea82dbdc7d 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs @@ -5,5 +5,5 @@ namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; public interface IFakeMultipleCounter { - public int Counter { get; set; } + int Counter { get; set; } } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs index e7bc46ec661..e9573e4e34f 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs @@ -5,5 +5,5 @@ namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; public interface IFakeOpenGenericCounter { - public int Counter { get; set; } + int Counter { get; set; } } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs index d7708a196d6..f8474f20599 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs @@ -5,5 +5,5 @@ namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; public interface IFakeServiceCounter { - public int Counter { get; set; } + int Counter { get; set; } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs index f7e164339f0..b5e50b9c273 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs @@ -16,6 +16,7 @@ using Xunit; namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; + public class HttpStandardResilienceOptionsCustomValidatorTests { [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LogBufferingFilterRuleTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LogBufferingFilterRuleTests.cs index 222d2aacc9a..a3b6bad6340 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LogBufferingFilterRuleTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LogBufferingFilterRuleTests.cs @@ -7,6 +7,7 @@ using Xunit; namespace Microsoft.Extensions.Diagnostics.Buffering.Test; + public class LogBufferingFilterRuleTests { private readonly LogBufferingFilterRuleSelector _selector = new(); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs index 6d4ed5908f8..58d2d50d9ba 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs @@ -5,6 +5,7 @@ using Moq; namespace Microsoft.Extensions.Diagnostics.Latency.Test; + internal static class MockLatencyContextRegistrationOptions { public static IOptions GetLatencyContextRegistrationOptions( From ecfdb53f58c8482b54897a40b93635b5d410e086 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:13:29 -0700 Subject: [PATCH 324/472] Update Microsoft.Extensions.AI changelog files with current NuGet versions (#6849) * Initial plan * Update Microsoft.Extensions.AI changelog files with current NuGet versions Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Correct OpenAI and AzureAIInference package versions to 9.9.1-preview.1.25474.6 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md | 2 +- .../Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md | 2 +- src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 2 +- src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 8a0e875557a..3ba9420a870 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.9.1 - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. - Added new `AITool.GetService` virtual method. diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index 412427dcad1..15b9f840773 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.9.1-preview.1.25474.6 - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index da67eb7c841..03fe8357eab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.9.1-preview.1.25474.6 - Updated to depend on OpenAI 2.5.0. - Added M.E.AI to OpenAI conversions for response format types. diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index e5c3028693f..275131b595a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## NOT YET RELEASED +## 9.9.1 - Updated the `EnableSensitiveData` properties on `OpenTelemetryChatClient/EmbeddingGenerator` to respect a `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. - Updated `OpenTelemetryChatClient/EmbeddingGenerator` to emit recent additions to the OpenTelemetry Semantic Conventions for Generative AI systems. From dbc92f8342e02603d64bb45342c2bb08fc62d5e4 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 29 Sep 2025 21:16:06 -0400 Subject: [PATCH 325/472] Fix GenerateImagesAsync_SingleImageGeneration integration test (#6843) Both DataContent and UriContent are valid in responses, and from both OpenAI and Azure OpenAI, I get back a UriContent. --- .../ImageGeneratorIntegrationTests.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs index 8e3078efbb5..76b08941bc5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ImageGeneratorIntegrationTests.cs @@ -43,13 +43,23 @@ public virtual async Task GenerateImagesAsync_SingleImageGeneration() Assert.NotNull(response); Assert.NotEmpty(response.Contents); - Assert.Single(response.Contents); - var content = response.Contents[0]; - Assert.IsType(content); - var dataContent = (DataContent)content; - Assert.False(dataContent.Data.IsEmpty); - Assert.StartsWith("image/", dataContent.MediaType, StringComparison.Ordinal); + var content = Assert.Single(response.Contents); + switch (content) + { + case UriContent uc: + Assert.StartsWith("http", uc.Uri.Scheme, StringComparison.Ordinal); + break; + + case DataContent dc: + Assert.False(dc.Data.IsEmpty); + Assert.StartsWith("image/", dc.MediaType, StringComparison.Ordinal); + break; + + default: + Assert.Fail($"Unexpected content type: {content.GetType()}"); + break; + } } [ConditionalFact] From c378af04f386f8c6b1980c47822b1ca0ac7bf639 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 30 Sep 2025 00:33:33 -0400 Subject: [PATCH 326/472] Re-enable IDE0032 (#6866) --- src/Libraries/.editorconfig | 6 +- .../ChatCompletion/ChatFinishReason.cs | 7 +- .../ChatCompletion/ChatResponseUpdate.cs | 7 +- .../Contents/DataContent.cs | 1 + .../Contents/ErrorContent.cs | 9 +- .../Contents/TextContent.cs | 8 +- .../Contents/TextReasoningContent.cs | 8 +- .../Embeddings/EmbeddingGenerationOptions.cs | 6 +- .../Image/ImageGenerationResponse.cs | 9 +- .../AzureStorageJsonUtilities.cs | 6 +- .../CSharp/JsonSerialization/JsonUtilities.cs | 6 +- .../ContentSafetyService.cs | 13 +- .../DistributedCachingChatClient.cs | 27 ++-- .../FunctionInvocationContext.cs | 32 ++--- .../FunctionInvokingChatClient.cs | 27 ++-- .../ChatReduction/SummarizingChatReducer.cs | 10 +- .../ManualHealthCheck.cs | 6 +- .../Polly/HttpRetryStrategyOptions.cs | 8 +- .../Routing/UriEndpoint.cs | 10 +- .../Logging/LoggerMessageHelper.cs | 19 +-- .../Latency/Internal/CheckpointTracker.cs | 13 +- .../Latency/Internal/LatencyContext.cs | 10 +- .../Logging/ExtendedLogger.LegacyTagJoiner.cs | 11 +- .../Logging/ExtendedLogger.ThreadLocals.cs | 40 +----- .../FakeTimeProvider.cs | 7 +- src/Shared/.editorconfig | 6 +- .../JsonSchemaExporter.JsonSchema.cs | 122 ++++++------------ src/Shared/ServerSentEvents/SseItem.cs | 14 +- test/.editorconfig | 2 +- .../HeaderParsingFeatureTests.cs | 7 +- .../Settings.cs | 11 +- .../Settings.cs | 11 +- .../Infrastructure/Project.cs | 21 +-- .../Infrastructure/TestCommandResult.cs | 7 +- 34 files changed, 166 insertions(+), 341 deletions(-) diff --git a/src/Libraries/.editorconfig b/src/Libraries/.editorconfig index d3164bd9547..d33ed319482 100644 --- a/src/Libraries/.editorconfig +++ b/src/Libraries/.editorconfig @@ -2012,7 +2012,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this +dotnet_diagnostic.IDE0032.severity = warning dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name @@ -5889,7 +5889,7 @@ dotnet_diagnostic.SA1414.severity = warning # Title : Braces for multi-line statements should not share line # Category : StyleCop.CSharp.LayoutRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md -dotnet_diagnostic.SA1500.severity = warning +dotnet_diagnostic.SA1500.severity = suggestion # rule does not work well with field-based property initializers # Title : Statement should not be on a single line # Category : StyleCop.CSharp.LayoutRules @@ -5957,7 +5957,7 @@ dotnet_diagnostic.SA1512.severity = none # Title : Closing brace should be followed by blank line # Category : StyleCop.CSharp.LayoutRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md -dotnet_diagnostic.SA1513.severity = warning +dotnet_diagnostic.SA1513.severity = suggestion # rule does not work well with field-based property initializers # Title : Element documentation header should be preceded by blank line # Category : StyleCop.CSharp.LayoutRules diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs index 18b3ec658e3..1852aa07f7c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs @@ -14,9 +14,6 @@ namespace Microsoft.Extensions.AI; [JsonConverter(typeof(Converter))] public readonly struct ChatFinishReason : IEquatable { - /// The finish reason value. If because `default(ChatFinishReason)` was used, the instance will behave like . - private readonly string? _value; - /// Initializes a new instance of the struct with a string that describes the reason. /// The reason value. /// is . @@ -24,11 +21,11 @@ namespace Microsoft.Extensions.AI; [JsonConstructor] public ChatFinishReason(string value) { - _value = Throw.IfNullOrWhitespace(value); + Value = Throw.IfNullOrWhitespace(value); } /// Gets the finish reason value. - public string Value => _value ?? Stop.Value; + public string Value => field ?? Stop.Value; /// public override bool Equals([NotNullWhen(true)] object? obj) => obj is ChatFinishReason other && Equals(other); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index 6a4f11c3777..0605d0785bb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -35,9 +35,6 @@ public class ChatResponseUpdate /// The response update content items. private IList? _contents; - /// The name of the author of the update. - private string? _authorName; - /// Initializes a new instance of the class. [JsonConstructor] public ChatResponseUpdate() @@ -64,8 +61,8 @@ public ChatResponseUpdate(ChatRole? role, IList? contents) /// Gets or sets the name of the author of the response update. public string? AuthorName { - get => _authorName; - set => _authorName = string.IsNullOrWhiteSpace(value) ? null : value; + get; + set => field = string.IsNullOrWhiteSpace(value) ? null : value; } /// Gets or sets the role of the author of the response update. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs index 4b8813c144e..7b2ef0ccb1a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs @@ -15,6 +15,7 @@ using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; +#pragma warning disable IDE0032 // Use auto property #pragma warning disable CA1307 // Specify StringComparison for clarity namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs index 4588531262b..4b82c4c5e91 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ErrorContent.cs @@ -14,22 +14,19 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public class ErrorContent : AIContent { - /// The error message. - private string? _message; - /// Initializes a new instance of the class with the specified error message. /// The error message to store in this content. public ErrorContent(string? message) { - _message = message; + Message = message; } /// Gets or sets the error message. [AllowNull] public string Message { - get => _message ?? string.Empty; - set => _message = value; + get => field ?? string.Empty; + set; } /// Gets or sets an error code associated with the error. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextContent.cs index 0f70a5f8b0a..d6bac57420f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextContent.cs @@ -12,15 +12,13 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class TextContent : AIContent { - private string? _text; - /// /// Initializes a new instance of the class. /// /// The text content. public TextContent(string? text) { - _text = text; + Text = text; } /// @@ -29,8 +27,8 @@ public TextContent(string? text) [AllowNull] public string Text { - get => _text ?? string.Empty; - set => _text = value; + get => field ?? string.Empty; + set; } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs index 345cb430dbd..57fec14cc0e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs @@ -17,15 +17,13 @@ namespace Microsoft.Extensions.AI; [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class TextReasoningContent : AIContent { - private string? _text; - /// /// Initializes a new instance of the class. /// /// The text reasoning content. public TextReasoningContent(string? text) { - _text = text; + Text = text; } /// @@ -34,8 +32,8 @@ public TextReasoningContent(string? text) [AllowNull] public string Text { - get => _text ?? string.Empty; - set => _text = value; + get => field ?? string.Empty; + set; } /// Gets or sets an optional opaque blob of data associated with this reasoning content. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs index b9a13d43dd0..4d88c85b760 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs @@ -10,12 +10,10 @@ namespace Microsoft.Extensions.AI; /// Represents the options for an embedding generation request. public class EmbeddingGenerationOptions { - private int? _dimensions; - /// Gets or sets the number of dimensions requested in the embedding. public int? Dimensions { - get => _dimensions; + get; set { if (value is not null) @@ -23,7 +21,7 @@ public int? Dimensions _ = Throw.IfLessThan(value.Value, 1, nameof(value)); } - _dimensions = value; + field = value; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs index ba3abe8f1a3..8f093634783 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationResponse.cs @@ -11,9 +11,6 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class ImageGenerationResponse { - /// The content items in the generated text response. - private IList? _contents; - /// Initializes a new instance of the class. [JsonConstructor] public ImageGenerationResponse() @@ -24,7 +21,7 @@ public ImageGenerationResponse() /// The contents for this response. public ImageGenerationResponse(IList? contents) { - _contents = contents; + Contents = contents; } /// Gets or sets the raw representation of the image generation response from an underlying implementation. @@ -46,8 +43,8 @@ public ImageGenerationResponse(IList? contents) [AllowNull] public IList Contents { - get => _contents ??= []; - set => _contents = value; + get => field ??= []; + set; } /// Gets or sets usage details for the image generation response. diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs index 204ac68394b..c2f7b418b01 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs @@ -13,16 +13,14 @@ internal static partial class AzureStorageJsonUtilities { internal static class Default { - private static JsonSerializerOptions? _options; - internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: true); + internal static JsonSerializerOptions Options => field ??= CreateJsonSerializerOptions(writeIndented: true); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); } internal static class Compact { - private static JsonSerializerOptions? _options; - internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: false); + internal static JsonSerializerOptions Options => field ??= CreateJsonSerializerOptions(writeIndented: false); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs index fb514bd33c8..9427b67be8e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs @@ -14,8 +14,7 @@ internal static partial class JsonUtilities { internal static class Default { - private static JsonSerializerOptions? _options; - internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: true); + internal static JsonSerializerOptions Options => field ??= CreateJsonSerializerOptions(writeIndented: true); internal static JsonTypeInfo DatasetTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); @@ -23,8 +22,7 @@ internal static class Default internal static class Compact { - private static JsonSerializerOptions? _options; - internal static JsonSerializerOptions Options => _options ??= CreateJsonSerializerOptions(writeIndented: false); + internal static JsonSerializerOptions Options => field ??= CreateJsonSerializerOptions(writeIndented: false); internal static JsonTypeInfo DatasetTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo CacheEntryTypeInfo => Options.GetTypeInfo(); internal static JsonTypeInfo ScenarioRunResultTypeInfo => Options.GetTypeInfo(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs index 18d9f52d2c4..26dec553915 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyService.cs @@ -20,15 +20,10 @@ internal sealed partial class ContentSafetyService(ContentSafetyServiceConfigura private const string APIVersionForServiceDiscoveryInHubBasedProjects = "?api-version=2023-08-01-preview"; private const string APIVersionForNonHubBasedProjects = "?api-version=2025-05-15-preview"; - private static HttpClient? _sharedHttpClient; - private static HttpClient SharedHttpClient - { - get - { - _sharedHttpClient ??= new HttpClient(); - return _sharedHttpClient; - } - } + private static HttpClient SharedHttpClient => + field ?? + Interlocked.CompareExchange(ref field, new(), null) ?? + field; private static readonly ConcurrentDictionary _serviceUrlCache = new ConcurrentDictionary(); diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs index 47984962598..44ddcf84081 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs @@ -44,9 +44,6 @@ public class DistributedCachingChatClient : CachingChatClient /// Additional values used to inform the cache key employed for storing state. private object[]? _cacheKeyAdditionalValues; - /// The to use when serializing cache data. - private JsonSerializerOptions _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; - /// Initializes a new instance of the class. /// The underlying . /// An instance that will be used as the backing store for the cache. @@ -59,9 +56,9 @@ public DistributedCachingChatClient(IChatClient innerClient, IDistributedCache s /// Gets or sets JSON serialization options to use when serializing cache data. public JsonSerializerOptions JsonSerializerOptions { - get => _jsonSerializerOptions; - set => _jsonSerializerOptions = Throw.IfNull(value); - } + get; + set => field = Throw.IfNull(value); + } = AIJsonUtilities.DefaultOptions; /// Gets or sets additional values used to inform the cache key employed for storing state. /// Any values set in this list will augment the other values used to inform the cache key. @@ -75,11 +72,11 @@ public IReadOnlyList? CacheKeyAdditionalValues protected override async Task ReadCacheAsync(string key, CancellationToken cancellationToken) { _ = Throw.IfNull(key); - _jsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(); if (await _storage.GetAsync(key, cancellationToken) is byte[] existingJson) { - return (ChatResponse?)JsonSerializer.Deserialize(existingJson, _jsonSerializerOptions.GetTypeInfo(typeof(ChatResponse))); + return (ChatResponse?)JsonSerializer.Deserialize(existingJson, JsonSerializerOptions.GetTypeInfo(typeof(ChatResponse))); } return null; @@ -89,11 +86,11 @@ public IReadOnlyList? CacheKeyAdditionalValues protected override async Task?> ReadCacheStreamingAsync(string key, CancellationToken cancellationToken) { _ = Throw.IfNull(key); - _jsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(); if (await _storage.GetAsync(key, cancellationToken) is byte[] existingJson) { - return (IReadOnlyList?)JsonSerializer.Deserialize(existingJson, _jsonSerializerOptions.GetTypeInfo(typeof(IReadOnlyList))); + return (IReadOnlyList?)JsonSerializer.Deserialize(existingJson, JsonSerializerOptions.GetTypeInfo(typeof(IReadOnlyList))); } return null; @@ -104,9 +101,9 @@ protected override async Task WriteCacheAsync(string key, ChatResponse value, Ca { _ = Throw.IfNull(key); _ = Throw.IfNull(value); - _jsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(); - var newJson = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions.GetTypeInfo(typeof(ChatResponse))); + var newJson = JsonSerializer.SerializeToUtf8Bytes(value, JsonSerializerOptions.GetTypeInfo(typeof(ChatResponse))); await _storage.SetAsync(key, newJson, cancellationToken); } @@ -115,9 +112,9 @@ protected override async Task WriteCacheStreamingAsync(string key, IReadOnlyList { _ = Throw.IfNull(key); _ = Throw.IfNull(value); - _jsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(); - var newJson = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions.GetTypeInfo(typeof(IReadOnlyList))); + var newJson = JsonSerializer.SerializeToUtf8Bytes(value, JsonSerializerOptions.GetTypeInfo(typeof(IReadOnlyList))); await _storage.SetAsync(key, newJson, cancellationToken); } @@ -151,7 +148,7 @@ protected override string GetCacheKey(IEnumerable messages, ChatOpt additionalValues.CopyTo(arr.AsSpan(FixedValuesCount)); clientValues.CopyTo(arr, FixedValuesCount + additionalValues.Length); - return AIJsonUtilities.HashDataToString(arr.AsSpan(0, length), _jsonSerializerOptions); + return AIJsonUtilities.HashDataToString(arr.AsSpan(0, length), JsonSerializerOptions); } finally { diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvocationContext.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvocationContext.cs index 0e426615cfd..554918b0a8e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvocationContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvocationContext.cs @@ -17,18 +17,6 @@ public class FunctionInvocationContext /// private static readonly AIFunction _nopFunction = AIFunctionFactory.Create(() => { }, nameof(FunctionInvocationContext)); - /// The chat contents associated with the operation that initiated this function call request. - private IList _messages = Array.Empty(); - - /// The AI function to be invoked. - private AIFunction _function = _nopFunction; - - /// The function call content information associated with this invocation. - private FunctionCallContent? _callContent; - - /// The arguments used with the function. - private AIFunctionArguments? _arguments; - /// Initializes a new instance of the class. public FunctionInvocationContext() { @@ -37,30 +25,30 @@ public FunctionInvocationContext() /// Gets or sets the AI function to be invoked. public AIFunction Function { - get => _function; - set => _function = Throw.IfNull(value); - } + get; + set => field = Throw.IfNull(value); + } = _nopFunction; /// Gets or sets the arguments associated with this invocation. public AIFunctionArguments Arguments { - get => _arguments ??= []; - set => _arguments = Throw.IfNull(value); + get => field ??= []; + set => field = Throw.IfNull(value); } /// Gets or sets the function call content information associated with this invocation. public FunctionCallContent CallContent { - get => _callContent ??= new(string.Empty, _nopFunction.Name, EmptyReadOnlyDictionary.Instance); - set => _callContent = Throw.IfNull(value); + get => field ??= new(string.Empty, _nopFunction.Name, EmptyReadOnlyDictionary.Instance); + set => field = Throw.IfNull(value); } /// Gets or sets the chat contents associated with the operation that initiated this function call request. public IList Messages { - get => _messages; - set => _messages = Throw.IfNull(value); - } + get; + set => field = Throw.IfNull(value); + } = Array.Empty(); /// Gets or sets the chat options associated with the operation that initiated this function call request. public ChatOptions? Options { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 4495d592adb..fdc5ef7a204 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -16,8 +16,6 @@ #pragma warning disable CA2213 // Disposable fields should be disposed #pragma warning disable S3353 // Unchanged local variables should be "const" -#pragma warning disable IDE0031 // Use null propagation, suppressed until repo updates to C# 14 -#pragma warning disable IDE0032 // Use auto property, suppressed until repo updates to C# 14 namespace Microsoft.Extensions.AI; @@ -79,12 +77,6 @@ public partial class FunctionInvokingChatClient : DelegatingChatClient /// This component does not own the instance and should not dispose it. private readonly ActivitySource? _activitySource; - /// Maximum number of roundtrips allowed to the inner client. - private int _maximumIterationsPerRequest = 40; // arbitrary default to prevent runaway execution - - /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing. - private int _maximumConsecutiveErrorsPerRequest = 3; - /// /// Initializes a new instance of the class. /// @@ -178,7 +170,7 @@ public static FunctionInvocationContext? CurrentContext /// public int MaximumIterationsPerRequest { - get => _maximumIterationsPerRequest; + get; set { if (value < 1) @@ -186,9 +178,9 @@ public int MaximumIterationsPerRequest Throw.ArgumentOutOfRangeException(nameof(value)); } - _maximumIterationsPerRequest = value; + field = value; } - } + } = 40; /// /// Gets or sets the maximum number of consecutive iterations that are allowed to fail with an error. @@ -220,9 +212,9 @@ public int MaximumIterationsPerRequest /// public int MaximumConsecutiveErrorsPerRequest { - get => _maximumConsecutiveErrorsPerRequest; - set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0); - } + get; + set => field = Throw.IfLessThan(value, 0); + } = 3; /// Gets or sets a collection of additional tools the client is able to invoke. /// @@ -1430,10 +1422,9 @@ private static (List? approvals, List cm) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs index b79d1d18197..f097c1c9a35 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -45,16 +45,14 @@ public sealed class SummarizingChatReducer : IChatReducer private readonly int _targetCount; private readonly int _thresholdCount; - private string _summarizationPrompt = DefaultSummarizationPrompt; - /// /// Gets or sets the prompt text used for summarization. /// public string SummarizationPrompt { - get => _summarizationPrompt; - set => _summarizationPrompt = Throw.IfNull(value); - } + get; + set => field = Throw.IfNull(value); + } = DefaultSummarizationPrompt; /// /// Initializes a new instance of the class with the specified chat client, @@ -79,7 +77,7 @@ public async Task> ReduceAsync(IEnumerable if (summarizedConversion.ShouldResummarize(_targetCount, _thresholdCount)) { summarizedConversion = await summarizedConversion.ResummarizeAsync( - _chatClient, _targetCount, _summarizationPrompt, cancellationToken); + _chatClient, _targetCount, SummarizationPrompt, cancellationToken); } return summarizedConversion.ToChatMessages(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs index 0302b1a896f..d9faa46f338 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs @@ -10,22 +10,20 @@ internal sealed class ManualHealthCheck : IManualHealthCheck { private static readonly object _lock = new(); - private HealthCheckResult _result; - public HealthCheckResult Result { get { lock (_lock) { - return _result; + return field; } } set { lock (_lock) { - _result = value; + field = value; } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs index db0a8850e20..7f105586ea6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs @@ -16,8 +16,6 @@ namespace Microsoft.Extensions.Http.Resilience; /// public class HttpRetryStrategyOptions : RetryStrategyOptions { - private bool _shouldRetryAfterHeader; - /// /// Initializes a new instance of the class. /// @@ -47,12 +45,12 @@ public HttpRetryStrategyOptions() /// public bool ShouldRetryAfterHeader { - get => _shouldRetryAfterHeader; + get; set { - _shouldRetryAfterHeader = value; + field = value; - if (_shouldRetryAfterHeader) + if (field) { DelayGenerator = args => args.Outcome.Result switch { diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/UriEndpoint.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/UriEndpoint.cs index 2373144556d..028eaca7762 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/UriEndpoint.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/UriEndpoint.cs @@ -6,15 +6,11 @@ namespace Microsoft.Extensions.Http.Resilience; -#pragma warning disable IDE0032 // Use auto property - /// /// Represents a URI-based endpoint. /// public class UriEndpoint { - private Uri? _uri; - /// /// Gets or sets the URL of the endpoint. /// @@ -22,9 +18,5 @@ public class UriEndpoint /// Only schema, domain name, and port are used. The rest of the URL is constructed from the request URL. /// [Required] - public Uri? Uri - { - get => _uri; - set => _uri = value; - } + public Uri? Uri { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs index bb3631909dc..34d9df008ad 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageHelper.cs @@ -18,26 +18,11 @@ namespace Microsoft.Extensions.Logging; [EditorBrowsable(EditorBrowsableState.Never)] public static class LoggerMessageHelper { - [ThreadStatic] - private static LoggerMessageState? _state; - /// /// Gets a thread-local instance of this type. /// - public static LoggerMessageState ThreadLocalState - { - get - { - var result = _state; - if (result == null) - { - result = new(); - _state = result; - } - - return result; - } - } + [field: ThreadStatic] + public static LoggerMessageState ThreadLocalState => field ??= new(); /// /// Enumerates an enumerable into a string. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs index ba2c1646760..5cee914c68a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs @@ -16,12 +16,13 @@ internal sealed class CheckpointTracker : IResettable private readonly Registry _checkpointNames; private readonly int[] _checkpointAdded; private readonly Checkpoint[] _checkpoints; - - private long _timestamp; - private int _numCheckpoints; - public long Elapsed => TimeProvider.GetTimestamp() - _timestamp; + public long Elapsed + { + get => TimeProvider.GetTimestamp() - field; + private set; + } public long Frequency => TimeProvider.TimestampFrequency; @@ -36,7 +37,7 @@ public CheckpointTracker(Registry registry) _checkpointAdded = new int[keyCount]; _checkpoints = new Checkpoint[keyCount]; TimeProvider = TimeProvider.System; - _timestamp = TimeProvider.GetTimestamp(); + Elapsed = TimeProvider.GetTimestamp(); } /// @@ -44,7 +45,7 @@ public CheckpointTracker(Registry registry) /// public bool TryReset() { - _timestamp = TimeProvider.GetTimestamp(); + Elapsed = TimeProvider.GetTimestamp(); _numCheckpoints = 0; Array.Clear(_checkpointAdded, 0, _checkpointAdded.Length); return true; diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs index 0777d03c571..251d7cedc1c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs @@ -22,8 +22,6 @@ internal sealed class LatencyContext : ILatencyContext, IResettable private readonly MeasureTracker _measureTracker; - private long _duration; - public LatencyContext(LatencyContextPool latencyContextPool) { var latencyInstrumentProvider = latencyContextPool.LatencyInstrumentProvider; @@ -36,7 +34,11 @@ public LatencyContext(LatencyContextPool latencyContextPool) public LatencyData LatencyData => IsDisposed ? default : new(_tagCollection.Tags, _checkpointTracker.Checkpoints, _measureTracker.Measures, Duration, _checkpointTracker.Frequency); - private long Duration => IsRunning ? _checkpointTracker.Elapsed : _duration; + private long Duration + { + get => IsRunning ? _checkpointTracker.Elapsed : field; + set; + } #region Checkpoints public void AddCheckpoint(CheckpointToken token) @@ -82,7 +84,7 @@ public void Freeze() if (IsRunning) { IsRunning = false; - _duration = _checkpointTracker.Elapsed; + Duration = _checkpointTracker.Elapsed; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.LegacyTagJoiner.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.LegacyTagJoiner.cs index 46bf77f0816..3f75af17a34 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.LegacyTagJoiner.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.LegacyTagJoiner.cs @@ -21,7 +21,6 @@ internal sealed class LegacyTagJoiner : IReadOnlyList> _extraTags = new(TagCapacity); private IReadOnlyList>? _incomingTags; - private int _incomingTagCount; public LegacyTagJoiner() { @@ -34,7 +33,7 @@ public void Clear() { _extraTags.Clear(); _incomingTags = null; - _incomingTagCount = 0; + Count = 0; State = null; Formatter = null; } @@ -43,7 +42,7 @@ public void Clear() public void SetIncomingTags(IReadOnlyList> value) { _incomingTags = value; - _incomingTagCount = _incomingTags.Count; + Count = _incomingTags.Count; } public KeyValuePair this[int index] @@ -77,7 +76,11 @@ public void SetIncomingTags(IReadOnlyList> value) } } - public int Count => _incomingTagCount + _extraTags.Count + StaticTags!.Length; + public int Count + { + get => field + _extraTags.Count + StaticTags!.Length; + private set; + } public IEnumerator> GetEnumerator() { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.ThreadLocals.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.ThreadLocals.cs index 77e1ddc1fd1..21cf296e62c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.ThreadLocals.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.ThreadLocals.cs @@ -5,43 +5,11 @@ namespace Microsoft.Extensions.Logging; -#pragma warning disable S2696 - internal sealed partial class ExtendedLogger : ILogger { - [ThreadStatic] - private static ModernTagJoiner? _modernJoiner; - - [ThreadStatic] - private static LegacyTagJoiner? _legacyJoiner; - - private static ModernTagJoiner ModernJoiner - { - get - { - var joiner = _modernJoiner; - if (joiner == null) - { - joiner = new(); - _modernJoiner = joiner; - } - - return joiner; - } - } - - private static LegacyTagJoiner LegacyJoiner - { - get - { - var joiner = _legacyJoiner; - if (joiner == null) - { - joiner = new(); - _legacyJoiner = joiner; - } + [field: ThreadStatic] + private static ModernTagJoiner ModernJoiner => field ??= new(); - return joiner; - } - } + [field: ThreadStatic] + private static LegacyTagJoiner LegacyJoiner => field ??= new(); } diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs index b9404afa30e..cfe601e9dfa 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs @@ -19,7 +19,6 @@ public class FakeTimeProvider : TimeProvider private DateTimeOffset _now = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); private TimeZoneInfo _localTimeZone = TimeZoneInfo.Utc; private volatile int _wakeWaitersGate; - private TimeSpan _autoAdvanceAmount; /// /// Initializes a new instance of the class. @@ -62,11 +61,11 @@ public FakeTimeProvider(DateTimeOffset startDateTime) /// The time value is less than . public TimeSpan AutoAdvanceAmount { - get => _autoAdvanceAmount; + get; set { _ = Throw.IfLessThan(value.Ticks, 0); - _autoAdvanceAmount = value; + field = value; } } @@ -78,7 +77,7 @@ public override DateTimeOffset GetUtcNow() lock (Waiters) { result = _now; - _now += _autoAdvanceAmount; + _now += AutoAdvanceAmount; } WakeWaiters(); diff --git a/src/Shared/.editorconfig b/src/Shared/.editorconfig index 59e9e2090d9..defd1d59afc 100644 --- a/src/Shared/.editorconfig +++ b/src/Shared/.editorconfig @@ -1991,7 +1991,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this +dotnet_diagnostic.IDE0032.severity = warning dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name @@ -5868,7 +5868,7 @@ dotnet_diagnostic.SA1414.severity = warning # Title : Braces for multi-line statements should not share line # Category : StyleCop.CSharp.LayoutRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md -dotnet_diagnostic.SA1500.severity = warning +dotnet_diagnostic.SA1500.severity = suggestion # rule does not work well with field-based property initializers # Title : Statement should not be on a single line # Category : StyleCop.CSharp.LayoutRules @@ -5936,7 +5936,7 @@ dotnet_diagnostic.SA1512.severity = none # Title : Closing brace should be followed by blank line # Category : StyleCop.CSharp.LayoutRules # Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md -dotnet_diagnostic.SA1513.severity = warning +dotnet_diagnostic.SA1513.severity = suggestion # rule does not work well with field-based property initializers # Title : Element documentation header should be preceded by blank line # Category : StyleCop.CSharp.LayoutRules diff --git a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.JsonSchema.cs b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.JsonSchema.cs index a395c133980..5380d208259 100644 --- a/src/Shared/JsonSchemaExporter/JsonSchemaExporter.JsonSchema.cs +++ b/src/Shared/JsonSchemaExporter/JsonSchemaExporter.JsonSchema.cs @@ -35,244 +35,204 @@ private JsonSchema(bool trueOrFalse) public string? Schema { - get => _schema; + get; set { VerifyMutable(); - _schema = value; + field = value; } } - private string? _schema; - public string? Title { - get => _title; + get; set { VerifyMutable(); - _title = value; + field = value; } } - private string? _title; - public string? Description { - get => _description; + get; set { VerifyMutable(); - _description = value; + field = value; } } - private string? _description; - public string? Ref { - get => _ref; + get; set { VerifyMutable(); - _ref = value; + field = value; } } - private string? _ref; - public string? Comment { - get => _comment; + get; set { VerifyMutable(); - _comment = value; + field = value; } } - private string? _comment; - public JsonSchemaType Type { - get => _type; + get; set { VerifyMutable(); - _type = value; + field = value; } - } - - private JsonSchemaType _type = JsonSchemaType.Any; + } = JsonSchemaType.Any; public string? Format { - get => _format; + get; set { VerifyMutable(); - _format = value; + field = value; } } - private string? _format; - public string? Pattern { - get => _pattern; + get; set { VerifyMutable(); - _pattern = value; + field = value; } } - private string? _pattern; - public JsonNode? Constant { - get => _constant; + get; set { VerifyMutable(); - _constant = value; + field = value; } } - private JsonNode? _constant; - public List>? Properties { - get => _properties; + get; set { VerifyMutable(); - _properties = value; + field = value; } } - private List>? _properties; - public List? Required { - get => _required; + get; set { VerifyMutable(); - _required = value; + field = value; } } - private List? _required; - public JsonSchema? Items { - get => _items; + get; set { VerifyMutable(); - _items = value; + field = value; } } - private JsonSchema? _items; - public JsonSchema? AdditionalProperties { - get => _additionalProperties; + get; set { VerifyMutable(); - _additionalProperties = value; + field = value; } } - private JsonSchema? _additionalProperties; - public JsonArray? Enum { - get => _enum; + get; set { VerifyMutable(); - _enum = value; + field = value; } } - private JsonArray? _enum; - public JsonSchema? Not { - get => _not; + get; set { VerifyMutable(); - _not = value; + field = value; } } - private JsonSchema? _not; - public List? AnyOf { - get => _anyOf; + get; set { VerifyMutable(); - _anyOf = value; + field = value; } } - private List? _anyOf; - public bool HasDefaultValue { - get => _hasDefaultValue; + get; set { VerifyMutable(); - _hasDefaultValue = value; + field = value; } } - private bool _hasDefaultValue; - public JsonNode? DefaultValue { - get => _defaultValue; + get; set { VerifyMutable(); - _defaultValue = value; + field = value; } } - private JsonNode? _defaultValue; - public int? MinLength { - get => _minLength; + get; set { VerifyMutable(); - _minLength = value; + field = value; } } - private int? _minLength; - public int? MaxLength { - get => _maxLength; + get; set { VerifyMutable(); - _maxLength = value; + field = value; } } - private int? _maxLength; - public JsonSchemaExporterContext? GenerationContext { get; set; } public int KeywordCount diff --git a/src/Shared/ServerSentEvents/SseItem.cs b/src/Shared/ServerSentEvents/SseItem.cs index 9c6092fd3cf..013d2fe9098 100644 --- a/src/Shared/ServerSentEvents/SseItem.cs +++ b/src/Shared/ServerSentEvents/SseItem.cs @@ -17,12 +17,6 @@ internal readonly struct SseItem [EditorBrowsable(EditorBrowsableState.Never)] internal readonly string? _eventType; - /// The event's id. - private readonly string? _eventId; - - /// The event's reconnection interval. - private readonly TimeSpan? _reconnectionInterval; - /// Initializes a new instance of the struct. /// The event's payload. /// The event's type. @@ -48,7 +42,7 @@ public SseItem(T data, string? eventType = null) /// Thrown when the value contains a line break. public string? EventId { - get => _eventId; + get; init { if (value.AsSpan().ContainsLineBreaks() is true) @@ -56,7 +50,7 @@ public string? EventId ThrowHelper.ThrowArgumentException_CannotContainLineBreaks(nameof(EventId)); } - _eventId = value; + field = value; } } @@ -66,7 +60,7 @@ public string? EventId /// public TimeSpan? ReconnectionInterval { - get => _reconnectionInterval; + get; init { if (value < TimeSpan.Zero) @@ -74,7 +68,7 @@ public TimeSpan? ReconnectionInterval ThrowHelper.ThrowArgumentException_CannotBeNegative(nameof(ReconnectionInterval)); } - _reconnectionInterval = value; + field = value; } } } diff --git a/test/.editorconfig b/test/.editorconfig index ca9523939db..e98d1472b6a 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -1981,7 +1981,7 @@ dotnet_style_null_propagation = true # Title : Use auto property # Category : Style # Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 -dotnet_diagnostic.IDE0032.severity = silent # https://github.com/dotnet/extensions/issues/6864 tracks re-enabling this +dotnet_diagnostic.IDE0032.severity = warning dotnet_style_prefer_auto_properties = true # Title : Use explicitly provided tuple name diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs index 7bc5ddc95f2..15000ccb21a 100644 --- a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs @@ -23,13 +23,10 @@ public sealed class HeaderParsingFeatureTests private readonly IOptions _options; private readonly IServiceCollection _services; private readonly FakeLogger _logger = new(); - private IHeaderRegistry? _registry; - private HttpContext? _context; - private IHeaderRegistry Registry => _registry ??= new HeaderRegistry(_services.BuildServiceProvider(), _options); + private IHeaderRegistry Registry => field ??= new HeaderRegistry(_services.BuildServiceProvider(), _options); - private HttpContext Context - => _context ??= new DefaultHttpContext { RequestServices = _services.BuildServiceProvider() }; + private HttpContext Context => field ??= new DefaultHttpContext { RequestServices = _services.BuildServiceProvider() }; public HeaderParsingFeatureTests() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs index 25ee9d544cb..7be73a05c10 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Settings.cs @@ -57,16 +57,7 @@ public Settings(IConfiguration config) #pragma warning restore CA2208 } - private static Settings? _currentSettings; - - public static Settings Current - { - get - { - _currentSettings ??= GetCurrentSettings(); - return _currentSettings; - } - } + public static Settings Current => field ??= GetCurrentSettings(); private static Settings GetCurrentSettings() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/Settings.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/Settings.cs index 366e4549748..1118d7f6624 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/Settings.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Tests/Settings.cs @@ -27,16 +27,7 @@ public Settings(IConfiguration config) #pragma warning restore CA2208 } - private static Settings? _currentSettings; - - public static Settings Current - { - get - { - _currentSettings ??= GetCurrentSettings(); - return _currentSettings; - } - } + public static Settings Current => field ??= GetCurrentSettings(); private static Settings GetCurrentSettings() { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/Project.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/Project.cs index 38ced5b1867..317e81a661f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/Project.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/Project.cs @@ -8,30 +8,31 @@ namespace Microsoft.Extensions.AI.Templates.Tests; public sealed class Project(string rootPath, string name) { - private string? _startupProjectRelativePath; - private string? _startupProjectFullPath; - public string RootPath => rootPath; public string Name => name; public string? StartupProjectRelativePath { - get => _startupProjectRelativePath; + get; set { if (value is null) { - _startupProjectRelativePath = null; - _startupProjectFullPath = null; + field = null; + StartupProjectFullPath = null!; } - else if (!string.Equals(value, _startupProjectRelativePath, StringComparison.Ordinal)) + else if (!string.Equals(value, field, StringComparison.Ordinal)) { - _startupProjectRelativePath = value; - _startupProjectFullPath = Path.Combine(rootPath, _startupProjectRelativePath); + field = value; + StartupProjectFullPath = Path.Combine(rootPath, field); } } } - public string StartupProjectFullPath => _startupProjectFullPath ?? rootPath; + public string StartupProjectFullPath + { + get => field ?? rootPath; + private set; + } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/TestCommandResult.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/TestCommandResult.cs index 09d09d50a1c..4b5e2dd2a28 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/TestCommandResult.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Infrastructure/TestCommandResult.cs @@ -7,12 +7,9 @@ namespace Microsoft.Extensions.AI.Templates.Tests; public sealed class TestCommandResult(StringBuilder standardOutputBuilder, StringBuilder standardErrorBuilder, int exitCode) { - private string? _standardOutput; - private string? _standardError; + public string StandardOutput => field ??= standardOutputBuilder.ToString(); - public string StandardOutput => _standardOutput ??= standardOutputBuilder.ToString(); - - public string StandardError => _standardError ??= standardErrorBuilder.ToString(); + public string StandardError => field ??= standardErrorBuilder.ToString(); public int ExitCode => exitCode; } From 76737b41479c8ef0e525b7aa7529f466e9650ede Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 30 Sep 2025 03:10:33 -0400 Subject: [PATCH 327/472] Add OpenTelemetrySpeechToTextClient and friends (#6845) This is basically the chat client OpenTelemetry client copy/pasted/tweaked to compile. The otel spec doesn't have anything specific to this modality yet, so this is making best guesses on what things should be and also being minimal in what's tracked. --- ...gingSpeechToTextClientBuilderExtensions.cs | 2 +- .../OpenTelemetrySpeechToTextClient.cs | 367 ++++++++++++++++++ ...etrySpeechToTextClientBuilderExtensions.cs | 42 ++ .../SpeechToText/SpeechToTextClientBuilder.cs | 4 +- .../OpenTelemetrySpeechToTextClientTests.cs | 150 +++++++ 5 files changed, 562 insertions(+), 3 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClient.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClientBuilderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/OpenTelemetrySpeechToTextClientTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs index 92a67189982..54ed411bd35 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/LoggingSpeechToTextClientBuilderExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public static class LoggingSpeechToTextClientBuilderExtensions { - /// Adds logging to the audio transcription client pipeline. + /// Adds logging to the speech-to-text client pipeline. /// The . /// /// An optional used to create a logger with which logging should be performed. diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClient.cs new file mode 100644 index 00000000000..40461ebe457 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClient.cs @@ -0,0 +1,367 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable S3358 // Ternary operators should not be nested +#pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter +#pragma warning disable SA1113 // Comma should be on the same line as previous parameter + +namespace Microsoft.Extensions.AI; + +/// Represents a delegating speech-to-text client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. +/// +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.37, defined at . +/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. +/// +[Experimental("MEAI001")] +public sealed class OpenTelemetrySpeechToTextClient : DelegatingSpeechToTextClient +{ + private readonly ActivitySource _activitySource; + private readonly Meter _meter; + + private readonly Histogram _tokenUsageHistogram; + private readonly Histogram _operationDurationHistogram; + + private readonly string? _defaultModelId; + private readonly string? _providerName; + private readonly string? _serverAddress; + private readonly int _serverPort; + + /// Initializes a new instance of the class. + /// The underlying . + /// The to use for emitting any logging data from the client. + /// An optional source name that will be used on the telemetry data. +#pragma warning disable IDE0060 // Remove unused parameter; it exists for consistency with IChatClient and future use + public OpenTelemetrySpeechToTextClient(ISpeechToTextClient innerClient, ILogger? logger = null, string? sourceName = null) +#pragma warning restore IDE0060 + : base(innerClient) + { + Debug.Assert(innerClient is not null, "Should have been validated by the base ctor"); + + if (innerClient!.GetService() is SpeechToTextClientMetadata metadata) + { + _defaultModelId = metadata.DefaultModelId; + _providerName = metadata.ProviderName; + _serverAddress = metadata.ProviderUri?.Host; + _serverPort = metadata.ProviderUri?.Port ?? 0; + } + + string name = string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!; + _activitySource = new(name); + _meter = new(name); + + _tokenUsageHistogram = _meter.CreateHistogram( + OpenTelemetryConsts.GenAI.Client.TokenUsage.Name, + OpenTelemetryConsts.TokensUnit, + OpenTelemetryConsts.GenAI.Client.TokenUsage.Description +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.TokenUsage.ExplicitBucketBoundaries } +#endif + ); + + _operationDurationHistogram = _meter.CreateHistogram( + OpenTelemetryConsts.GenAI.Client.OperationDuration.Name, + OpenTelemetryConsts.SecondsUnit, + OpenTelemetryConsts.GenAI.Client.OperationDuration.Description +#if NET9_0_OR_GREATER + , advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.OperationDuration.ExplicitBucketBoundaries } +#endif + ); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activitySource.Dispose(); + _meter.Dispose(); + } + + base.Dispose(disposing); + } + + /// + /// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry. + /// + /// + /// if potentially sensitive information should be included in telemetry; + /// if telemetry shouldn't include raw inputs and outputs. + /// The default value is , unless the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT + /// environment variable is set to "true" (case-insensitive). + /// + /// + /// By default, telemetry includes metadata, such as token counts, but not raw inputs + /// and outputs, such as message content, function call arguments, and function call results. + /// The default value can be overridden by setting the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT + /// environment variable to "true". Explicitly setting this property will override the environment variable. + /// + public bool EnableSensitiveData { get; set; } = TelemetryHelpers.EnableSensitiveDataDefault; + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) => + serviceType == typeof(ActivitySource) ? _activitySource : + base.GetService(serviceType, serviceKey); + + /// + public override async Task GetTextAsync(Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(audioSpeechStream); + + using Activity? activity = CreateAndConfigureActivity(options); + Stopwatch? stopwatch = _operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null; + string? requestModelId = options?.ModelId ?? _defaultModelId; + + SpeechToTextResponse? response = null; + Exception? error = null; + try + { + response = await base.GetTextAsync(audioSpeechStream, options, cancellationToken); + return response; + } + catch (Exception ex) + { + error = ex; + throw; + } + finally + { + TraceResponse(activity, requestModelId, response, error, stopwatch); + } + } + + /// + public override async IAsyncEnumerable GetStreamingTextAsync( + Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(audioSpeechStream); + + using Activity? activity = CreateAndConfigureActivity(options); + Stopwatch? stopwatch = _operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null; + string? requestModelId = options?.ModelId ?? _defaultModelId; + + IAsyncEnumerable updates; + try + { + updates = base.GetStreamingTextAsync(audioSpeechStream, options, cancellationToken); + } + catch (Exception ex) + { + TraceResponse(activity, requestModelId, response: null, ex, stopwatch); + throw; + } + + var responseEnumerator = updates.GetAsyncEnumerator(cancellationToken); + List trackedUpdates = []; + Exception? error = null; + try + { + while (true) + { + SpeechToTextResponseUpdate update; + try + { + if (!await responseEnumerator.MoveNextAsync()) + { + break; + } + + update = responseEnumerator.Current; + } + catch (Exception ex) + { + error = ex; + throw; + } + + trackedUpdates.Add(update); + yield return update; + Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802 + } + } + finally + { + TraceResponse(activity, requestModelId, trackedUpdates.ToSpeechToTextResponse(), error, stopwatch); + + await responseEnumerator.DisposeAsync(); + } + } + + /// Creates an activity for a speech-to-text request, or returns if not enabled. + private Activity? CreateAndConfigureActivity(SpeechToTextOptions? options) + { + Activity? activity = null; + if (_activitySource.HasListeners()) + { + string? modelId = options?.ModelId ?? _defaultModelId; + + activity = _activitySource.StartActivity( + string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContentName : $"{OpenTelemetryConsts.GenAI.GenerateContentName} {modelId}", + ActivityKind.Client); + + if (activity is { IsAllDataRequested: true }) + { + _ = activity + .AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName) + .AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId) + .AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName) + .AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeText); + + if (_serverAddress is not null) + { + _ = activity + .AddTag(OpenTelemetryConsts.Server.Address, _serverAddress) + .AddTag(OpenTelemetryConsts.Server.Port, _serverPort); + } + + if (options is not null) + { + if (EnableSensitiveData) + { + // Log all additional request options as raw values on the span. + // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data. + if (options.AdditionalProperties is { } props) + { + foreach (KeyValuePair prop in props) + { + _ = activity.AddTag(prop.Key, prop.Value); + } + } + } + } + } + } + + return activity; + } + + /// Adds speech-to-text response information to the activity. + private void TraceResponse( + Activity? activity, + string? requestModelId, + SpeechToTextResponse? response, + Exception? error, + Stopwatch? stopwatch) + { + if (_operationDurationHistogram.Enabled && stopwatch is not null) + { + TagList tags = default; + + AddMetricTags(ref tags, requestModelId, response); + if (error is not null) + { + tags.Add(OpenTelemetryConsts.Error.Type, error.GetType().FullName); + } + + _operationDurationHistogram.Record(stopwatch.Elapsed.TotalSeconds, tags); + } + + if (_tokenUsageHistogram.Enabled && response?.Usage is { } usage) + { + if (usage.InputTokenCount is long inputTokens) + { + TagList tags = default; + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeInput); + AddMetricTags(ref tags, requestModelId, response); + _tokenUsageHistogram.Record((int)inputTokens, tags); + } + + if (usage.OutputTokenCount is long outputTokens) + { + TagList tags = default; + tags.Add(OpenTelemetryConsts.GenAI.Token.Type, OpenTelemetryConsts.TokenTypeOutput); + AddMetricTags(ref tags, requestModelId, response); + _tokenUsageHistogram.Record((int)outputTokens, tags); + } + } + + if (error is not null) + { + _ = activity? + .AddTag(OpenTelemetryConsts.Error.Type, error.GetType().FullName) + .SetStatus(ActivityStatusCode.Error, error.Message); + } + + if (response is not null) + { + AddOutputMessagesTags(response, activity); + + if (activity is not null) + { + if (!string.IsNullOrWhiteSpace(response.ResponseId)) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Id, response.ResponseId); + } + + if (response.ModelId is not null) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Model, response.ModelId); + } + + if (response.Usage?.InputTokenCount is long inputTokens) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.InputTokens, (int)inputTokens); + } + + if (response.Usage?.OutputTokenCount is long outputTokens) + { + _ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.OutputTokens, (int)outputTokens); + } + + // Log all additional response properties as raw values on the span. + // Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data. + if (EnableSensitiveData && response.AdditionalProperties is { } props) + { + foreach (KeyValuePair prop in props) + { + _ = activity.AddTag(prop.Key, prop.Value); + } + } + } + } + + void AddMetricTags(ref TagList tags, string? requestModelId, SpeechToTextResponse? response) + { + tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName); + + if (requestModelId is not null) + { + tags.Add(OpenTelemetryConsts.GenAI.Request.Model, requestModelId); + } + + tags.Add(OpenTelemetryConsts.GenAI.Provider.Name, _providerName); + + if (_serverAddress is string endpointAddress) + { + tags.Add(OpenTelemetryConsts.Server.Address, endpointAddress); + tags.Add(OpenTelemetryConsts.Server.Port, _serverPort); + } + + if (response?.ModelId is string responseModel) + { + tags.Add(OpenTelemetryConsts.GenAI.Response.Model, responseModel); + } + } + } + + private void AddOutputMessagesTags(SpeechToTextResponse response, Activity? activity) + { + if (EnableSensitiveData && activity is { IsAllDataRequested: true }) + { + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Output.Messages, + OpenTelemetryChatClient.SerializeChatMessages([new(ChatRole.Assistant, response.Contents)])); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClientBuilderExtensions.cs new file mode 100644 index 00000000000..5e23a41358e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/OpenTelemetrySpeechToTextClientBuilderExtensions.cs @@ -0,0 +1,42 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Provides extensions for configuring instances. +[Experimental("MEAI001")] +public static class OpenTelemetrySpeechToTextClientBuilderExtensions +{ + /// + /// Adds OpenTelemetry support to the speech-to-text client pipeline, following the OpenTelemetry Semantic Conventions for Generative AI systems. + /// + /// + /// The draft specification this follows is available at . + /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. + /// + /// The . + /// An optional to use to create a logger for logging events. + /// An optional source name that will be used on the telemetry data. + /// An optional callback that can be used to configure the instance. + /// The . + public static SpeechToTextClientBuilder UseOpenTelemetry( + this SpeechToTextClientBuilder builder, + ILoggerFactory? loggerFactory = null, + string? sourceName = null, + Action? configure = null) => + Throw.IfNull(builder).Use((innerClient, services) => + { + loggerFactory ??= services.GetService(); + + var client = new OpenTelemetrySpeechToTextClient(innerClient, loggerFactory?.CreateLogger(typeof(OpenTelemetrySpeechToTextClient)), sourceName); + configure?.Invoke(client); + + return client; + }); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs index dae4224a94d..1945a140762 100644 --- a/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.AI/SpeechToText/SpeechToTextClientBuilder.cs @@ -58,7 +58,7 @@ public ISpeechToTextClient Build(IServiceProvider? services = null) return audioClient; } - /// Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline. + /// Adds a factory for an intermediate speech-to-text client to the speech-to-text client pipeline. /// The client factory function. /// The updated instance. public SpeechToTextClientBuilder Use(Func clientFactory) @@ -68,7 +68,7 @@ public SpeechToTextClientBuilder Use(Func clientFactory(innerClient)); } - /// Adds a factory for an intermediate audio transcription client to the audio transcription client pipeline. + /// Adds a factory for an intermediate speech-to-text client to the speech-to-text client pipeline. /// The client factory function. /// The updated instance. public SpeechToTextClientBuilder Use(Func clientFactory) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/OpenTelemetrySpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/OpenTelemetrySpeechToTextClientTests.cs new file mode 100644 index 00000000000..c243bf2bf12 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/SpeechToText/OpenTelemetrySpeechToTextClientTests.cs @@ -0,0 +1,150 @@ +// 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; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Trace; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class OpenTelemetrySpeechToTextClientTests +{ + [Fact] + public void InvalidArgs_Throws() + { + Assert.Throws("innerClient", () => new OpenTelemetrySpeechToTextClient(null!)); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task ExpectedInformationLogged_Async(bool streaming, bool enableSensitiveData) + { + var sourceName = Guid.NewGuid().ToString(); + var activities = new List(); + using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddInMemoryExporter(activities) + .Build(); + + using var innerClient = new TestSpeechToTextClient + { + GetTextAsyncCallback = async (request, options, cancellationToken) => + { + await Task.Yield(); + return new("This is the recognized text.") + { + Usage = new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + }, + }; + }, + + GetStreamingTextAsyncCallback = TestClientStreamAsync, + + GetServiceCallback = (serviceType, serviceKey) => + serviceType == typeof(SpeechToTextClientMetadata) ? new SpeechToTextClientMetadata("testservice", new Uri("http://localhost:12345/something"), "amazingmodel") : + null, + }; + + static async IAsyncEnumerable TestClientStreamAsync( + Stream request, SpeechToTextOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.Yield(); + yield return new("This is"); + yield return new(" the recognized"); + yield return new() + { + Contents = + [ + new TextContent(" text."), + new UsageContent(new() + { + InputTokenCount = 10, + OutputTokenCount = 20, + TotalTokenCount = 30, + }), + ] + }; + } + + using var client = innerClient + .AsBuilder() + .UseOpenTelemetry(null, sourceName, configure: instance => + { + instance.EnableSensitiveData = enableSensitiveData; + }) + .Build(); + + SpeechToTextOptions options = new() + { + ModelId = "mycoolspeechmodel", + AdditionalProperties = new() + { + ["service_tier"] = "value1", + ["SomethingElse"] = "value2", + }, + }; + + var response = streaming ? + await client.GetStreamingTextAsync(Stream.Null, options).ToSpeechToTextResponseAsync() : + await client.GetTextAsync(Stream.Null, options); + + var activity = Assert.Single(activities); + + Assert.NotNull(activity.Id); + Assert.NotEmpty(activity.Id); + + Assert.Equal("localhost", activity.GetTagItem("server.address")); + Assert.Equal(12345, (int)activity.GetTagItem("server.port")!); + + Assert.Equal("generate_content mycoolspeechmodel", activity.DisplayName); + Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name")); + + Assert.Equal("mycoolspeechmodel", activity.GetTagItem("gen_ai.request.model")); + Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("service_tier")); + Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("SomethingElse")); + + Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens")); + Assert.Equal(20, activity.GetTagItem("gen_ai.usage.output_tokens")); + + Assert.True(activity.Duration.TotalMilliseconds > 0); + + var tags = activity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + if (enableSensitiveData) + { + Assert.Equal(ReplaceWhitespace(""" + [ + { + "role": "assistant", + "parts": [ + { + "type": "text", + "content": "This is the recognized text." + } + ] + } + ] + """), ReplaceWhitespace(tags["gen_ai.output.messages"])); + } + else + { + Assert.False(tags.ContainsKey("gen_ai.output.messages")); + } + + static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); + } +} From 0df2dc11014e3d60ae27c32267be398cb807d93e Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 12:25:48 -0500 Subject: [PATCH 328/472] Get ServiceDiscovery libraries and tests building clean in dotnet/extensions. For now, disable analyzers in the projects to get them building. --- eng/Version.Details.xml | 12 ++ eng/Versions.props | 5 + eng/packages/General-LTS.props | 3 + eng/packages/General-net9.props | 3 + eng/packages/General.props | 3 + ...sions.ServiceDiscovery.Abstractions.csproj | 12 +- ...xtensions.ServiceDiscovery.Abstractions.cs | 71 --------- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 7 +- ...crosoft.Extensions.ServiceDiscovery.Dns.cs | 52 ------- ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 6 +- ...rosoft.Extensions.ServiceDiscovery.Yarp.cs | 19 --- ...crosoft.Extensions.ServiceDiscovery.csproj | 9 +- .../Microsoft.Extensions.ServiceDiscovery.cs | 68 -------- .../CallerArgumentExpressionAttribute.cs | 10 -- src/Shared/FxPolyfills/FxPolyfills.targets | 2 + src/Shared/FxPolyfills/IsExternalInit.cs | 8 - ....ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 5 +- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 14 +- .../Resolver/CancellationTests.cs | 2 +- .../Resolver/LoopbackDnsTestBase.cs | 9 +- .../Resolver/ResolveAddressesTests.cs | 4 +- .../Resolver/ResolveServiceTests.cs | 2 +- .../Resolver/RetryTests.cs | 2 +- .../Resolver/TcpFailoverTests.cs | 2 +- .../XunitLoggerFactoryExtensions.cs | 145 ++++++++++++++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 10 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 13 +- 27 files changed, 227 insertions(+), 271 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs delete mode 100644 src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs delete mode 100644 src/Shared/FxPolyfills/IsExternalInit.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e4f5ea11353..3b5a0c53b49 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,6 +4,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 893c2ebbd49952ca49e93298148af2d95a61a0a4 + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 @@ -80,6 +84,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 893c2ebbd49952ca49e93298148af2d95a61a0a4 + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 @@ -180,6 +188,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore ff66c263be7ed395794bdaf616322977b8ec897c + + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore + ff66c263be7ed395794bdaf616322977b8ec897c + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore ff66c263be7ed395794bdaf616322977b8ec897c diff --git a/eng/Versions.props b/eng/Versions.props index 91a09df773f..22d61962791 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,6 +34,7 @@ 9.0.9 + 9.0.9 9.0.9 9.0.9 9.0.9 @@ -53,6 +54,7 @@ 9.0.9 9.0.9 9.0.9 + 9.0.9 9.0.9 9.0.9 9.0.9 @@ -78,6 +80,7 @@ 9.0.9 9.0.9 9.0.9 + 9.0.9 9.0.9 9.0.9 @@ -107,6 +110,7 @@ 8.0.1 8.0.0 8.0.2 + 8.0.0 8.0.20 8.0.20 8.0.0 @@ -132,6 +136,7 @@ 8.0.20 8.0.20 8.0.20 + 8.0.20 8.0.20 8.0.20 diff --git a/eng/packages/General-LTS.props b/eng/packages/General-LTS.props index 884d874c5e1..e5e06d632de 100644 --- a/eng/packages/General-LTS.props +++ b/eng/packages/General-LTS.props @@ -4,6 +4,7 @@ of the framework, we should use the following LTS versions instead --> + @@ -17,6 +18,7 @@ + @@ -28,6 +30,7 @@ + diff --git a/eng/packages/General-net9.props b/eng/packages/General-net9.props index 341f69458a8..e3ff1198cec 100644 --- a/eng/packages/General-net9.props +++ b/eng/packages/General-net9.props @@ -4,6 +4,7 @@ of the framework, the following versions should be used. --> + @@ -17,6 +18,7 @@ + @@ -28,6 +30,7 @@ + diff --git a/eng/packages/General.props b/eng/packages/General.props index 5be4031ad4d..7a5bd0d46a0 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -5,6 +5,7 @@ + @@ -21,6 +22,7 @@ + @@ -33,6 +35,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index c32fb4c87e5..494e1cfbfbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,12 +1,14 @@ - + - netstandard2.0;net462;$(DefaultTargetFramework) + $(TargetFrameworks);netstandard2.0 true - true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery + + $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 + enable @@ -22,7 +24,7 @@ - - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs deleted file mode 100644 index a7ed4ec5404..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.ServiceDiscovery -{ - public partial interface IHostNameFeature - { - string HostName { get; } - } - - public partial interface IServiceEndpointBuilder - { - System.Collections.Generic.IList Endpoints { get; } - - AspNetCore.Http.Features.IFeatureCollection Features { get; } - - void AddChangeToken(Primitives.IChangeToken changeToken); - } - - public partial interface IServiceEndpointProvider : System.IAsyncDisposable - { - System.Threading.Tasks.ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, System.Threading.CancellationToken cancellationToken); - } - - public partial interface IServiceEndpointProviderFactory - { - bool TryCreateProvider(ServiceEndpointQuery query, out IServiceEndpointProvider? provider); - } - - public abstract partial class ServiceEndpoint - { - public abstract System.Net.EndPoint EndPoint { get; } - public abstract AspNetCore.Http.Features.IFeatureCollection Features { get; } - - public static ServiceEndpoint Create(System.Net.EndPoint endPoint, AspNetCore.Http.Features.IFeatureCollection? features = null) { throw null; } - } - - public sealed partial class ServiceEndpointQuery - { - internal ServiceEndpointQuery() { } - - public string? EndpointName { get { throw null; } } - - public System.Collections.Generic.IReadOnlyList IncludedSchemes { get { throw null; } } - - public string ServiceName { get { throw null; } } - - public override string? ToString() { throw null; } - - public static bool TryParse(string input, out ServiceEndpointQuery? query) { throw null; } - } - - [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] - public sealed partial class ServiceEndpointSource - { - public ServiceEndpointSource(System.Collections.Generic.List? endpoints, Primitives.IChangeToken changeToken, AspNetCore.Http.Features.IFeatureCollection features) { } - - public Primitives.IChangeToken ChangeToken { get { throw null; } } - - public System.Collections.Generic.IReadOnlyList Endpoints { get { throw null; } } - - public AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } - - public override string ToString() { throw null; } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 6d8bbb47842..f1a59b08dac 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,11 +1,14 @@ - $(DefaultTargetFramework) + $(NetCoreTargetFrameworks) true - true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. $(DefaultDotnetIconFullPath) + + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1515;SA1600;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable + false diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs deleted file mode 100644 index 15f99b179ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs +++ /dev/null @@ -1,52 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.Hosting -{ - public static partial class ServiceDiscoveryDnsServiceCollectionExtensions - { - public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery.Dns -{ - public partial class DnsServiceEndpointProviderOptions - { - public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } - - public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } - - public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } - - public double RetryBackOffFactor { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } - - public partial class DnsSrvServiceEndpointProviderOptions - { - public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } - - public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } - - public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } - - public string? QuerySuffix { get { throw null; } set { } } - - public double RetryBackOffFactor { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 74870a87668..16da6587759 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -1,13 +1,15 @@ - $(DefaultTargetFramework) + $(NetCoreTargetFrameworks) enable enable true - true Provides extensions for service discovery for the YARP reverse proxy. $(DefaultDotnetIconFullPath) + + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S2692;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1414;SA1515;SA1600;SA1615;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs deleted file mode 100644 index fc608f86a92..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs +++ /dev/null @@ -1,19 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection -{ - public static partial class ServiceDiscoveryReverseProxyServiceCollectionExtensions - { - public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { throw null; } - - public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { throw null; } - - public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { throw null; } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 2556df195e6..d501e3e22ad 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,11 +1,14 @@ - netstandard2.0;net462;$(DefaultTargetFramework) + $(TargetFrameworks);netstandard2.0 true - true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) + + $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable + false @@ -22,6 +25,6 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs deleted file mode 100644 index a6ba654085e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs +++ /dev/null @@ -1,68 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection -{ - public static partial class ServiceDiscoveryHttpClientBuilderExtensions - { - public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { throw null; } - } - - public static partial class ServiceDiscoveryServiceCollectionExtensions - { - public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery -{ - public sealed partial class ConfigurationServiceEndpointProviderOptions - { - public string SectionName { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } - - public sealed partial class ServiceDiscoveryOptions - { - public bool AllowAllSchemes { get { throw null; } set { } } - - public System.Collections.Generic.IList AllowedSchemes { get { throw null; } set { } } - - public System.TimeSpan RefreshPeriod { get { throw null; } set { } } - } - - public sealed partial class ServiceEndpointResolver : System.IAsyncDisposable - { - internal ServiceEndpointResolver() { } - - public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } - - public System.Threading.Tasks.ValueTask GetEndpointsAsync(string serviceName, System.Threading.CancellationToken cancellationToken) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery.Http -{ - public partial interface IServiceDiscoveryHttpMessageHandlerFactory - { - System.Net.Http.HttpMessageHandler CreateHandler(System.Net.Http.HttpMessageHandler handler); - } -} \ No newline at end of file diff --git a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs deleted file mode 100644 index 6d82b4a25c9..00000000000 --- a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices; - -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute -{ - public string ParameterName => parameterName; -} diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets index ea9db526b69..cc03ee52970 100644 --- a/src/Shared/FxPolyfills/FxPolyfills.targets +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -1,6 +1,8 @@ $(MSBuildThisFileDirectory) + + $(NoWarn);CS8763;CS8777;CS8603;CA1031;IDE0058;S108;S2166;S2302;S2333;S2486;S3400;SA1402;SA1509;SA1515;SA1649;EA0014;LA0001;VSTHRD003 diff --git a/src/Shared/FxPolyfills/IsExternalInit.cs b/src/Shared/FxPolyfills/IsExternalInit.cs deleted file mode 100644 index f2bac777b13..00000000000 --- a/src/Shared/FxPolyfills/IsExternalInit.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices; - -internal static class IsExternalInit -{ -} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj index 6572c27a1fa..1e4a55d35bb 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -1,10 +1,11 @@  - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable Exe + $(NoWarn);IDE0040;IDE0061;IDE1006;S5034;SA1400;VSTHRD002 @@ -12,7 +13,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index f6202d17d37..45603e5fc5c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -1,15 +1,14 @@ - + - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);IDE0004;IDE0017;IDE0040;IDE0055;IDE1006;CA1012;CA1031;CA1063;CA1816;CA2000;S103;S107;S1067;S1121;S1128;S1135;S1144;S1186;S2148;S3442;S3459;S4136;SA1106;SA1127;SA1204;SA1208;SA1210;SA1128;SA1316;SA1400;SA1402;SA1407;SA1414;SA1500;SA1513;SA1515;VSTHRD003 - - @@ -17,9 +16,10 @@ - - - + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs index 8c646ac18ee..786882afc1d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs index f76621db93c..14abd659029 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -1,10 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; -using Microsoft.Extensions.Time.Testing; -using Microsoft.Extensions.Logging; +using System.Globalization; +using System.Text; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Dns.Tests; +using Microsoft.Extensions.Time.Testing; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs index b87e1362f3d..c2d033ecdae 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs @@ -1,9 +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 Xunit; using System.Net; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; @@ -304,4 +304,4 @@ public async Task Resolve_HeaderMismatch_Ignores() Assert.Equal(address, result.Address); } -} \ No newline at end of file +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs index e1cd1df2959..82ca3175789 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs index 800905d1ac5..49985846570 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Net.Sockets; -using Xunit; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs index 40841e3d11a..cbdb5e282e9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -1,9 +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 Xunit; using System.Net; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs new file mode 100644 index 00000000000..6667688f16e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs @@ -0,0 +1,145 @@ +// 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.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +internal static class XunitLoggerFactoryExtensions +{ + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output)); + return builder; + } + + public static IServiceCollection AddXunitLogging(this IServiceCollection services, ITestOutputHelper output) => + services.AddLogging(b => b.AddXunit(output)); +} + +internal class XunitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } +} + +internal class XunitLogger : ILogger +{ + private static readonly string[] s_newLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private readonly DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? + $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3", CultureInfo.InvariantCulture)}s" : + DateTimeOffset.UtcNow.ToString("s", CultureInfo.InvariantCulture); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(s_newLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(s_newLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) where TState : notnull + => new NullScope(); + + private sealed class NullScope : IDisposable + { + public void Dispose() + { + } + } +} + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 20147d5d465..1a9f8aaae8c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,13 +1,12 @@ - $(DefaultTargetFramework) - $(TargetFrameworks);net472 enable enable + $(NoWarn);IDE0004;IDE0040;IDE0055;IDE1006;CA2000;S1121;S1128;SA1316;SA1500;SA1513 - + @@ -15,12 +14,11 @@ - - - + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 296a3dcd861..c097e0f2503 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -1,23 +1,22 @@ - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);CA2000;S103;S1144;S3459;S4136;SA1208;SA1210;VSTHRD003 - - - - - - + + + + From 0034beec76acb6f99b36ce26f07cdaec38c33f77 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 14:49:59 -0500 Subject: [PATCH 329/472] Remove FxPolyfills from the Shared.csproj --- src/Shared/Shared.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index d25c011a05f..ecffd480a44 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -26,6 +26,10 @@ 85 + + + + From 675c83e1a7b47e19939d80e30ac5bb1478c1497a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 15:57:32 -0500 Subject: [PATCH 330/472] PR Feedback --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../ServiceEndpointQuery.cs | 2 +- .../DnsSrvServiceEndpointProviderFactory.cs | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 +- src/Shared/FxPolyfills/FxPolyfills.targets | 2 +- ...crosoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Tests.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj | 1 + 11 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 494e1cfbfbb..b3a9f892419 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -6,7 +6,7 @@ Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery - + $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 20a17d0878f..36fca0893cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -38,7 +38,7 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpoin ArgumentException.ThrowIfNullOrEmpty(input); bool hasScheme; - if (!input.Contains("://", StringComparison.InvariantCulture) + if (!input.Contains("://", StringComparison.Ordinal) && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) { hasScheme = false; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 085ee30123b..57820560a63 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -95,7 +95,7 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou var lines = File.ReadAllLines(s_resolveConfPath); foreach (var line in lines) { - if (!line.StartsWith("search ")) + if (!line.StartsWith("search ", StringComparison.Ordinal)) { continue; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index f1a59b08dac..890f8daab3e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -5,7 +5,7 @@ true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. $(DefaultDotnetIconFullPath) - + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1515;SA1600;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 16da6587759..e990866bd16 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true Provides extensions for service discovery for the YARP reverse proxy. $(DefaultDotnetIconFullPath) - + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S2692;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1414;SA1515;SA1600;SA1615;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index d501e3e22ad..8ff69baf576 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -5,7 +5,7 @@ true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) - + $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets index cc03ee52970..ca38f9ab986 100644 --- a/src/Shared/FxPolyfills/FxPolyfills.targets +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -1,7 +1,7 @@ $(MSBuildThisFileDirectory) - + $(NoWarn);CS8763;CS8777;CS8603;CA1031;IDE0058;S108;S2166;S2302;S2333;S2486;S3400;SA1402;SA1509;SA1515;SA1649;EA0014;LA0001;VSTHRD003 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj index 1e4a55d35bb..c291dff12c8 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -5,6 +5,7 @@ enable enable Exe + $(NoWarn);IDE0040;IDE0061;IDE1006;S5034;SA1400;VSTHRD002 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 45603e5fc5c..4911d854007 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);IDE0004;IDE0017;IDE0040;IDE0055;IDE1006;CA1012;CA1031;CA1063;CA1816;CA2000;S103;S107;S1067;S1121;S1128;S1135;S1144;S1186;S2148;S3442;S3459;S4136;SA1106;SA1127;SA1204;SA1208;SA1210;SA1128;SA1316;SA1400;SA1402;SA1407;SA1414;SA1500;SA1513;SA1515;VSTHRD003 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 1a9f8aaae8c..6a39ff1b9af 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -3,6 +3,7 @@ enable enable + $(NoWarn);IDE0004;IDE0040;IDE0055;IDE1006;CA2000;S1121;S1128;SA1316;SA1500;SA1513 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index c097e0f2503..00211519268 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);CA2000;S103;S1144;S3459;S4136;SA1208;SA1210;VSTHRD003 From 27af1fda75e782b9731cb00b65d768d2549b52ec Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 30 Sep 2025 16:58:01 -0400 Subject: [PATCH 331/472] Update `ModelContextProtocol` version (#6870) --- src/ProjectTemplates/GeneratedContent.targets | 2 +- .../mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj | 2 +- .../mcpserver.Basic.verified/mcpserver/mcpserver.csproj | 2 +- .../mcpserver/mcpserver.csproj | 2 +- .../mcpserver.net10.verified/mcpserver/mcpserver.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 31093493821..d6549ea0abf 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -44,7 +44,7 @@ 9.3.1 1.61.0 1.61.0-preview - 0.3.0-preview.2 + 0.4.0-preview.1 5.1.18 1.12.0 0.1.10 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj index 27a6ad45810..1b2dd939947 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj @@ -38,7 +38,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index a3199648740..f6da2d9485e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -34,7 +34,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj index cb3812885c6..a25caa73486 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj @@ -27,7 +27,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj index 27c45f47efb..393d0558d5e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj @@ -34,7 +34,7 @@ - + From 588babc7bbdd1ae1a072c6ad06d71e81336997fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as?= <50237907+ViveliDuCh@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:32:09 -0700 Subject: [PATCH 332/472] Scope Ollama resilience settings to Web/Program.cs and restore ServiceDefaults (#6850) Move Ollama-specific HTTP client resiliency configuration out of Extensions.cs in ServiceDefaults to an extension method called later in Web/Program.cs. --- .../.template.config/template.json | 6 ++++ .../Extensions.cs | 13 ------- .../OllamaResilienceHandlerExtensions.cs | 34 +++++++++++++++++++ .../Program.Aspire.cs | 7 ++++ .../aichatweb.ServiceDefaults/Extensions.cs | 10 +----- .../OllamaResilienceHandlerExtensions.cs | 34 +++++++++++++++++++ .../aichatweb/aichatweb.Web/Program.cs | 5 +++ 7 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/OllamaResilienceHandlerExtensions.cs create mode 100644 test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/OllamaResilienceHandlerExtensions.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index d95f7c41c13..3c5ca06f86d 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -64,6 +64,12 @@ "*.sln" ] }, + { + "condition": "(!IsAspire || !IsOllama)", + "exclude": [ + "ChatWithCustomData-CSharp.Web/OllamaResilienceHandlerExtensions.cs" + ] + }, { "condition": "(IsAspire)", "exclude": [ diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs index 108f1ed2a08..2fa2f11898e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs @@ -29,21 +29,8 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where http.RemoveAllResilienceHandlers(); #pragma warning restore EXTEXP0001 -#if (IsOllama) - // Turn on resilience by default - http.AddStandardResilienceHandler(config => - { - // Extend the HTTP Client timeout for Ollama - config.AttemptTimeout.Timeout = TimeSpan.FromMinutes(3); - - // Must be at least double the AttemptTimeout to pass options validation - config.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10); - config.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); - }); -#else // Turn on resilience by default http.AddStandardResilienceHandler(); -#endif // Turn on service discovery by default http.AddServiceDiscovery(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/OllamaResilienceHandlerExtensions.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/OllamaResilienceHandlerExtensions.cs new file mode 100644 index 00000000000..fed9c91ca93 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/OllamaResilienceHandlerExtensions.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace ChatWithCustomData_CSharp.Web.Services; + +public static class OllamaResilienceHandlerExtensions +{ + public static IServiceCollection AddOllamaResilienceHandler(this IServiceCollection services) + { + services.ConfigureHttpClientDefaults(http => + { +#pragma warning disable EXTEXP0001 // RemoveAllResilienceHandlers is experimental + http.RemoveAllResilienceHandlers(); +#pragma warning restore EXTEXP0001 + + // Turn on resilience by default + http.AddStandardResilienceHandler(config => + { + // Extend the HTTP Client timeout for Ollama + config.AttemptTimeout.Timeout = TimeSpan.FromMinutes(3); + + // Must be at least double the AttemptTimeout to pass options validation + config.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10); + config.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); + }); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return services; + } +} + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 84475f45a54..243aa46c8d7 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -50,6 +50,13 @@ #endif builder.Services.AddScoped(); builder.Services.AddSingleton(); +#if (IsOllama) +// Applies robust HTTP resilience settings for all HttpClients in the Web project, +// not across the entire solution. It's aimed at supporting Ollama scenarios due +// to its self-hosted nature and potentially slow responses. +// Remove this if you want to use the global or a different HTTP resilience policy instead. +builder.Services.AddOllamaResilienceHandler(); +#endif var app = builder.Build(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs index 81cc28b27d2..f56908872e0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -30,15 +30,7 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where #pragma warning restore EXTEXP0001 // Turn on resilience by default - http.AddStandardResilienceHandler(config => - { - // Extend the HTTP Client timeout for Ollama - config.AttemptTimeout.Timeout = TimeSpan.FromMinutes(3); - - // Must be at least double the AttemptTimeout to pass options validation - config.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10); - config.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); - }); + http.AddStandardResilienceHandler(); // Turn on service discovery by default http.AddServiceDiscovery(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/OllamaResilienceHandlerExtensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/OllamaResilienceHandlerExtensions.cs new file mode 100644 index 00000000000..ae82393302c --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/OllamaResilienceHandlerExtensions.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace aichatweb.Web.Services; + +public static class OllamaResilienceHandlerExtensions +{ + public static IServiceCollection AddOllamaResilienceHandler(this IServiceCollection services) + { + services.ConfigureHttpClientDefaults(http => + { +#pragma warning disable EXTEXP0001 // RemoveAllResilienceHandlers is experimental + http.RemoveAllResilienceHandlers(); +#pragma warning restore EXTEXP0001 + + // Turn on resilience by default + http.AddStandardResilienceHandler(config => + { + // Extend the HTTP Client timeout for Ollama + config.AttemptTimeout.Timeout = TimeSpan.FromMinutes(3); + + // Must be at least double the AttemptTimeout to pass options validation + config.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10); + config.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); + }); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return services; + } +} + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs index cdc88a082b7..c67c70db5d6 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Program.cs @@ -20,6 +20,11 @@ builder.Services.AddQdrantCollection("data-aichatweb-documents"); builder.Services.AddScoped(); builder.Services.AddSingleton(); +// Applies robust HTTP resilience settings for all HttpClients in the Web project, +// not across the entire solution. It's aimed at supporting Ollama scenarios due +// to its self-hosted nature and potentially slow responses. +// Remove this if you want to use the global or a different HTTP resilience policy instead. +builder.Services.AddOllamaResilienceHandler(); var app = builder.Build(); From af84086d5e188bc097de0d9985a11f541779f0b3 Mon Sep 17 00:00:00 2001 From: Varorbc Date: Wed, 1 Oct 2025 05:54:38 +0800 Subject: [PATCH 333/472] Update Aspire (#6858) * Update Aspire * Update Aspire template README --------- Co-authored-by: Mackinnon Buck --- src/ProjectTemplates/GeneratedContent.targets | 6 ++-- .../.template.config/template.json | 4 +-- .../{Program.cs => AppHost.cs} | 0 ...hatWithCustomData-CSharp.AppHost.csproj.in | 1 - .../Properties/launchSettings.json | 8 ++--- .../Extensions.cs | 16 ++++++--- .../Services/Ingestion/DataIngestor.cs | 6 ++-- .../src/ChatWithCustomData/README.Aspire.md | 36 ++----------------- .../aichatweb/README.md | 17 ++------- .../{Program.cs => AppHost.cs} | 0 .../Properties/launchSettings.json | 8 ++--- .../aichatweb.AppHost.csproj | 9 +++-- .../aichatweb.ServiceDefaults/Extensions.cs | 16 ++++++--- .../Services/Ingestion/DataIngestor.cs | 6 ++-- .../aichatweb.Web/aichatweb.Web.csproj | 6 ++-- .../Services/Ingestion/DataIngestor.cs | 6 ++-- .../aichatweb/README.md | 4 +-- .../{Program.cs => AppHost.cs} | 0 .../Properties/launchSettings.json | 8 ++--- .../aichatweb.AppHost.csproj | 5 ++- .../aichatweb.ServiceDefaults/Extensions.cs | 16 ++++++--- .../Services/Ingestion/DataIngestor.cs | 6 ++-- .../aichatweb.Web/aichatweb.Web.csproj | 4 +-- .../aichatweb/README.md | 4 +-- .../{Program.cs => AppHost.cs} | 0 .../Properties/launchSettings.json | 8 ++--- .../aichatweb.AppHost.csproj | 7 ++-- .../aichatweb.ServiceDefaults/Extensions.cs | 16 ++++++--- .../Services/Ingestion/DataIngestor.cs | 6 ++-- .../aichatweb.Web/aichatweb.Web.csproj | 2 +- .../Services/Ingestion/DataIngestor.cs | 6 ++-- 31 files changed, 112 insertions(+), 125 deletions(-) rename src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/{Program.cs => AppHost.cs} (100%) rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/{Program.cs => AppHost.cs} (100%) rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/{Program.cs => AppHost.cs} (100%) rename test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/{Program.cs => AppHost.cs} (100%) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index d6549ea0abf..2d30c4faf5f 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -33,9 +33,9 @@ Specifies external packages that get referenced in generated template content. --> - 9.4.0 - 9.4.0-preview.1.25378.8 - 2.3.0-beta.1 + 9.5.0 + 9.5.0-preview.1.25474.7 + 2.3.0-beta.2 1.0.0-beta.9 1.14.0 11.6.1 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index 3c5ca06f86d..cffbaa7598f 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/template", "author": "Microsoft", - "classifications": [ "Common", "AI", "Web", "Blazor", ".NET Aspire" ], + "classifications": [ "Common", "AI", "Web", "Blazor", "Aspire" ], "identity": "Microsoft.Extensions.AI.Templates.WebChat.CSharp", "name": "AI Chat Web App", "description": "A project template for creating an AI chat application, which uses retrieval-augmented generation (RAG) to chat with your own data.", @@ -183,7 +183,7 @@ "displayName": "Use Aspire orchestration", "datatype": "bool", "defaultValue": "false", - "description": "Create the project as a distributed application using .NET Aspire." + "description": "Create the project as a distributed application using Aspire." }, "IsAspire": { "type": "computed", diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs similarity index 100% rename from src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Program.cs rename to src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index 3bbd301af75..8f5b8baabc0 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -7,7 +7,6 @@ net9.0 enable enable - true b2f4f5e9-1083-472c-8c3b-f055ac67ba54 diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Properties/launchSettings.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Properties/launchSettings.json index cff9159f816..ff3cb400c10 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Properties/launchSettings.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/Properties/launchSettings.json @@ -9,8 +9,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000" } }, "http": { @@ -21,8 +21,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19000", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20000" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19000", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20000" } } } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs index 2fa2f11898e..204f7a64164 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.ServiceDefaults/Extensions.cs @@ -10,11 +10,14 @@ namespace Microsoft.Extensions.Hosting; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -64,7 +67,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation() @@ -111,10 +119,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs index 7eeb41c99fb..175cc505670 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs @@ -31,7 +31,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -39,7 +39,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -54,7 +54,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index 0b1934bfff7..14aa513acae 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -84,38 +84,8 @@ Note: Ollama and Docker are excellent open source products, but are not maintain #### ---#if (IsAzureOpenAI || UseAzureAISearch) ## Using Azure Provisioning -The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). +The project is set up to automatically provision Azure resources. When running the app for the first time, you will be prompted to provide Azure configuration values. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -#### ---#if (hostIdentifier == "vs") -Configure local provisioning for this project using .NET User Secrets: - -1. In Visual Studio, right-click on the ChatWithCustomData-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". -2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following configuration: - - ```json - { - "Azure": { - "SubscriptionId": "", - "AllowResourceGroupCreation": true, - "ResourceGroup": "", - "Location": "" - } - } - ``` - -#### ---#else -From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: - -```sh -cd ChatWithCustomData-CSharp.AppHost -dotnet user-secrets set Azure:SubscriptionId "" -dotnet user-secrets set Azure:AllowResourceGroupCreation "true" -dotnet user-secrets set Azure:ResourceGroup "" -dotnet user-secrets set Azure:Location "" -``` -#### ---#endif - -Make sure to replace placeholder values with real configuration values. #### ---#endif #### ---#if (UseQdrant) @@ -143,9 +113,9 @@ Note: Qdrant and Docker are excellent open source products, but are not maintain ## Trust the localhost certificate -Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. +Several Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. -See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. +See [Troubleshoot untrusted localhost certificate in Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. # Updating JavaScript dependencies diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md index d1459703de1..a2f61924c32 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md @@ -17,19 +17,8 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See ## Using Azure Provisioning -The project is set up to automatically provision Azure resources, but local configuration is configured. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). +The project is set up to automatically provision Azure resources. When running the app for the first time, you will be prompted to provide Azure configuration values. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). -From the command line, configure local provisioning for this project using .NET User Secrets by running the following commands: - -```sh -cd aichatweb.AppHost -dotnet user-secrets set Azure:SubscriptionId "" -dotnet user-secrets set Azure:AllowResourceGroupCreation "true" -dotnet user-secrets set Azure:ResourceGroup "" -dotnet user-secrets set Azure:Location "" -``` - -Make sure to replace placeholder values with real configuration values. # Running the application @@ -47,9 +36,9 @@ Make sure to replace placeholder values with real configuration values. ## Trust the localhost certificate -Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. +Several Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. -See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. +See [Troubleshoot untrusted localhost certificate in Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. # Updating JavaScript dependencies diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/AppHost.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Program.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/AppHost.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json index 4444e808585..681e3bf0d26 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json @@ -9,8 +9,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" } }, "http": { @@ -21,8 +21,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" } } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 54bcd4bc3a0..2ba82b12b27 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,20 +1,19 @@  - + Exe net9.0 enable enable - true secret - - - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs index f56908872e0..b44d60b604b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -10,11 +10,14 @@ namespace Microsoft.Extensions.Hosting; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -64,7 +67,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation() @@ -111,10 +119,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 59732141849..2fe43370071 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -26,7 +26,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -34,7 +34,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -49,7 +49,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index a4c6ec68b94..56e1e1ec23a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,14 +8,14 @@ - - + + - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs index 65b520980c1..89fe287ebed 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -26,7 +26,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -34,7 +34,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -49,7 +49,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md index c05c18281ef..94c542fda7f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/README.md @@ -43,9 +43,9 @@ Learn more about [prototyping with AI models using GitHub Models](https://docs.g ## Trust the localhost certificate -Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. +Several Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. -See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. +See [Troubleshoot untrusted localhost certificate in Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. # Updating JavaScript dependencies diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/AppHost.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Program.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/AppHost.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json index 4444e808585..681e3bf0d26 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json @@ -9,8 +9,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" } }, "http": { @@ -21,8 +21,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" } } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index ffef1abf363..a7c93eb2928 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,18 +1,17 @@  - + Exe net9.0 enable enable - true secret - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs index f56908872e0..b44d60b604b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -10,11 +10,14 @@ namespace Microsoft.Extensions.Hosting; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -64,7 +67,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation() @@ -111,10 +119,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index 59732141849..2fe43370071 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -26,7 +26,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -34,7 +34,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -49,7 +49,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 7fe3b1022ee..ed4f3c914c8 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md index 70e543ffeae..0ef2c04a907 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/README.md @@ -46,9 +46,9 @@ Note: Qdrant and Docker are excellent open source products, but are not maintain ## Trust the localhost certificate -Several .NET Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. +Several Aspire templates include ASP.NET Core projects that are configured to use HTTPS by default. If this is the first time you're running the project, an exception might occur when loading the Aspire dashboard. This error can be resolved by trusting the self-signed development certificate with the .NET CLI. -See [Troubleshoot untrusted localhost certificate in .NET Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. +See [Troubleshoot untrusted localhost certificate in Aspire](https://learn.microsoft.com/dotnet/aspire/troubleshooting/untrusted-localhost-certificate) for more information. # Updating JavaScript dependencies diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/AppHost.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Program.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/AppHost.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json index 4444e808585..681e3bf0d26 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json @@ -9,8 +9,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:9999" } }, "http": { @@ -21,8 +21,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:9999", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:9999" } } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 7637a36ca6f..c78ef8c5d91 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -1,19 +1,18 @@  - + Exe net9.0 enable enable - true secret - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs index f56908872e0..b44d60b604b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs @@ -10,11 +10,14 @@ namespace Microsoft.Extensions.Hosting; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -64,7 +67,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation() @@ -111,10 +119,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs index d0f7a6bc3a8..894b85c10de 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs @@ -26,7 +26,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -34,7 +34,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -49,7 +49,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 85dc735b4c4..03fba5231ac 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs index 65b520980c1..89fe287ebed 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -26,7 +26,7 @@ public async Task IngestDataAsync(IIngestionSource source) var deletedDocuments = await source.GetDeletedDocumentsAsync(documentsForSource); foreach (var deletedDocument in deletedDocuments) { - logger.LogInformation("Removing ingested data for {documentId}", deletedDocument.DocumentId); + logger.LogInformation("Removing ingested data for {DocumentId}", deletedDocument.DocumentId); await DeleteChunksForDocumentAsync(deletedDocument); await documentsCollection.DeleteAsync(deletedDocument.Key); } @@ -34,7 +34,7 @@ public async Task IngestDataAsync(IIngestionSource source) var modifiedDocuments = await source.GetNewOrModifiedDocumentsAsync(documentsForSource); foreach (var modifiedDocument in modifiedDocuments) { - logger.LogInformation("Processing {documentId}", modifiedDocument.DocumentId); + logger.LogInformation("Processing {DocumentId}", modifiedDocument.DocumentId); await DeleteChunksForDocumentAsync(modifiedDocument); await documentsCollection.UpsertAsync(modifiedDocument); @@ -49,7 +49,7 @@ async Task DeleteChunksForDocumentAsync(IngestedDocument document) { var documentId = document.DocumentId; var chunksToDelete = await chunksCollection.GetAsync(record => record.DocumentId == documentId, int.MaxValue).ToListAsync(); - if (chunksToDelete.Any()) + if (chunksToDelete.Count != 0) { await chunksCollection.DeleteAsync(chunksToDelete.Select(r => r.Key)); } From 75116baf6fde9fb90a5b38141717c1c674783aaf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:32:22 -0700 Subject: [PATCH 334/472] Update MCP template for new registry specification (#6796) * Initial plan * Update MCP template server.json for new registry specification Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> * Fix root property name from 'id' back to 'name' in MCP server.json template Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> * Update schema URI to use static.modelcontextprotocol.io and server.schema.json Co-authored-by: joelverhagen <94054+joelverhagen@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: timheuer <4821+timheuer@users.noreply.github.com> Co-authored-by: joelverhagen <94054+joelverhagen@users.noreply.github.com> --- .../src/McpServer/McpServer-CSharp/.mcp/server.json | 13 +++++++------ .../mcpserver/.mcp/server.json | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json index 34c19714f79..8a2ad2b07a5 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -1,12 +1,16 @@ { - "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", "description": "", "name": "io.github./", + "version": "0.1.0-beta", "packages": [ { - "registry_name": "nuget", - "name": "", + "registry_type": "nuget", + "identifier": "", "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, "package_arguments": [], "environment_variables": [] } @@ -14,8 +18,5 @@ "repository": { "url": "https://github.com//", "source": "github" - }, - "version_detail": { - "version": "0.1.0-beta" } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json index 02908c09afb..7c7602bea75 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -1,12 +1,16 @@ { - "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", "description": "", "name": "io.github./", + "version": "0.1.0-beta", "packages": [ { - "registry_name": "nuget", - "name": "", + "registry_type": "nuget", + "identifier": "", "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, "package_arguments": [], "environment_variables": [] } @@ -14,8 +18,5 @@ "repository": { "url": "https://github.com//", "source": "github" - }, - "version_detail": { - "version": "0.1.0-beta" } } From e8657b78e7916e4cf189440a5edeaa730ca7d6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Wed, 1 Oct 2025 12:26:03 +0200 Subject: [PATCH 335/472] Add support for `HostApplicationBuilder` in AmbientMetadata extension (#6867) --- ...pplicationMetadataHostBuilderExtensions.cs | 21 +++++++++ ...xtensions.AmbientMetadata.Application.json | 6 ++- .../README.md | 1 + .../AcceptanceTests.cs | 45 ++++++++++++++++++- .../ApplicationMetadataExtensionsTests.cs | 21 +++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataHostBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataHostBuilderExtensions.cs index e2635bbcc4d..2c7054c4b11 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataHostBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataHostBuilderExtensions.cs @@ -32,4 +32,25 @@ public static IHostBuilder UseApplicationMetadata(this IHostBuilder builder, str .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder) => configurationBuilder.AddApplicationMetadata(hostBuilderContext.HostingEnvironment, sectionName)) .ConfigureServices((hostBuilderContext, serviceCollection) => serviceCollection.AddApplicationMetadata(hostBuilderContext.Configuration.GetSection(sectionName))); } + + /// + /// Registers a configuration provider for application metadata and binds a model object onto the configuration. + /// + /// . + /// The host builder. + /// Section name to bind configuration from. Default set to "ambientmetadata:application". + /// The value of . + /// is . + /// is either , empty, or whitespace. + public static TBuilder UseApplicationMetadata(this TBuilder builder, string sectionName = DefaultSectionName) + where TBuilder : IHostApplicationBuilder + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrWhitespace(sectionName); + + _ = builder.Configuration.AddApplicationMetadata(builder.Environment, sectionName); + _ = builder.Services.AddApplicationMetadata(builder.Configuration.GetSection(sectionName)); + + return builder; + } } diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.json b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.json index 35db568194a..ecdcf8916fa 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.json +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.json @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.AmbientMetadata.Application, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.AmbientMetadata.Application, Version=9.10.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "class Microsoft.Extensions.AmbientMetadata.ApplicationMetadata", @@ -46,6 +46,10 @@ { "Member": "static Microsoft.Extensions.Hosting.IHostBuilder Microsoft.Extensions.Hosting.ApplicationMetadataHostBuilderExtensions.UseApplicationMetadata(this Microsoft.Extensions.Hosting.IHostBuilder builder, string sectionName = \"ambientmetadata:application\");", "Stage": "Stable" + }, + { + "Member": "static TBuilder Microsoft.Extensions.Hosting.ApplicationMetadataHostBuilderExtensions.UseApplicationMetadata(this TBuilder builder, string sectionName = \"ambientmetadata:application\");", + "Stage": "Stable" } ] }, diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/README.md b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/README.md index 45cba75f480..5ef2e71c271 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/README.md +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/README.md @@ -26,6 +26,7 @@ The services can be registered using any of the following methods: ```csharp public static IHostBuilder UseApplicationMetadata(this IHostBuilder builder, string sectionName = DefaultSectionName) +public static TBuilder UseApplicationMetadata(this TBuilder builder, string sectionName = DefaultSectionName) where TBuilder : IHostApplicationBuilder public static IServiceCollection AddApplicationMetadata(this IServiceCollection services, Action configure) ``` diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs index d66842b018b..ace2a6148e6 100644 --- a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs @@ -39,12 +39,28 @@ await RunAsync( }, sectionName); + [Theory] + [InlineData("ambientmetadata:application")] + [InlineData(null)] + public async Task UseApplicationMetadata_HostApplicationBuilder_CreatesPopulatesAndRegistersOptions(string? sectionName) => + await RunAsync_HostBuilder( + (options, hostEnvironment) => + { + options.BuildVersion.Should().Be(_metadata.BuildVersion); + options.DeploymentRing.Should().Be(_metadata.DeploymentRing); + options.ApplicationName.Should().Be(_metadata.ApplicationName); + options.EnvironmentName.Should().Be(hostEnvironment.EnvironmentName); + + return Task.CompletedTask; + }, + sectionName); + private static async Task RunAsync(Func func, string? sectionName) { using var host = await FakeHost.CreateBuilder() // need to set applicationName manually, because - // netfx console test runner cannot get assebly name + // netfx console test runner cannot get assembly name // to be able to set it automatically // see https://source.dot.net/#Microsoft.Extensions.Hosting/HostBuilder.cs,240 .ConfigureHostConfiguration("applicationname", _metadata.ApplicationName) @@ -60,4 +76,31 @@ await func(host.Services.GetRequiredService>().Val host.Services.GetRequiredService()); await host.StopAsync(); } + + private static async Task RunAsync_HostBuilder(Func func, string? sectionName) + { + var builder = Host.CreateEmptyApplicationBuilder(new() + { + ApplicationName = _metadata.ApplicationName + }); + + // need to set applicationName manually, because + // netfx console test runner cannot get assembly name + // to be able to set it automatically + // see https://source.dot.net/#Microsoft.Extensions.Hosting/HostBuilder.cs,240 + builder + .UseApplicationMetadata(sectionName ?? "ambientmetadata:application") + .Services.AddApplicationMetadata(metadata => + { + metadata.BuildVersion = _metadata.BuildVersion; + metadata.DeploymentRing = _metadata.DeploymentRing; + }); + + using var host = builder.Build(); + await host.StartAsync(); + + await func(host.Services.GetRequiredService>().Value, + host.Services.GetRequiredService()); + await host.StopAsync(); + } } diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs index 5091bd87e21..7fbc66a1e73 100644 --- a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs @@ -40,10 +40,20 @@ public void ApplicationMetadataExtensions_GivenAnyNullArgument_Throws() Assert.Throws(() => serviceCollection.AddApplicationMetadata((Action)null!)); Assert.Throws(() => serviceCollection.AddApplicationMetadata((IConfigurationSection)null!)); Assert.Throws(() => ((IHostBuilder)null!).UseApplicationMetadata(_fixture.Create())); + Assert.Throws(() => ((IHostApplicationBuilder)null!).UseApplicationMetadata(_fixture.Create())); Assert.Throws(() => new ConfigurationBuilder().AddApplicationMetadata(null!)); Assert.Throws(() => ((IConfigurationBuilder)null!).AddApplicationMetadata(null!)); } + [Fact] + public void ApplicationMetadataExtensions_GivenEmptyAction_DoesNotThrow() + { + var serviceCollection = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + + Assert.Null(Record.Exception(() => serviceCollection.AddApplicationMetadata(_ => { }))); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -66,6 +76,17 @@ public void UseApplicationMetadata_InvalidSectionName_Throws(string? sectionName act.Should().Throw(); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void UseApplicationMetadata_HostApplicationBuilder_InvalidSectionName_Throws(string? sectionName) + { + var act = () => Host.CreateEmptyApplicationBuilder(new()).UseApplicationMetadata(sectionName!); + act.Should().Throw(); + } + [Fact] public void AddApplicationMetadata_BuildsConfig() { From d474a89b0d4e972135212432acb927342801e784 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Wed, 1 Oct 2025 08:41:22 -0700 Subject: [PATCH 336/472] Fix mcpserver test baselines (#6874) --- .../mcpserver/.mcp/server.json | 13 +++++++------ .../mcpserver/.mcp/server.json | 13 +++++++------ .../mcpserver/.mcp/server.json | 13 +++++++------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json index 02908c09afb..7c7602bea75 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json @@ -1,12 +1,16 @@ { - "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", "description": "", "name": "io.github./", + "version": "0.1.0-beta", "packages": [ { - "registry_name": "nuget", - "name": "", + "registry_type": "nuget", + "identifier": "", "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, "package_arguments": [], "environment_variables": [] } @@ -14,8 +18,5 @@ "repository": { "url": "https://github.com//", "source": "github" - }, - "version_detail": { - "version": "0.1.0-beta" } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json index 02908c09afb..7c7602bea75 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json @@ -1,12 +1,16 @@ { - "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", "description": "", "name": "io.github./", + "version": "0.1.0-beta", "packages": [ { - "registry_name": "nuget", - "name": "", + "registry_type": "nuget", + "identifier": "", "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, "package_arguments": [], "environment_variables": [] } @@ -14,8 +18,5 @@ "repository": { "url": "https://github.com//", "source": "github" - }, - "version_detail": { - "version": "0.1.0-beta" } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json index 02908c09afb..7c7602bea75 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json @@ -1,12 +1,16 @@ { - "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", "description": "", "name": "io.github./", + "version": "0.1.0-beta", "packages": [ { - "registry_name": "nuget", - "name": "", + "registry_type": "nuget", + "identifier": "", "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, "package_arguments": [], "environment_variables": [] } @@ -14,8 +18,5 @@ "repository": { "url": "https://github.com//", "source": "github" - }, - "version_detail": { - "version": "0.1.0-beta" } } From 839ef9e8d48ea1b807cd2030fe9c5c54c5f3cc30 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 2 Oct 2025 21:15:57 -0400 Subject: [PATCH 337/472] Add copy constructors to option types (ChatOptions, etc.) (#6882) * Add copy constructors to option types (ChatOptions, etc.) To enable the Clone methods to support derivation and to enable decorators to pass down cloneable state. This also fixes a bug that EmbeddingGenerationOptions.Clone and SpeechToTextOptions.Clone were not cloning raw representation. --- .../CHANGELOG.md | 4 + .../ChatCompletion/ChatOptions.cs | 79 +++++++++++-------- .../Embeddings/EmbeddingGenerationOptions.cs | 27 +++++-- .../Image/ImageGenerationOptions.cs | 38 +++++---- .../Microsoft.Extensions.AI.Abstractions.json | 8 ++ .../SpeechToText/SpeechToTextOptions.cs | 35 +++++--- .../ContentSafetyChatOptions.cs | 23 +++++- .../ChatCompletion/ChatOptionsTests.cs | 66 ++++++++++++++++ .../EmbeddingGenerationOptionsTests.cs | 71 +++++++++++++++++ .../Image/ImageGenerationOptionsTests.cs | 66 ++++++++++++++++ .../SpeechToText/SpeechToTextOptionsTests.cs | 72 +++++++++++++++++ 11 files changed, 418 insertions(+), 71 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 3ba9420a870..af9fa5a328d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## NOT YET RELEASED + +- Added protected copy constructors to options types. + ## 9.9.1 - Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 0be912430fa..4447bace386 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -11,6 +11,46 @@ namespace Microsoft.Extensions.AI; /// Provide options. public class ChatOptions { + /// Initializes a new instance of the class. + public ChatOptions() + { + } + + /// Initializes a new instance of the class, performing a shallow copy of all properties from . + protected ChatOptions(ChatOptions? other) + { + if (other is null) + { + return; + } + + AdditionalProperties = other.AdditionalProperties?.Clone(); + AllowMultipleToolCalls = other.AllowMultipleToolCalls; + ConversationId = other.ConversationId; + FrequencyPenalty = other.FrequencyPenalty; + Instructions = other.Instructions; + MaxOutputTokens = other.MaxOutputTokens; + ModelId = other.ModelId; + PresencePenalty = other.PresencePenalty; + RawRepresentationFactory = other.RawRepresentationFactory; + ResponseFormat = other.ResponseFormat; + Seed = other.Seed; + Temperature = other.Temperature; + ToolMode = other.ToolMode; + TopK = other.TopK; + TopP = other.TopP; + + if (other.StopSequences is not null) + { + StopSequences = [.. other.StopSequences]; + } + + if (other.Tools is not null) + { + Tools = [.. other.Tools]; + } + } + /// Gets or sets an optional identifier used to associate a request with an existing conversation. /// Stateless vs. stateful clients. public string? ConversationId { get; set; } @@ -141,41 +181,14 @@ public class ChatOptions /// Produces a clone of the current instance. /// A clone of the current instance. /// + /// /// The clone will have the same values for all properties as the original instance. Any collections, like , /// , and , are shallow-cloned, meaning a new collection instance is created, /// but any references contained by the collections are shared with the original. + /// + /// + /// Derived types should override to return an instance of the derived type. + /// /// - public virtual ChatOptions Clone() - { - ChatOptions options = new() - { - AdditionalProperties = AdditionalProperties?.Clone(), - AllowMultipleToolCalls = AllowMultipleToolCalls, - ConversationId = ConversationId, - FrequencyPenalty = FrequencyPenalty, - Instructions = Instructions, - MaxOutputTokens = MaxOutputTokens, - ModelId = ModelId, - PresencePenalty = PresencePenalty, - RawRepresentationFactory = RawRepresentationFactory, - ResponseFormat = ResponseFormat, - Seed = Seed, - Temperature = Temperature, - ToolMode = ToolMode, - TopK = TopK, - TopP = TopP, - }; - - if (StopSequences is not null) - { - options.StopSequences = new List(StopSequences); - } - - if (Tools is not null) - { - options.Tools = new List(Tools); - } - - return options; - } + public virtual ChatOptions Clone() => new(this); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs index 4d88c85b760..db547a7b8fa 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs @@ -10,6 +10,25 @@ namespace Microsoft.Extensions.AI; /// Represents the options for an embedding generation request. public class EmbeddingGenerationOptions { + /// Initializes a new instance of the class. + public EmbeddingGenerationOptions() + { + } + + /// Initializes a new instance of the class, performing a shallow copy of all properties from . + protected EmbeddingGenerationOptions(EmbeddingGenerationOptions? other) + { + if (other is null) + { + return; + } + + AdditionalProperties = other.AdditionalProperties?.Clone(); + Dimensions = other.Dimensions; + ModelId = other.ModelId; + RawRepresentationFactory = other.RawRepresentationFactory; + } + /// Gets or sets the number of dimensions requested in the embedding. public int? Dimensions { @@ -56,11 +75,5 @@ public int? Dimensions /// The clone will have the same values for all properties as the original instance. Any collections, like /// are shallow-cloned, meaning a new collection instance is created, but any references contained by the collections are shared with the original. /// - public virtual EmbeddingGenerationOptions Clone() => - new() - { - ModelId = ModelId, - Dimensions = Dimensions, - AdditionalProperties = AdditionalProperties?.Clone(), - }; + public virtual EmbeddingGenerationOptions Clone() => new(this); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs index bf95b5d5aed..fce02e9e88e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGenerationOptions.cs @@ -12,6 +12,28 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class ImageGenerationOptions { + /// Initializes a new instance of the class. + public ImageGenerationOptions() + { + } + + /// Initializes a new instance of the class, performing a shallow copy of all properties from . + protected ImageGenerationOptions(ImageGenerationOptions? other) + { + if (other is null) + { + return; + } + + AdditionalProperties = other.AdditionalProperties?.Clone(); + Count = other.Count; + ImageSize = other.ImageSize; + MediaType = other.MediaType; + ModelId = other.ModelId; + RawRepresentationFactory = other.RawRepresentationFactory; + ResponseFormat = other.ResponseFormat; + } + /// /// Gets or sets the number of images to generate. /// @@ -64,21 +86,7 @@ public class ImageGenerationOptions /// Produces a clone of the current instance. /// A clone of the current instance. - public virtual ImageGenerationOptions Clone() - { - ImageGenerationOptions options = new() - { - AdditionalProperties = AdditionalProperties?.Clone(), - Count = Count, - MediaType = MediaType, - ImageSize = ImageSize, - ModelId = ModelId, - RawRepresentationFactory = RawRepresentationFactory, - ResponseFormat = ResponseFormat - }; - - return options; - } + public virtual ImageGenerationOptions Clone() => new(this); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 73799733746..daa5063cf51 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -983,6 +983,10 @@ "Member": "Microsoft.Extensions.AI.ChatOptions.ChatOptions();", "Stage": "Stable" }, + { + "Member": "Microsoft.Extensions.AI.ChatOptions.ChatOptions(Microsoft.Extensions.AI.ChatOptions? other);", + "Stage": "Stable" + }, { "Member": "virtual Microsoft.Extensions.AI.ChatOptions Microsoft.Extensions.AI.ChatOptions.Clone();", "Stage": "Stable" @@ -1693,6 +1697,10 @@ "Member": "Microsoft.Extensions.AI.EmbeddingGenerationOptions.EmbeddingGenerationOptions();", "Stage": "Stable" }, + { + "Member": "Microsoft.Extensions.AI.EmbeddingGenerationOptions.EmbeddingGenerationOptions(Microsoft.Extensions.AI.EmbeddingGenerationOptions? other);", + "Stage": "Stable" + }, { "Member": "virtual Microsoft.Extensions.AI.EmbeddingGenerationOptions Microsoft.Extensions.AI.EmbeddingGenerationOptions.Clone();", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 8efbf510164..f57dbe2dd3f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -11,6 +11,27 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class SpeechToTextOptions { + /// Initializes a new instance of the class. + public SpeechToTextOptions() + { + } + + /// Initializes a new instance of the class, performing a shallow copy of all properties from . + protected SpeechToTextOptions(SpeechToTextOptions? other) + { + if (other is null) + { + return; + } + + AdditionalProperties = other.AdditionalProperties?.Clone(); + ModelId = other.ModelId; + RawRepresentationFactory = other.RawRepresentationFactory; + SpeechLanguage = other.SpeechLanguage; + SpeechSampleRate = other.SpeechSampleRate; + TextLanguage = other.TextLanguage; + } + /// Gets or sets any additional properties associated with the options. public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } @@ -47,17 +68,5 @@ public class SpeechToTextOptions /// Produces a clone of the current instance. /// A clone of the current instance. - public virtual SpeechToTextOptions Clone() - { - SpeechToTextOptions options = new() - { - AdditionalProperties = AdditionalProperties?.Clone(), - ModelId = ModelId, - SpeechLanguage = SpeechLanguage, - SpeechSampleRate = SpeechSampleRate, - TextLanguage = TextLanguage, - }; - - return options; - } + public virtual SpeechToTextOptions Clone() => new(this); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs index 2b95d3b5f39..c59f585b4ad 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Safety/ContentSafetyChatOptions.cs @@ -1,10 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Shared.Diagnostics; + namespace Microsoft.Extensions.AI.Evaluation.Safety; -internal sealed class ContentSafetyChatOptions(string annotationTask, string evaluatorName) : ChatOptions +internal sealed class ContentSafetyChatOptions : ChatOptions { - internal string AnnotationTask { get; } = annotationTask; - internal string EvaluatorName { get; } = evaluatorName; + public ContentSafetyChatOptions(string annotationTask, string evaluatorName) + { + AnnotationTask = annotationTask; + EvaluatorName = evaluatorName; + } + + private ContentSafetyChatOptions(ContentSafetyChatOptions other) + : base(Throw.IfNull(other)) + { + AnnotationTask = other.AnnotationTask; + EvaluatorName = other.EvaluatorName; + } + + public string AnnotationTask { get; } + public string EvaluatorName { get; } + + public override ChatOptions Clone() => new ContentSafetyChatOptions(this); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs index b7645c26245..5c9fec9111d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs @@ -198,4 +198,70 @@ public void JsonSerialization_Roundtrips() Assert.IsType(value); Assert.Equal("value", ((JsonElement)value!).GetString()); } + + [Fact] + public void CopyConstructors_EnableHierarchyCloning() + { + OptionsB b = new() + { + ModelId = "test", + A = 42, + B = 84, + }; + + ChatOptions clone = b.Clone(); + + Assert.Equal("test", clone.ModelId); + Assert.Equal(42, Assert.IsType(clone, exactMatch: false).A); + Assert.Equal(84, Assert.IsType(clone, exactMatch: true).B); + } + + private class OptionsA : ChatOptions + { + public OptionsA() + { + } + + protected OptionsA(OptionsA other) + : base(other) + { + A = other.A; + } + + public int A { get; set; } + + public override ChatOptions Clone() => new OptionsA(this); + } + + private class OptionsB : OptionsA + { + public OptionsB() + { + } + + protected OptionsB(OptionsB other) + : base(other) + { + B = other.B; + } + + public int B { get; set; } + + public override ChatOptions Clone() => new OptionsB(this); + } + + [Fact] + public void CopyConstructor_Null_Valid() + { + PassedNullToBaseOptions options = new(); + Assert.NotNull(options); + } + + private class PassedNullToBaseOptions : ChatOptions + { + public PassedNullToBaseOptions() + : base(null) + { + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs index 97ffecfc1f6..34cbcd63e1b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs @@ -41,18 +41,23 @@ public void Properties_Roundtrip() ["key"] = "value", }; + Func rawRepresentationFactory = (c) => null; + options.ModelId = "modelId"; options.Dimensions = 1536; options.AdditionalProperties = additionalProps; + options.RawRepresentationFactory = rawRepresentationFactory; Assert.Equal("modelId", options.ModelId); Assert.Equal(1536, options.Dimensions); Assert.Same(additionalProps, options.AdditionalProperties); + Assert.Same(rawRepresentationFactory, options.RawRepresentationFactory); EmbeddingGenerationOptions clone = options.Clone(); Assert.Equal("modelId", clone.ModelId); Assert.Equal(1536, clone.Dimensions); Assert.Equal(additionalProps, clone.AdditionalProperties); + Assert.Same(rawRepresentationFactory, clone.RawRepresentationFactory); } [Fact] @@ -83,4 +88,70 @@ public void JsonSerialization_Roundtrips() Assert.IsType(value); Assert.Equal("value", ((JsonElement)value!).GetString()); } + + [Fact] + public void CopyConstructors_EnableHierarchyCloning() + { + OptionsB b = new() + { + ModelId = "test", + A = 42, + B = 84, + }; + + EmbeddingGenerationOptions clone = b.Clone(); + + Assert.Equal("test", clone.ModelId); + Assert.Equal(42, Assert.IsType(clone, exactMatch: false).A); + Assert.Equal(84, Assert.IsType(clone, exactMatch: true).B); + } + + private class OptionsA : EmbeddingGenerationOptions + { + public OptionsA() + { + } + + protected OptionsA(OptionsA other) + : base(other) + { + A = other.A; + } + + public int A { get; set; } + + public override EmbeddingGenerationOptions Clone() => new OptionsA(this); + } + + private class OptionsB : OptionsA + { + public OptionsB() + { + } + + protected OptionsB(OptionsB other) + : base(other) + { + B = other.B; + } + + public int B { get; set; } + + public override EmbeddingGenerationOptions Clone() => new OptionsB(this); + } + + [Fact] + public void CopyConstructor_Null_Valid() + { + PassedNullToBaseOptions options = new(); + Assert.NotNull(options); + } + + private class PassedNullToBaseOptions : EmbeddingGenerationOptions + { + public PassedNullToBaseOptions() + : base(null) + { + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs index f6cd167e82a..68040e9c29c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Image/ImageGenerationOptionsTests.cs @@ -132,4 +132,70 @@ public void ImageGenerationResponseFormat_JsonSerialization_Roundtrips() Assert.Equal(responseFormat, deserialized); } } + + [Fact] + public void CopyConstructors_EnableHierarchyCloning() + { + OptionsB b = new() + { + ModelId = "test", + A = 42, + B = 84, + }; + + ImageGenerationOptions clone = b.Clone(); + + Assert.Equal("test", clone.ModelId); + Assert.Equal(42, Assert.IsType(clone, exactMatch: false).A); + Assert.Equal(84, Assert.IsType(clone, exactMatch: true).B); + } + + private class OptionsA : ImageGenerationOptions + { + public OptionsA() + { + } + + protected OptionsA(OptionsA other) + : base(other) + { + A = other.A; + } + + public int A { get; set; } + + public override ImageGenerationOptions Clone() => new OptionsA(this); + } + + private class OptionsB : OptionsA + { + public OptionsB() + { + } + + protected OptionsB(OptionsB other) + : base(other) + { + B = other.B; + } + + public int B { get; set; } + + public override ImageGenerationOptions Clone() => new OptionsB(this); + } + + [Fact] + public void CopyConstructor_Null_Valid() + { + PassedNullToBaseOptions options = new(); + Assert.NotNull(options); + } + + private class PassedNullToBaseOptions : ImageGenerationOptions + { + public PassedNullToBaseOptions() + : base(null) + { + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs index 20936fd4517..4cf0f6461ee 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -1,6 +1,7 @@ // 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 Xunit; @@ -34,21 +35,26 @@ public void Properties_Roundtrip() ["key"] = "value", }; + Func rawRepresentationFactory = (c) => null; + options.ModelId = "modelId"; options.SpeechLanguage = "en-US"; options.SpeechSampleRate = 44100; options.AdditionalProperties = additionalProps; + options.RawRepresentationFactory = rawRepresentationFactory; Assert.Equal("modelId", options.ModelId); Assert.Equal("en-US", options.SpeechLanguage); Assert.Equal(44100, options.SpeechSampleRate); Assert.Same(additionalProps, options.AdditionalProperties); + Assert.Same(rawRepresentationFactory, options.RawRepresentationFactory); SpeechToTextOptions clone = options.Clone(); Assert.Equal("modelId", clone.ModelId); Assert.Equal("en-US", clone.SpeechLanguage); Assert.Equal(44100, clone.SpeechSampleRate); Assert.Equal(additionalProps, clone.AdditionalProperties); + Assert.Same(rawRepresentationFactory, clone.RawRepresentationFactory); } [Fact] @@ -81,4 +87,70 @@ public void JsonSerialization_Roundtrips() Assert.IsType(value); Assert.Equal("value", ((JsonElement)value!).GetString()); } + + [Fact] + public void CopyConstructors_EnableHierarchyCloning() + { + OptionsB b = new() + { + ModelId = "test", + A = 42, + B = 84, + }; + + SpeechToTextOptions clone = b.Clone(); + + Assert.Equal("test", clone.ModelId); + Assert.Equal(42, Assert.IsType(clone, exactMatch: false).A); + Assert.Equal(84, Assert.IsType(clone, exactMatch: true).B); + } + + private class OptionsA : SpeechToTextOptions + { + public OptionsA() + { + } + + protected OptionsA(OptionsA other) + : base(other) + { + A = other.A; + } + + public int A { get; set; } + + public override SpeechToTextOptions Clone() => new OptionsA(this); + } + + private class OptionsB : OptionsA + { + public OptionsB() + { + } + + protected OptionsB(OptionsB other) + : base(other) + { + B = other.B; + } + + public int B { get; set; } + + public override SpeechToTextOptions Clone() => new OptionsB(this); + } + + [Fact] + public void CopyConstructor_Null_Valid() + { + PassedNullToBaseOptions options = new(); + Assert.NotNull(options); + } + + private class PassedNullToBaseOptions : SpeechToTextOptions + { + public PassedNullToBaseOptions() + : base(null) + { + } + } } From 254951e6a4f56e3d64f4f23e39cac08ee9cf07be Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:33:12 +0000 Subject: [PATCH 338/472] Fix ChatMessage.CreatedAt being always overwritten by the latest timestamp. (#6885) * Initial plan * Add failing test for Unix epoch timestamp issue Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix CreatedAt timestamp overwrite by only updating if later Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Enhance timestamp test with comprehensive scenarios Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Replace DateTimeOffset.UnixEpoch with manual construction for compatibility Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Apply suggestions from code review * Update test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs * Add theory test for timestamp folding with pairs of timestamps Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Refactor ToChatResponse_TimestampFolding to use MemberData instead of duplicated InlineData Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub --- .../ChatCompletion/ChatResponseExtensions.cs | 4 +- .../ChatResponseUpdateExtensionsTests.cs | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 81ac83cd59c..87a957130f5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -346,7 +346,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon message.AuthorName = update.AuthorName; } - if (update.CreatedAt is not null) + if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt)) { message.CreatedAt = update.CreatedAt; } @@ -391,7 +391,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon response.ConversationId = update.ConversationId; } - if (update.CreatedAt is not null) + if (response.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > response.CreatedAt)) { response.CreatedAt = update.CreatedAt; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 50bd9c86293..3328d0be083 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -359,6 +359,98 @@ public async Task ToChatResponse_UsageContentExtractedFromContents() Assert.Equal("Hello, world!", Assert.IsType(Assert.Single(Assert.Single(response.Messages).Contents)).Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) + { + DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); + DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero); + DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero); + DateTimeOffset unixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + ChatResponseUpdate[] updates = + [ + + // Start with an early timestamp + new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, + + // Unix epoch (as "null") should not overwrite + new(null, "b") { CreatedAt = unixEpoch }, + + // Newer timestamp should overwrite + new(null, "c") { CreatedAt = middle }, + + // Older timestamp should not overwrite + new(null, "d") { CreatedAt = early }, + + // Even newer timestamp should overwrite + new(null, "e") { CreatedAt = late }, + + // Unix epoch should not overwrite again + new(null, "f") { CreatedAt = unixEpoch }, + + // null should not overwrite + new(null, "g") { CreatedAt = null }, + ]; + + ChatResponse response = useAsync ? + updates.ToChatResponse() : + await YieldAsync(updates).ToChatResponseAsync(); + Assert.Single(response.Messages); + + Assert.Equal("abcdefg", response.Messages[0].Text); + Assert.Equal(ChatRole.Tool, response.Messages[0].Role); + Assert.Equal(late, response.Messages[0].CreatedAt); + Assert.Equal(late, response.CreatedAt); + } + + public static IEnumerable ToChatResponse_TimestampFolding_MemberData() + { + // Base test cases + var testCases = new (string? timestamp1, string? timestamp2, string? expectedTimestamp)[] + { + (null, null, null), + ("2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z"), + (null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + ("2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z"), + ("1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + }; + + // Yield each test case twice, once for useAsync = false and once for useAsync = true + foreach (var (timestamp1, timestamp2, expectedTimestamp) in testCases) + { + yield return new object?[] { false, timestamp1, timestamp2, expectedTimestamp }; + yield return new object?[] { true, timestamp1, timestamp2, expectedTimestamp }; + } + } + + [Theory] + [MemberData(nameof(ToChatResponse_TimestampFolding_MemberData))] + public async Task ToChatResponse_TimestampFolding(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp) + { + DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null; + DateTimeOffset? second = timestamp2 is not null ? DateTimeOffset.Parse(timestamp2) : null; + DateTimeOffset? expected = expectedTimestamp is not null ? DateTimeOffset.Parse(expectedTimestamp) : null; + + ChatResponseUpdate[] updates = + [ + new(ChatRole.Assistant, "a") { CreatedAt = first }, + new(null, "b") { CreatedAt = second }, + ]; + + ChatResponse response = useAsync ? + updates.ToChatResponse() : + await YieldAsync(updates).ToChatResponseAsync(); + + Assert.Single(response.Messages); + Assert.Equal("ab", response.Messages[0].Text); + Assert.Equal(expected, response.Messages[0].CreatedAt); + Assert.Equal(expected, response.CreatedAt); + } + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) { foreach (ChatResponseUpdate update in updates) From 7fef00a7a1de1418718ac805594cf69734717ea2 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:04:03 -0700 Subject: [PATCH 339/472] small doc fixes (#6887) --- .../Contents/McpServerToolCallContent.cs | 2 +- .../Tools/HostedMcpServerTool.cs | 4 ++-- .../CSharp/IEvaluationReportWriter.cs | 2 +- .../ApplicationMetadataConfigurationBuilderExtensions.cs | 4 ++-- .../ApplicationMetadataServiceCollectionExtensions.cs | 2 +- .../Microsoft.Extensions.AsyncState/IAsyncContext.cs | 2 +- .../Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs | 2 +- .../Microsoft.Extensions.AsyncState/IAsyncState.cs | 2 +- .../FakeRedactionServiceCollectionExtensions.cs | 6 +++--- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 5ed6385789c..ec7f993a25f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -24,7 +24,7 @@ public sealed class McpServerToolCallContent : AIContent /// The tool call ID. /// The tool name. /// The MCP server name. - /// , , or are . + /// , , or is . /// , , or are empty or composed entirely of whitespace. public McpServerToolCallContent(string callId, string toolName, string serverName) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index b5ed4938a45..26fc3cd6434 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -19,7 +19,7 @@ public class HostedMcpServerTool : AITool /// /// The name of the remote MCP server. /// The URL of the remote MCP server. - /// or are . + /// or is . /// is empty or composed entirely of whitespace. public HostedMcpServerTool(string serverName, [StringSyntax(StringSyntaxAttribute.Uri)] string url) : this(serverName, new Uri(Throw.IfNull(url))) @@ -31,7 +31,7 @@ public HostedMcpServerTool(string serverName, [StringSyntax(StringSyntaxAttribut /// /// The name of the remote MCP server. /// The URL of the remote MCP server. - /// or are . + /// or is . /// is empty or composed entirely of whitespace. public HostedMcpServerTool(string serverName, Uri url) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationReportWriter.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationReportWriter.cs index 97c1fdca15e..70b6492a17c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationReportWriter.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/IEvaluationReportWriter.cs @@ -17,7 +17,7 @@ public interface IEvaluationReportWriter /// Writes a report containing all the s present in the supplied /// s. /// - /// An enumeration of s. + /// A collection of run results from which to generate the report. /// A that can cancel the operation. /// A that represents the asynchronous operation. ValueTask WriteReportAsync( diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataConfigurationBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataConfigurationBuilderExtensions.cs index 4dfacb812de..c3b303a5a0e 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataConfigurationBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataConfigurationBuilderExtensions.cs @@ -20,8 +20,8 @@ public static class ApplicationMetadataConfigurationBuilderExtensions /// /// The configuration builder. /// An instance of . - /// Section name to save configuration into. Default set to "ambientmetadata:application". - /// The value of >. + /// The section name to save configuration into. The default is "ambientmetadata:application". + /// The value of . /// or is . /// is either , empty, or whitespace. public static IConfigurationBuilder AddApplicationMetadata(this IConfigurationBuilder builder, IHostEnvironment hostEnvironment, string sectionName = DefaultSectionName) diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataServiceCollectionExtensions.cs index bc08c2a60e9..1e84e50c4f1 100644 --- a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataServiceCollectionExtensions.cs @@ -35,7 +35,7 @@ public static IServiceCollection AddApplicationMetadata(this IServiceCollection /// /// The dependency injection container to add the instance to. /// The delegate to configure with. - /// The value of >. + /// The value of . /// or is . public static IServiceCollection AddApplicationMetadata(this IServiceCollection services, Action configure) { diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs index 478faa315ea..f10827b1954 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AsyncState; /// /// Provides access to the current async context. -/// Some implementations of this interface may not be thread safe. +/// Some implementations of this interface might not be thread safe. /// /// The type of the asynchronous state. public interface IAsyncContext diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs index 9873375a78b..ed33f8afeb2 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.AsyncState; /// /// Provides access to the current async context stored outside of the HTTP pipeline. -/// Some implementations of this interface may not be thread safe. +/// Some implementations of this interface might not be thread safe. /// /// The type of the asynchronous state. /// This type is intended for internal use. Use instead. diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs index 2593136a736..4bf4c748146 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AsyncState; /// /// Encapsulates all information within the asynchronous flow in an variable. -/// Some implementations of this interface may not be thread safe. +/// Some implementations of this interface might not be thread safe. /// public interface IAsyncState { diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionServiceCollectionExtensions.cs index eab81632049..43637a7d726 100644 --- a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionServiceCollectionExtensions.cs @@ -18,7 +18,7 @@ public static class FakeRedactionServiceCollectionExtensions /// /// Registers the fake redactor provider that always returns fake redactor instances. /// - /// Container used to register fake redaction classes. + /// The container used to register fake redaction classes. /// The value of . /// is . public static IServiceCollection AddFakeRedaction(this IServiceCollection services) @@ -42,10 +42,10 @@ public static IServiceCollection AddFakeRedaction(this IServiceCollection servic /// /// Registers the fake redactor provider that always returns fake redactor instances. /// - /// Container used to register fake redaction classes. + /// The container used to register fake redaction classes. /// Configures fake redactor. /// The value of . - /// or > are . + /// or is . public static IServiceCollection AddFakeRedaction(this IServiceCollection services, Action configure) { _ = Throw.IfNull(services); From 8e32d98fda73d246526cc953da5eeeb850de2c6e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 3 Oct 2025 15:26:29 -0400 Subject: [PATCH 340/472] Update AI changelogs with some recent additions (#6886) --- .../Microsoft.Extensions.AI.Abstractions/CHANGELOG.md | 4 +++- .../Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md | 2 ++ src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 2 ++ src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index af9fa5a328d..433978c322a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -2,7 +2,9 @@ ## NOT YET RELEASED -- Added protected copy constructors to options types. +- Added protected copy constructors to options types (e.g. `ChatOptions`). +- Fixed `EmbeddingGeneratorOptions`/`SpeechToTextOptions` `Clone` methods to correctly copy all properties. +- Fixed `ToChatResponse` to not overwrite `ChatMessage/ChatResponse.CreatedAt` with older timestamps during coalescing. ## 9.9.1 diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md index 15b9f840773..f3e0a7b96d5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md @@ -1,5 +1,7 @@ # Release History +## NOT YET RELEASED + ## 9.9.1-preview.1.25474.6 - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 03fe8357eab..eb7152558ad 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,5 +1,7 @@ # Release History +## NOT YET RELEASED + ## 9.9.1-preview.1.25474.6 - Updated to depend on OpenAI 2.5.0. diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 275131b595a..987afd4b437 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## NOT YET RELEASED + +- Added `OpenTelemetrySpeechToTextClient` to provide Open Telemetry instrumentation for `ISpeechToTextClient` implementations. + ## 9.9.1 - Updated the `EnableSensitiveData` properties on `OpenTelemetryChatClient/EmbeddingGenerator` to respect a `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. From 22cbbb76d2106f6c6a1a84c1465b2af11b0e59bb Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 6 Oct 2025 17:04:59 -0400 Subject: [PATCH 341/472] Fix Assistants IChatClient handling of unrelated tool calls in history (#6891) The implementation is assuming that any tool results in the incoming message history mean a thread ID is required, which would be necessary if those tool results were for an active run. But that's not the case if the history just contains historical function calls/responses, as is the case in agent scenarios where the message history from one agent is passed to another. The fix is to just stop throwing based on this incorrect assumption. --- .../Microsoft.Extensions.AI.OpenAI/CHANGELOG.md | 2 ++ .../OpenAIAssistantsChatClient.cs | 12 ++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index eb7152558ad..589790cd8ee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -2,6 +2,8 @@ ## NOT YET RELEASED +- Fixed issue with IChatClient for Assistants API where a chat history including unrelated function calls would cause an exception. + ## 9.9.1-preview.1.25474.6 - Updated to depend on OpenAI 2.5.0. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index ea682a11beb..20bc87dd9f3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -80,14 +80,10 @@ public async IAsyncEnumerable GetStreamingResponseAsync( // Get the thread ID. string? threadId = options?.ConversationId ?? _defaultThreadId; - if (threadId is null && toolResults is not null) - { - Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results."); - } // Get any active run ID for this thread. This is necessary in case a thread has been left with an - // active run, in which all attempts other than submitting tools will fail. We thus need to cancel - // any active run on the thread. + // active run, in which case all attempts other than submitting tools will fail. We thus need to cancel + // any active run on the thread if we're not submitting tool results to it. ThreadRun? threadRun = null; if (threadId is not null) { @@ -111,7 +107,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && toolRunId == threadRun.Id) { - // There's an active run and we have tool results to submit, so submit the results and continue streaming. + // There's an active run and, critically, we have tool results to submit for that exact run, so submit the results and continue streaming. // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs, // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare. updates = _client.SubmitToolOutputsToRunStreamingAsync(threadRun.ThreadId, threadRun.Id, toolOutputs, cancellationToken); @@ -487,7 +483,7 @@ void AppendSystemInstructions(string? toAppend) messageContents.Add(MessageContent.FromImageUri(image.Uri)); break; - case FunctionResultContent result: + case FunctionResultContent result when chatMessage.Role == ChatRole.Tool: (functionResults ??= []).Add(result); break; } From 53ab8925343db6cdb1a10b2ab2cf70a976e85d9f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 7 Oct 2025 11:33:16 -0400 Subject: [PATCH 342/472] Fix duplication with OpenAI Assistants pre-configured tools (#6896) Assistants lets you configure an agent with function signatures it's implicitly aware of, rather than them needing to be provided per request. That, however, creates complication for callers, as if they provide that function in ChatTools.Options, it leads to the function's signature being sent as part of the request, and the duplication of it with the pre-configured function signature results in an error. It's possible to work around this, by simply not including that function in the request, but it's a natural thing for a developer to do, especially if they want the function to be automatically invoked when the model requests it. You can achieve that by putting the function into the FunctionInvocationChatClient's AdditionalTools, which exists for this kind of purpose, but that's harder to discover. Rather than try something more complicated, this commit simply deduplicates all tools by putting them into a set, deduplicating any duplicates provided. --- .../OpenAIAssistantsChatClient.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 20bc87dd9f3..1de5dd79d4a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -306,6 +306,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( if (options.Tools is { Count: > 0 } tools) { + HashSet toolsOverride = new(ToolDefinitionNameEqualityComparer.Instance); + // If the caller has provided any tool overrides, we'll assume they don't want to use the assistant's tools. // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to // just add them. To handle that, we'll get all of the assistant's tools and add them to the override list @@ -318,10 +320,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( _assistantTools = assistant.Value.Tools; } - foreach (var tool in _assistantTools) - { - runOptions.ToolsOverride.Add(tool); - } + toolsOverride.UnionWith(_assistantTools); } // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. @@ -330,12 +329,12 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( switch (tool) { case AIFunctionDeclaration aiFunction: - runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); + _ = toolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; case HostedCodeInterpreterTool codeInterpreterTool: var interpreterToolDef = ToolDefinition.CreateCodeInterpreter(); - runOptions.ToolsOverride.Add(interpreterToolDef); + _ = toolsOverride.Add(interpreterToolDef); if (codeInterpreterTool.Inputs?.Count is > 0) { @@ -358,7 +357,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; case HostedFileSearchTool fileSearchTool: - runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); + _ = toolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs) { foreach (var input in fileSearchInputs) @@ -374,6 +373,11 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; } } + + foreach (var tool in toolsOverride) + { + runOptions.ToolsOverride.Add(tool); + } } // Store the tool mode, if relevant. @@ -543,4 +547,22 @@ void AppendSystemInstructions(string? toAppend) return runId; } + + /// + /// Provides the same behavior as , except + /// for it compares names so that two function tool definitions with the + /// same name compare equally. + /// + private sealed class ToolDefinitionNameEqualityComparer : IEqualityComparer + { + public static ToolDefinitionNameEqualityComparer Instance { get; } = new(); + + public bool Equals(ToolDefinition? x, ToolDefinition? y) => + x is FunctionToolDefinition xFtd && y is FunctionToolDefinition yFtd ? xFtd.FunctionName.Equals(yFtd.FunctionName, StringComparison.Ordinal) : + EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(ToolDefinition obj) => + obj is FunctionToolDefinition ftd ? ftd.FunctionName.GetHashCode(StringComparison.Ordinal) : + EqualityComparer.Default.GetHashCode(obj); + } } From 4f78337697b739e12c17d3133c7f36105d0fdaef Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Tue, 7 Oct 2025 11:54:50 -0400 Subject: [PATCH 343/472] Update MCP Server Template to adhere to 2025-09-29 server.json schema (#6888) * Update to latest schema * Fix test files --- .../src/McpServer/McpServer-CSharp/.mcp/server.json | 8 ++++---- .../mcpserver.AotTrue.verified/mcpserver/.mcp/server.json | 8 ++++---- .../mcpserver.Basic.verified/mcpserver/.mcp/server.json | 8 ++++---- .../mcpserver/.mcp/server.json | 8 ++++---- .../mcpserver.net10.verified/mcpserver/.mcp/server.json | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json index 8a2ad2b07a5..ec5eab2b15c 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -1,18 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "description": "", "name": "io.github./", "version": "0.1.0-beta", "packages": [ { - "registry_type": "nuget", + "registryType": "nuget", "identifier": "", "version": "0.1.0-beta", "transport": { "type": "stdio" }, - "package_arguments": [], - "environment_variables": [] + "packageArguments": [], + "environmentVariables": [] } ], "repository": { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json index 7c7602bea75..3c028ad6c40 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/.mcp/server.json @@ -1,18 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "description": "", "name": "io.github./", "version": "0.1.0-beta", "packages": [ { - "registry_type": "nuget", + "registryType": "nuget", "identifier": "", "version": "0.1.0-beta", "transport": { "type": "stdio" }, - "package_arguments": [], - "environment_variables": [] + "packageArguments": [], + "environmentVariables": [] } ], "repository": { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json index 7c7602bea75..3c028ad6c40 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -1,18 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "description": "", "name": "io.github./", "version": "0.1.0-beta", "packages": [ { - "registry_type": "nuget", + "registryType": "nuget", "identifier": "", "version": "0.1.0-beta", "transport": { "type": "stdio" }, - "package_arguments": [], - "environment_variables": [] + "packageArguments": [], + "environmentVariables": [] } ], "repository": { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json index 7c7602bea75..3c028ad6c40 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/.mcp/server.json @@ -1,18 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "description": "", "name": "io.github./", "version": "0.1.0-beta", "packages": [ { - "registry_type": "nuget", + "registryType": "nuget", "identifier": "", "version": "0.1.0-beta", "transport": { "type": "stdio" }, - "package_arguments": [], - "environment_variables": [] + "packageArguments": [], + "environmentVariables": [] } ], "repository": { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json index 7c7602bea75..3c028ad6c40 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/.mcp/server.json @@ -1,18 +1,18 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "description": "", "name": "io.github./", "version": "0.1.0-beta", "packages": [ { - "registry_type": "nuget", + "registryType": "nuget", "identifier": "", "version": "0.1.0-beta", "transport": { "type": "stdio" }, - "package_arguments": [], - "environment_variables": [] + "packageArguments": [], + "environmentVariables": [] } ], "repository": { From 5858a796217aab02fe4bcabe67447987c2cd6530 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:52:59 -0700 Subject: [PATCH 344/472] Remove Azure.AI.OpenAI dependency from templates and tests (#6873) * Initial plan * Remove Azure.AI.OpenAI package references from test projects Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix Azure authentication to use OpenAI client with token credentials Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update template source to use OpenAI client for Azure OpenAI Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Remove remaining package references to Azure.AI.OpenAI * Use computed symbols for all bool checks in the template. Sort usings in Program.cs. * Update to use BearerTokenPolicy * Change scope * Bump Azure.Identity. Fix up usings and pragmas. Clearer if/elif logic. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Jeff Handley Co-authored-by: Stephen Toub --- eng/packages/General.props | 4 +- eng/packages/TestOnly.props | 1 - src/ProjectTemplates/GeneratedContent.targets | 4 +- .../.template.config/template.json | 16 +++-- .../AppHost.cs | 14 ++-- ...hatWithCustomData-CSharp.AppHost.csproj.in | 4 +- .../ChatWithCustomData-CSharp.Web.csproj.in | 17 ++--- .../Program.Aspire.cs | 16 ++--- .../ChatWithCustomData-CSharp.Web/Program.cs | 69 +++++++++++-------- .../ChatWithCustomData-CSharp.Web/README.md | 8 +-- .../Services/IngestedChunk.cs | 4 +- .../Services/IngestedDocument.cs | 4 +- .../Services/Ingestion/DataIngestor.cs | 2 +- .../Services/Ingestion/PDFDirectorySource.cs | 4 +- .../Services/SemanticSearch.cs | 2 +- .../src/ChatWithCustomData/README.Aspire.md | 6 +- ...ons.AI.Evaluation.Integration.Tests.csproj | 3 +- .../Setup.cs | 30 ++++---- .../IntegrationTestHelpers.cs | 17 +++-- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 2 +- .../aichatweb/README.md | 3 + .../aichatweb.Web/aichatweb.Web.csproj | 1 - .../aichatweb/Program.cs | 6 +- .../aichatweb/aichatweb.Web/Program.cs | 2 +- .../aichatweb.Web/aichatweb.Web.csproj | 1 - .../aichatweb/Program.cs | 16 +++-- .../aichatweb/README.md | 3 + .../aichatweb/aichatweb.csproj | 2 +- 28 files changed, 139 insertions(+), 122 deletions(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index 7a5bd0d46a0..b66f1a4ffa8 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -1,8 +1,8 @@ - - + + diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index d8748a4ae9c..5875491f919 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -2,7 +2,6 @@ - diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 2d30c4faf5f..11fc8667c96 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -35,9 +35,8 @@ 9.5.0 9.5.0-preview.1.25474.7 - 2.3.0-beta.2 1.0.0-beta.9 - 1.14.0 + 1.16.0 11.6.1 9.4.1-beta.291 10.0.0-preview.6.25358.103 @@ -62,7 +61,6 @@ TemplatePackageVersion_Aspire=$(TemplatePackageVersion_Aspire); TemplatePackageVersion_Aspire_Preview=$(TemplatePackageVersion_Aspire_Preview); - TemplatePackageVersion_AzureAIOpenAI=$(TemplatePackageVersion_AzureAIOpenAI); TemplatePackageVersion_AzureAIProjects=$(TemplatePackageVersion_AzureAIProjects); TemplatePackageVersion_AzureIdentity=$(TemplatePackageVersion_AzureIdentity); TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json index cffbaa7598f..603ed1a2735 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json @@ -88,7 +88,7 @@ ] }, { - "condition": "(!UseLocalVectorStore)", + "condition": "(!IsLocalVectorStore)", "exclude": [ "ChatWithCustomData-CSharp.Web/Services/JsonVectorStore.cs" ] @@ -185,6 +185,10 @@ "defaultValue": "false", "description": "Create the project as a distributed application using Aspire." }, + "IsManagedIdentity": { + "type": "computed", + "value": "(UseManagedIdentity)" + }, "IsAspire": { "type": "computed", "value": "(UseAspire || VectorStore == \"qdrant\")" @@ -209,21 +213,21 @@ "type": "computed", "value": "(AiServiceProvider == \"azureaifoundry\")" }, - "UseAzureAISearch": { + "IsAzureAISearch": { "type": "computed", "value": "(VectorStore == \"azureaisearch\")" }, - "UseLocalVectorStore": { + "IsLocalVectorStore": { "type": "computed", "value": "(VectorStore == \"local\")" }, - "UseQdrant": { + "IsQdrant": { "type": "computed", "value": "(VectorStore == \"qdrant\")" }, - "UseAzure": { + "IsAzure": { "type": "computed", - "value": "(IsAzureOpenAI || IsAzureAiFoundry || UseAzureAISearch)" + "value": "(IsAzureOpenAI || IsAzureAIFoundry || IsAzureAISearch)" }, "ChatModel": { "type": "parameter", diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs index df2f6765d9e..a859ce397a1 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/AppHost.cs @@ -29,7 +29,7 @@ modelName: "text-embedding-3-small", modelVersion: "1"); #endif -#if (UseAzureAISearch) +#if (IsAzureAISearch) // See https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration // for instructions providing configuration values @@ -42,13 +42,13 @@ var chat = ollama.AddModel("chat", "llama3.2"); var embeddings = ollama.AddModel("embeddings", "all-minilm"); #endif -#if (UseAzureAISearch) // VECTOR DATABASE CONFIGURATION -#elif (UseQdrant) +#if (IsAzureAISearch) // VECTOR DATABASE CONFIGURATION +#elif (IsQdrant) var vectorDB = builder.AddQdrant("vectordb") .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent); -#else // UseLocalVectorStore +#else // IsLocalVectorStore #endif var webApp = builder.AddProject("aichatweb-app"); @@ -65,15 +65,15 @@ .WithReference(openai) .WaitFor(openai); #endif -#if (UseAzureAISearch) // VECTOR DATABASE REFERENCES +#if (IsAzureAISearch) // VECTOR DATABASE REFERENCES webApp .WithReference(search) .WaitFor(search); -#elif (UseQdrant) +#elif (IsQdrant) webApp .WithReference(vectorDB) .WaitFor(vectorDB); -#else // UseLocalVectorStore +#else // IsLocalVectorStore #endif builder.Build().Run(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in index 8f5b8baabc0..b7d8f13bdfa 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.AppHost/ChatWithCustomData-CSharp.AppHost.csproj.in @@ -12,9 +12,9 @@ - diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 0e753e8c907..05e9a9726de 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -15,38 +15,35 @@ #elif ((IsGHModels || IsOpenAI) && !IsAspire) -#elif (IsAzureAiFoundry) - +#elif (IsAzureAIFoundry) #endif --> - - - + - - +#elif (IsQdrant)--> - + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs index 243aa46c8d7..e7137ac6dd3 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs @@ -1,12 +1,10 @@ using Microsoft.Extensions.AI; +#if (IsOpenAI || IsGHModels) +using OpenAI; +#endif using ChatWithCustomData_CSharp.Web.Components; using ChatWithCustomData_CSharp.Web.Services; using ChatWithCustomData_CSharp.Web.Services.Ingestion; -#if (IsOllama) -#elif (IsOpenAI || IsGHModels) -using OpenAI; -#else // IsAzureOpenAI -#endif var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); @@ -20,7 +18,7 @@ c.EnableSensitiveData = builder.Environment.IsDevelopment()); builder.AddOllamaApiClient("embeddings") .AddEmbeddingGenerator(); -#elif (IsAzureAiFoundry) +#elif (IsAzureAIFoundry) #else // (IsOpenAI || IsAzureOpenAI || IsGHModels) #if (IsOpenAI) var openai = builder.AddOpenAIClient("openai"); @@ -34,15 +32,15 @@ openai.AddEmbeddingGenerator("text-embedding-3-small"); #endif -#if (UseAzureAISearch) +#if (IsAzureAISearch) builder.AddAzureSearchClient("search"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks"); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents"); -#elif (UseQdrant) +#elif (IsQdrant) builder.AddQdrantClient("vectordb"); builder.Services.AddQdrantCollection("data-ChatWithCustomData-CSharp.Web-chunks"); builder.Services.AddQdrantCollection("data-ChatWithCustomData-CSharp.Web-documents"); -#else // UseLocalVectorStore +#else // IsLocalVectorStore var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-chunks", vectorStoreConnectionString); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs index f8d2826ab9a..3765185721a 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs @@ -1,22 +1,22 @@ -using Microsoft.Extensions.AI; -using ChatWithCustomData_CSharp.Web.Components; -using ChatWithCustomData_CSharp.Web.Services; -using ChatWithCustomData_CSharp.Web.Services.Ingestion; -#if(IsAzureOpenAI || UseAzureAISearch) +#if (IsGHModels || IsOpenAI || (IsAzureOpenAI && !IsManagedIdentity)) +using System.ClientModel; +#elif (IsAzureOpenAI && IsManagedIdentity) +using System.ClientModel.Primitives; +#endif +#if (IsAzureAISearch && !IsManagedIdentity) using Azure; -#if (UseManagedIdentity) +#elif (IsManagedIdentity) using Azure.Identity; #endif -#endif +using Microsoft.Extensions.AI; #if (IsOllama) using OllamaSharp; -#elif (IsOpenAI || IsGHModels) +#elif (IsGHModels || IsOpenAI || IsAzureOpenAI) using OpenAI; -using System.ClientModel; -#else -using Azure.AI.OpenAI; -using System.ClientModel; #endif +using ChatWithCustomData_CSharp.Web.Components; +using ChatWithCustomData_CSharp.Web.Services; +using ChatWithCustomData_CSharp.Web.Services.Ingestion; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); @@ -45,54 +45,63 @@ // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory // dotnet user-secrets set OpenAI:Key YOUR-API-KEY + var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +#pragma warning disable OPENAI001 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates. var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); -#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore OPENAI001 + var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); -#elif (IsAzureAiFoundry) +#elif (IsAzureAIFoundry) -#else // IsAzureOpenAI +#elif (IsAzureOpenAI) // You will need to set the endpoint and key to your own values // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory // dotnet user-secrets set AzureOpenAI:Endpoint https://YOUR-DEPLOYMENT-NAME.openai.azure.com -#if (!UseManagedIdentity) +#if (!IsManagedIdentity) // dotnet user-secrets set AzureOpenAI:Key YOUR-API-KEY #endif -var azureOpenAi = new AzureOpenAIClient( - new Uri(builder.Configuration["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Endpoint. See the README for details.")), -#if (UseManagedIdentity) - new DefaultAzureCredential()); -#else - new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details."))); +var azureOpenAIEndpoint = new Uri(new Uri(builder.Configuration["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Endpoint. See the README for details.")), "/openai/v1"); +#if (IsManagedIdentity) +#pragma warning disable OPENAI001 // OpenAIClient(AuthenticationPolicy, OpenAIClientOptions) and GetOpenAIResponseClient(string) are experimental and subject to change or removal in future updates. +var azureOpenAi = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint }); + +#elif (!IsManagedIdentity) +var openAIOptions = new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint }; +var azureOpenAi = new OpenAIClient(new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details.")), openAIOptions); + +#pragma warning disable OPENAI001 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates. #endif -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var chatClient = azureOpenAi.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); -#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore OPENAI001 + var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); #endif -#if (UseAzureAISearch) +#if (IsAzureAISearch) // You will need to set the endpoint and key to your own values // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory // dotnet user-secrets set AzureAISearch:Endpoint https://YOUR-DEPLOYMENT-NAME.search.windows.net -#if (!UseManagedIdentity) +#if (!IsManagedIdentity) // dotnet user-secrets set AzureAISearch:Key YOUR-API-KEY #endif var azureAISearchEndpoint = new Uri(builder.Configuration["AzureAISearch:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Endpoint. See the README for details.")); -#if (UseManagedIdentity) +#if (IsManagedIdentity) var azureAISearchCredential = new DefaultAzureCredential(); -#else +#elif (!IsManagedIdentity) var azureAISearchCredential = new AzureKeyCredential(builder.Configuration["AzureAISearch:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureAISearch:Key. See the README for details.")); #endif builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-chunks", azureAISearchEndpoint, azureAISearchCredential); builder.Services.AddAzureAISearchCollection("data-ChatWithCustomData-CSharp.Web-documents", azureAISearchEndpoint, azureAISearchCredential); -#else // UseLocalVectorStore +#elif (IsLocalVectorStore) var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; builder.Services.AddSqliteCollection("data-ChatWithCustomData-CSharp.Web-chunks", vectorStoreConnectionString); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md index a828e164ff8..88dff74d315 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/README.md @@ -5,7 +5,7 @@ This project is an AI chat application that demonstrates how to chat with custom >[!NOTE] > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. -#### ---#if (UseAzure) +#### ---#if (IsAzure) ### Prerequisites To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). @@ -104,7 +104,7 @@ To use Azure OpenAI, you will need an Azure account and an Azure OpenAI Service ### 2. Deploy the Models Deploy the `gpt-4o-mini` and `text-embedding-3-small` models to your Azure OpenAI Service resource. When creating those deployments, give them the same names as the models (`gpt-4o-mini` and `text-embedding-3-small`). See the Azure OpenAI documentation to learn how to [Deploy a model](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). -#### ---#if (UseManagedIdentity) +#### ---#if (IsManagedIdentity) ### 3. Configure Azure OpenAI for Keyless Authentication This template is configured to use keyless authentication (also known as Managed Identity, with Entra ID). In the Azure Portal, when viewing the Azure OpenAI resource you just created, view access control settings and assign yourself the `Azure AI Developer` role. [Learn more about configuring authentication for local development](https://learn.microsoft.com/azure/developer/ai/keyless-connections?tabs=csharp%2Cazure-cli#authenticate-for-local-development). @@ -160,7 +160,7 @@ Make sure to replace `YOUR-AZURE-OPENAI-KEY` and `YOUR-AZURE-OPENAI-ENDPOINT` wi #### ---#endif #### ---#endif -#### ---#if (UseAzureAISearch) +#### ---#if (IsAzureAISearch) ## Configure Azure AI Search To use Azure AI Search, you will need an Azure account and an Azure AI Search resource. For detailed instructions, see the [Azure AI Search documentation](https://learn.microsoft.com/azure/search/search-create-service-portal). @@ -170,7 +170,7 @@ Follow the instructions in the [Azure portal](https://portal.azure.com/) to crea Note that if you previously used the same Azure AI Search resource with different model using this project name, you may need to delete your `data-ChatWithCustomData-CSharp.Web-chunks` and `data-ChatWithCustomData-CSharp.Web-documents` indexes using the [Azure portal](https://portal.azure.com/) first before continuing; otherwise, data ingestion may fail due to a vector dimension mismatch. -#### ---#if (UseManagedIdentity) +#### ---#if (IsManagedIdentity) ### 2. Configure Azure AI Search for Keyless Authentication This template is configured to use keyless authentication (also known as Managed Identity, with Entra ID). Before continuing, you'll need to configure your Azure AI Search resource to support this. [Learn more](https://learn.microsoft.com/azure/search/keyless-connections). After creation, ensure that you have selected Role-Based Access Control (RBAC) under Settings > Keys, as this is not the default. Assign yourself the roles called out for local development. [Learn more](https://learn.microsoft.com/azure/search/keyless-connections#roles-for-local-development). diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs index c1369e1bf65..deff2580f52 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedChunk.cs @@ -9,14 +9,14 @@ public class IngestedChunk #else private const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model #endif -#if (UseAzureAISearch || UseQdrant) +#if (IsAzureAISearch || IsQdrant) private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; #else private const string VectorDistanceFunction = DistanceFunction.CosineDistance; #endif [VectorStoreKey] -#if (UseQdrant) +#if (IsQdrant) public required Guid Key { get; set; } #else public required string Key { get; set; } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs index 339a7479217..27ea85df7b8 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/IngestedDocument.cs @@ -5,14 +5,14 @@ namespace ChatWithCustomData_CSharp.Web.Services; public class IngestedDocument { private const int VectorDimensions = 2; -#if (UseAzureAISearch || UseQdrant) +#if (IsAzureAISearch || IsQdrant) private const string VectorDistanceFunction = DistanceFunction.CosineSimilarity; #else private const string VectorDistanceFunction = DistanceFunction.CosineDistance; #endif [VectorStoreKey] -#if (UseQdrant) +#if (IsQdrant) public required Guid Key { get; set; } #else public required string Key { get; set; } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs index 175cc505670..5440772df42 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/DataIngestor.cs @@ -5,7 +5,7 @@ namespace ChatWithCustomData_CSharp.Web.Services.Ingestion; public class DataIngestor( ILogger logger, -#if (UseQdrant) +#if (IsQdrant) VectorStoreCollection chunksCollection, VectorStoreCollection documentsCollection) #else diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs index fdbe058556e..0ea678d888b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/Ingestion/PDFDirectorySource.cs @@ -26,7 +26,7 @@ public Task> GetNewOrModifiedDocumentsAsync(IReadO var existingDocumentVersion = existingDocumentsById.TryGetValue(sourceFileId, out var existingDocument) ? existingDocument.DocumentVersion : null; if (existingDocumentVersion != sourceFileVersion) { -#if (UseQdrant) +#if (IsQdrant) results.Add(new() { Key = Guid.CreateVersion7(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); #else results.Add(new() { Key = Guid.CreateVersion7().ToString(), SourceId = SourceId, DocumentId = sourceFileId, DocumentVersion = sourceFileVersion }); @@ -52,7 +52,7 @@ public Task> CreateChunksForDocumentAsync(IngestedDoc return Task.FromResult(paragraphs.Select(p => new IngestedChunk { -#if (UseQdrant) +#if (IsQdrant) Key = Guid.CreateVersion7(), #else Key = Guid.CreateVersion7().ToString(), diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs index 44cfcc18fc4..42abf3151fc 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Services/SemanticSearch.cs @@ -3,7 +3,7 @@ namespace ChatWithCustomData_CSharp.Web.Services; public class SemanticSearch( -#if (UseQdrant) +#if (IsQdrant) VectorStoreCollection vectorCollection) #else VectorStoreCollection vectorCollection) diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md index 14aa513acae..f30fcbfcf2e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/README.Aspire.md @@ -5,7 +5,7 @@ This project is an AI chat application that demonstrates how to chat with custom >[!NOTE] > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. -#### ---#if (UseAzure) +#### ---#if (IsAzure) ### Prerequisites To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). @@ -81,13 +81,13 @@ Download, install, and run Docker Desktop from the [official website](https://ww Note: Ollama and Docker are excellent open source products, but are not maintained by Microsoft. #### ---#endif -#### ---#if (IsAzureOpenAI || UseAzureAISearch) +#### ---#if (IsAzureOpenAI || IsAzureAISearch) ## Using Azure Provisioning The project is set up to automatically provision Azure resources. When running the app for the first time, you will be prompted to provide Azure configuration values. For detailed instructions, see the [Local Provisioning documentation](https://learn.microsoft.com/dotnet/aspire/azure/local-provisioning#configuration). #### ---#endif -#### ---#if (UseQdrant) +#### ---#if (IsQdrant) ## Setting up a local environment for Qdrant This project is configured to run Qdrant in a Docker container. Docker Desktop must be installed and running for the project to run successfully. A Qdrant container will automatically start when running the application. diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj index 6e3332ebca6..8ee7f39ee1c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Microsoft.Extensions.AI.Evaluation.Integration.Tests.csproj @@ -4,6 +4,7 @@ $(LatestTargetFramework) Microsoft.Extensions.AI Integration tests for Microsoft.Extensions.AI.Evaluation. + $(NoWarn);OPENAI001 @@ -20,7 +21,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs index 388ba1f1415..8ff4d7f23d3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Evaluation.Integration.Tests/Setup.cs @@ -3,8 +3,9 @@ using System; using System.ClientModel; -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; +using OpenAI; namespace Microsoft.Extensions.AI.Evaluation.Integration.Tests; @@ -15,22 +16,27 @@ internal static class Setup internal static ChatConfiguration CreateChatConfiguration() { - AzureOpenAIClient azureOpenAIClient = GetAzureOpenAIClient(); - IChatClient chatClient = azureOpenAIClient.GetChatClient(Settings.Current.DeploymentName).AsIChatClient(); + OpenAI.Chat.ChatClient openAIClient = GetOpenAIClient(); + IChatClient chatClient = openAIClient.AsIChatClient(); return new ChatConfiguration(chatClient); } - private static AzureOpenAIClient GetAzureOpenAIClient() + private static OpenAI.Chat.ChatClient GetOpenAIClient() { - var endpoint = new Uri(Settings.Current.Endpoint); - AzureOpenAIClientOptions options = new(); - var credential = new ChainedTokenCredential(new AzureCliCredential(), new DefaultAzureCredential()); + // Use Azure endpoint with /openai/v1 suffix + var options = new OpenAIClientOptions + { + Endpoint = new Uri(new Uri(Settings.Current.Endpoint), "/openai/v1") + }; - AzureOpenAIClient azureOpenAIClient = - OfflineOnly - ? new AzureOpenAIClient(endpoint, new ApiKeyCredential("Bogus"), options) - : new AzureOpenAIClient(endpoint, credential, options); + OpenAIClient client = OfflineOnly ? + new OpenAIClient(new ApiKeyCredential("Bogus"), options) : + new OpenAIClient( + new BearerTokenPolicy( + new ChainedTokenCredential(new AzureCliCredential(), new DefaultAzureCredential()), + "https://ai.azure.com/.default"), + options); - return azureOpenAIClient; + return client.GetChatClient(Settings.Current.DeploymentName); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index 5c98f814868..d06f20c3f74 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -3,7 +3,7 @@ using System; using System.ClientModel; -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using OpenAI; @@ -25,14 +25,13 @@ internal static class IntegrationTestHelpers var endpoint = configuration["OpenAI:Endpoint"] ?? throw new InvalidOperationException("To use AzureOpenAI, set a value for OpenAI:Endpoint"); - if (apiKey is not null) - { - return new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey)); - } - else - { - return new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()); - } + // Use Azure endpoint with /openai/v1 suffix + var options = new OpenAIClientOptions { Endpoint = new Uri(new Uri(endpoint), "/openai/v1") }; + return apiKey is not null ? + new OpenAIClient(new ApiKeyCredential(apiKey), options) : + new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + options); } else if (apiKey is not null) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 536c250cb47..093260d779c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -32,7 +32,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md index a2f61924c32..7bebc5d7595 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/README.md @@ -5,6 +5,9 @@ This project is an AI chat application that demonstrates how to chat with custom >[!NOTE] > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. +### Prerequisites +To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). + ### Known Issues #### Errors running Ollama or Docker diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 56e1e1ec23a..619003eb225 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,7 +8,6 @@ - diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs index 0e97b8efffe..1ff3845eb08 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Program.cs @@ -1,9 +1,9 @@ -using Microsoft.Extensions.AI; +using System.ClientModel; +using Microsoft.Extensions.AI; +using OpenAI; using aichatweb.Components; using aichatweb.Services; using aichatweb.Services.Ingestion; -using OpenAI; -using System.ClientModel; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs index a98905903b3..6d23308d93a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.AI; +using OpenAI; using aichatweb.Web.Components; using aichatweb.Web.Services; using aichatweb.Web.Services.Ingestion; -using OpenAI; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index ed4f3c914c8..16f0b1a2e8f 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,7 +8,6 @@ - diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs index d469d9c43db..434e5662d6d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs @@ -1,11 +1,10 @@ -using Microsoft.Extensions.AI; +using System.ClientModel; +using Azure.Identity; +using Microsoft.Extensions.AI; +using OpenAI; using aichatweb.Components; using aichatweb.Services; using aichatweb.Services.Ingestion; -using Azure; -using Azure.Identity; -using OpenAI; -using System.ClientModel; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); @@ -14,11 +13,14 @@ // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory // dotnet user-secrets set OpenAI:Key YOUR-API-KEY + var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +#pragma warning disable OPENAI001 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates. var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient(); -#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore OPENAI001 + var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); // You will need to set the endpoint and key to your own values diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md index 3f50502fb9f..73375f14cfa 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/README.md @@ -5,6 +5,9 @@ This project is an AI chat application that demonstrates how to chat with custom >[!NOTE] > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. +### Prerequisites +To use Azure OpenAI or Azure AI Search, you need an Azure account. If you don't already have one, [create an Azure account](https://azure.microsoft.com/free/). + # Configure the AI Model Provider ## Using OpenAI diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index ceb6b34e3bf..383bfa54682 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -9,7 +9,7 @@ - + From 01354adeaf015558dfda647098b205a121a64ab9 Mon Sep 17 00:00:00 2001 From: Peter Waldschmidt Date: Wed, 8 Oct 2025 16:41:29 -0400 Subject: [PATCH 345/472] Remove UnsafeRelaxedJsonEscaping (#6899) --- .../JsonSerialization/AzureStorageJsonUtilities.cs | 2 -- .../CSharp/JsonSerialization/JsonUtilities.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs index c2f7b418b01..da8d699699d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting.Azure/JsonSerialization/AzureStorageJsonUtilities.cs @@ -1,7 +1,6 @@ // 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.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -32,7 +31,6 @@ private static JsonSerializerOptions CreateJsonSerializerOptions(bool writeInden var options = new JsonSerializerOptions(JsonContext.Default.Options) { WriteIndented = writeIndented, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); options.MakeReadOnly(); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs index 9427b67be8e..a94282ad7f3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/JsonSerialization/JsonUtilities.cs @@ -1,7 +1,6 @@ // 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.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -35,7 +34,6 @@ private static JsonSerializerOptions CreateJsonSerializerOptions(bool writeInden var options = new JsonSerializerOptions(JsonContext.Default.Options) { WriteIndented = writeIndented, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); options.MakeReadOnly(); From a9b33850bb93d4e6977ae6015ed9bcb27fa06383 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:38:46 +0100 Subject: [PATCH 346/472] Fix bug to yield remaining buffered FCC (#6903) --- .../FunctionInvokingChatClient.cs | 10 +- ...unctionInvokingChatClientApprovalsTests.cs | 115 ++++++++++++++++-- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index fdc5ef7a204..febf0a1336e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -529,7 +529,7 @@ public override async IAsyncEnumerable GetStreamingResponseA .ToArray(); } - if (approvalRequiredFunctions is not { Length: > 0 }) + if (approvalRequiredFunctions is not { Length: > 0 } || functionCallContents is not { Count: > 0 }) { // If there are no function calls to make yet, or if none of the functions require approval at all, // we can yield the update as-is. @@ -574,6 +574,14 @@ public override async IAsyncEnumerable GetStreamingResponseA // or when we reach the end of the updates stream. } + // We need to yield any remaining updates that were not yielded while looping through the streamed updates. + for (; lastYieldedUpdateIndex < updates.Count; lastYieldedUpdateIndex++) + { + var updateToYield = updates[lastYieldedUpdateIndex]; + yield return updateToYield; + Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802 + } + // If there's nothing more to do, break out of the loop and allow the handling at the // end to configure the response with aggregated data from previous requests. if (iteration >= MaximumIterationsPerRequest || diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index a0836a864c4..7c42c0edaf9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -485,6 +485,69 @@ public async Task AlreadyExecutedApprovalsAreIgnoredAsync() await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput); } + /// + /// This verifies the following scenario: + /// 1. We are streaming (also including non-streaming in the test for completeness). + /// 2. There is one function that requires approval and one that does not. + /// 3. We only get back FCC for the function that does not require approval. + /// 4. This means that once we receive this FCC, we need to buffer all updates until the end, because we might receive more FCCs and some may require approval. + /// 5. We then need to verify that we will still stream all updates once we reach the end, including the buffered FCC. + /// + [Fact] + public async Task MixedApprovalRequiredToolsWithNonApprovalRequiringFunctionCallAsync() + { + var options = new ChatOptions + { + Tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func1")), + AIFunctionFactory.Create((int i) => $"Result 2: {i}", "Func2"), + ] + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + ]; + + Func>> expectedDownstreamClientInput = () => new Queue>( + [ + new List + { + new ChatMessage(ChatRole.User, "hello"), + }, + new List + { + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]) + } + ]); + + Func>> downstreamClientOutput = () => new Queue>( + [ + new List + { + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + }, + new List + { + new ChatMessage(ChatRole.Assistant, "World again"), + } + ]); + + List output = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]), + new ChatMessage(ChatRole.Assistant, "World again"), + ]; + + await InvokeAndAssertMultiRoundAsync(options, input, downstreamClientOutput(), output, expectedDownstreamClientInput()); + + await InvokeAndAssertStreamingMultiRoundAsync(options, input, downstreamClientOutput(), output, expectedDownstreamClientInput()); + } + [Fact] public async Task ApprovalRequestWithoutApprovalResponseThrowsAsync() { @@ -781,7 +844,7 @@ async IAsyncEnumerable YieldInnerClientUpdates( } } - private static async Task> InvokeAndAssertAsync( + private static Task> InvokeAndAssertAsync( ChatOptions? options, List input, List downstreamClientOutput, @@ -789,6 +852,23 @@ private static async Task> InvokeAndAssertAsync( List? expectedDownstreamClientInput = null, Func? configurePipeline = null, AITool[]? additionalTools = null) + => InvokeAndAssertMultiRoundAsync( + options, + input, + new Queue>(new[] { downstreamClientOutput }), + expectedOutput, + expectedDownstreamClientInput is null ? null : new Queue>(new[] { expectedDownstreamClientInput }), + configurePipeline, + additionalTools); + + private static async Task> InvokeAndAssertMultiRoundAsync( + ChatOptions? options, + List input, + Queue> downstreamClientOutput, + List expectedOutput, + Queue>? expectedDownstreamClientInput = null, + Func? configurePipeline = null, + AITool[]? additionalTools = null) { Assert.NotEmpty(input); @@ -804,7 +884,7 @@ private static async Task> InvokeAndAssertAsync( Assert.Equal(cts.Token, actualCancellationToken); if (expectedDownstreamClientInput is not null) { - AssertExtensions.EqualMessageLists(expectedDownstreamClientInput, contents.ToList()); + AssertExtensions.EqualMessageLists(expectedDownstreamClientInput.Dequeue(), contents.ToList()); } await Task.Yield(); @@ -812,8 +892,9 @@ private static async Task> InvokeAndAssertAsync( var usage = CreateRandomUsage(); expectedTotalTokenCounts += usage.InputTokenCount!.Value; - downstreamClientOutput.ForEach(m => m.MessageId = Guid.NewGuid().ToString("N")); - return new ChatResponse(downstreamClientOutput) { Usage = usage }; + var output = downstreamClientOutput.Dequeue(); + output.ForEach(m => m.MessageId = Guid.NewGuid().ToString("N")); + return new ChatResponse(output) { Usage = usage }; } }; @@ -851,7 +932,7 @@ private static UsageDetails CreateRandomUsage() }; } - private static async Task> InvokeAndAssertStreamingAsync( + private static Task> InvokeAndAssertStreamingAsync( ChatOptions? options, List input, List downstreamClientOutput, @@ -859,6 +940,23 @@ private static async Task> InvokeAndAssertStreamingAsync( List? expectedDownstreamClientInput = null, Func? configurePipeline = null, AITool[]? additionalTools = null) + => InvokeAndAssertStreamingMultiRoundAsync( + options, + input, + new Queue>(new[] { downstreamClientOutput }), + expectedOutput, + expectedDownstreamClientInput is null ? null : new Queue>(new[] { expectedDownstreamClientInput }), + configurePipeline, + additionalTools); + + private static async Task> InvokeAndAssertStreamingMultiRoundAsync( + ChatOptions? options, + List input, + Queue> downstreamClientOutput, + List expectedOutput, + Queue>? expectedDownstreamClientInput = null, + Func? configurePipeline = null, + AITool[]? additionalTools = null) { Assert.NotEmpty(input); @@ -873,11 +971,12 @@ private static async Task> InvokeAndAssertStreamingAsync( Assert.Equal(cts.Token, actualCancellationToken); if (expectedDownstreamClientInput is not null) { - AssertExtensions.EqualMessageLists(expectedDownstreamClientInput, contents.ToList()); + AssertExtensions.EqualMessageLists(expectedDownstreamClientInput.Dequeue(), contents.ToList()); } - downstreamClientOutput.ForEach(m => m.MessageId = Guid.NewGuid().ToString("N")); - return YieldAsync(new ChatResponse(downstreamClientOutput).ToChatResponseUpdates()); + var output = downstreamClientOutput.Dequeue(); + output.ForEach(m => m.MessageId = Guid.NewGuid().ToString("N")); + return YieldAsync(new ChatResponse(output).ToChatResponseUpdates()); } }; From ba9f9256db44c0b8901727607f6d2138f9743782 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 9 Oct 2025 10:18:42 -0400 Subject: [PATCH 347/472] Fix serialization of [Experimental] AIContent-derived types (#6900) --- .../Contents/AIContent.cs | 4 +- .../Utilities/AIJsonUtilities.Defaults.cs | 20 ++++++++++ .../Utilities/AIJsonUtilities.cs | 12 +++--- .../Contents/AIContentTests.cs | 37 +++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index a0e240be991..8c23406cc8a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -21,7 +21,9 @@ namespace Microsoft.Extensions.AI; // These should be added in once they're no longer [Experimental]. If they're included while still // experimental, any JsonSerializerContext that includes AIContent will incur errors about using -// experimental types in its source generated files. +// experimental types in its source generated files. When [Experimental] is removed from these types, +// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions +// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. // [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")] // [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] // [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index 4b8a4fb1576..721a08418bf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -48,6 +48,16 @@ private static JsonSerializerOptions CreateDefaultOptions() Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; + // Temporary workaround: these types are [Experimental] and can't be added as [JsonDerivedType] on AIContent yet, + // or else consuming assemblies that used source generation with AIContent would implicitly reference them. + // Once they're no longer [Experimental] and added as [JsonDerivedType] on AIContent, these lines should be removed. + AddAIContentType(options, typeof(FunctionApprovalRequestContent), typeDiscriminatorId: "functionApprovalRequest", checkBuiltIn: false); + AddAIContentType(options, typeof(FunctionApprovalResponseContent), typeDiscriminatorId: "functionApprovalResponse", checkBuiltIn: false); + AddAIContentType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall", checkBuiltIn: false); + AddAIContentType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult", checkBuiltIn: false); + AddAIContentType(options, typeof(McpServerToolApprovalRequestContent), typeDiscriminatorId: "mcpServerToolApprovalRequest", checkBuiltIn: false); + AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false); + if (JsonSerializer.IsReflectionEnabledByDefault) { // If reflection-based serialization is enabled by default, use it as a fallback for all other types. @@ -116,6 +126,16 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(Embedding))] [JsonSerializable(typeof(AIContent))] [JsonSerializable(typeof(AIFunctionArguments))] + + // Temporary workaround: + // These should be added in once they're no longer [Experimental] and included via [JsonDerivedType] on AIContent. + [JsonSerializable(typeof(FunctionApprovalRequestContent))] + [JsonSerializable(typeof(FunctionApprovalResponseContent))] + [JsonSerializable(typeof(McpServerToolCallContent))] + [JsonSerializable(typeof(McpServerToolResultContent))] + [JsonSerializable(typeof(McpServerToolApprovalRequestContent))] + [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] + [EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead. private sealed partial class JsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index ab66bf61317..b69d0fb2aab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -36,7 +36,7 @@ public static void AddAIContentType(this JsonSerializerOptions options _ = Throw.IfNull(options); _ = Throw.IfNull(typeDiscriminatorId); - AddAIContentTypeCore(options, typeof(TContent), typeDiscriminatorId); + AddAIContentType(options, typeof(TContent), typeDiscriminatorId, checkBuiltIn: true); } /// @@ -56,10 +56,10 @@ public static void AddAIContentType(this JsonSerializerOptions options, Type con if (!typeof(AIContent).IsAssignableFrom(contentType)) { - Throw.ArgumentException(nameof(contentType), "The content type must derive from AIContent."); + Throw.ArgumentException(nameof(contentType), $"The content type must derive from {nameof(AIContent)}."); } - AddAIContentTypeCore(options, contentType, typeDiscriminatorId); + AddAIContentType(options, contentType, typeDiscriminatorId, checkBuiltIn: true); } /// Serializes the supplied values and computes a string hash of the resulting JSON. @@ -186,11 +186,11 @@ static void NormalizeJsonNode(JsonNode? node) } } - private static void AddAIContentTypeCore(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) + private static void AddAIContentType(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId, bool checkBuiltIn) { - if (contentType.Assembly == typeof(AIContent).Assembly) + if (checkBuiltIn && (contentType.Assembly == typeof(AIContent).Assembly)) { - Throw.ArgumentException(nameof(contentType), "Cannot register built-in AI content types."); + Throw.ArgumentException(nameof(contentType), $"Cannot register built-in {nameof(AIContent)} types."); } IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index 64a20fc5e4a..e5734ccd7cf 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Text.Json; using Xunit; @@ -53,4 +54,40 @@ public void Serialization_Roundtrips() Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); } + + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + ChatMessage message = new(ChatRole.User, + [ + new TextContent("a"), + new TextReasoningContent("reasoning text"), + new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream"), + new UriContent("http://example.com", "application/json"), + new ErrorContent("error message"), + new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } }), + new FunctionResultContent("call123", "result data"), + new HostedFileContent("file123"), + new HostedVectorStoreContent("vectorStore123"), + new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20, TotalTokenCount = 30 }), + new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new FunctionApprovalResponseContent("request123", approved: true, new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new McpServerToolCallContent("call123", "myTool", "myServer"), + new McpServerToolResultContent("call123"), + new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new McpServerToolApprovalResponseContent("request123", approved: true) + ]); + + var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); + ChatMessage? deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + + Assert.Equal(message.Role, deserialized.Role); + Assert.Equal(message.Contents.Count, deserialized.Contents.Count); + for (int i = 0; i < message.Contents.Count; i++) + { + Assert.NotNull(message.Contents[i]); + Assert.Equal(message.Contents[i].GetType(), deserialized.Contents[i].GetType()); + } + } } From 62b701241fad3c5bad702cf52f5e1db8dbcd5029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire?= Date: Fri, 10 Oct 2025 13:59:40 +0200 Subject: [PATCH 348/472] Give FunctionInvokingChatClient span a more OTELy name (#6911) --- .../ChatCompletion/FunctionInvokingChatClient.cs | 4 ++-- .../Microsoft.Extensions.AI/OpenTelemetryConsts.cs | 1 + .../ChatCompletion/FunctionInvokingChatClientTests.cs | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index febf0a1336e..ff71e9f83ed 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -269,7 +269,7 @@ public override async Task GetResponseAsync( // A single request into this GetResponseAsync may result in multiple requests to the inner client. // Create an activity to group them together for better observability. - using Activity? activity = _activitySource?.StartActivity($"{nameof(FunctionInvokingChatClient)}.{nameof(GetResponseAsync)}"); + using Activity? activity = _activitySource?.StartActivity(OpenTelemetryConsts.GenAI.OrchestrateToolsName); // Copy the original messages in order to avoid enumerating the original messages multiple times. // The IEnumerable can represent an arbitrary amount of work. @@ -408,7 +408,7 @@ public override async IAsyncEnumerable GetStreamingResponseA // A single request into this GetStreamingResponseAsync may result in multiple requests to the inner client. // Create an activity to group them together for better observability. - using Activity? activity = _activitySource?.StartActivity($"{nameof(FunctionInvokingChatClient)}.{nameof(GetStreamingResponseAsync)}"); + using Activity? activity = _activitySource?.StartActivity(OpenTelemetryConsts.GenAI.OrchestrateToolsName); UsageDetails? totalUsage = activity is { IsAllDataRequested: true } ? new() : null; // tracked usage across all turns, to be used for activity purposes // Copy the original messages in order to avoid enumerating the original messages multiple times. diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index c9b4a1ded1c..3def478d0e1 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -35,6 +35,7 @@ public static class GenAI public const string ChatName = "chat"; public const string EmbeddingsName = "embeddings"; public const string ExecuteToolName = "execute_tool"; + public const string OrchestrateToolsName = "orchestrate_tools"; // Non-standard public const string GenerateContentName = "generate_content"; public const string SystemInstructions = "gen_ai.system_instructions"; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 7a3c09db438..27c0d3ff0d6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -678,11 +678,11 @@ public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry, bo Func configure = b => b.Use(c => new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: sourceName) { EnableSensitiveData = enableSensitiveData })); - await InvokeAsync(() => InvokeAndAssertAsync(options, plan, configurePipeline: configure), streaming: false); + await InvokeAsync(() => InvokeAndAssertAsync(options, plan, configurePipeline: configure)); - await InvokeAsync(() => InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure), streaming: true); + await InvokeAsync(() => InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure)); - async Task InvokeAsync(Func work, bool streaming) + async Task InvokeAsync(Func work) { var activities = new List(); using TracerProvider? tracerProvider = enableTelemetry ? @@ -700,7 +700,7 @@ async Task InvokeAsync(Func work, bool streaming) activity => Assert.Equal("chat", activity.DisplayName), activity => Assert.Equal("execute_tool Func1", activity.DisplayName), activity => Assert.Equal("chat", activity.DisplayName), - activity => Assert.Equal(streaming ? "FunctionInvokingChatClient.GetStreamingResponseAsync" : "FunctionInvokingChatClient.GetResponseAsync", activity.DisplayName)); + activity => Assert.Equal("orchestrate_tools", activity.DisplayName)); var executeTool = activities[1]; if (enableSensitiveData) From e50ef7cbf3c7607b1667032cbd4a5752f97b6242 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:40:04 -0700 Subject: [PATCH 349/472] Update repository branding from 9.10 to 10.0 (#6907) * Initial plan * Update repository branding from 9.10 to 10.0 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Update src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.json --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> Co-authored-by: Jose Perez Rodriguez --- eng/Versions.props | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.Basic.verified/aichatweb/aichatweb.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 4 ++-- .../aichatweb.ServiceDefaults.csproj | 2 +- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 2 +- .../aichatweb/aichatweb.csproj | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 22d61962791..7b6ad58b5b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ - 9 - 10 + 10 + 0 0 preview 1 diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index fb2e9b9a23a..3269c3844b0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 619003eb225..1b35668bbdc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 3f313c097c6..f24a068b764 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index fb2e9b9a23a..3269c3844b0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 16f0b1a2e8f..e05bb937db8 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index fb2e9b9a23a..3269c3844b0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 03fba5231ac..59a80737b7c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 383bfa54682..90584f34ff0 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,9 +8,9 @@ - + - + From 1bcc171b829792f8bda032013dabbb07c7b4b151 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 10 Oct 2025 19:40:34 +0000 Subject: [PATCH 350/472] Merged PR 54223: Getting the branch ready for 9.10 release Getting the branch ready for 9.10 release ---- #### AI description (iteration 1) #### PR Classification This pull request prepares the branch for the 9.10 release by updating dependency versions and adjusting build configurations. #### PR Summary The update bumps multiple dependency versions from 9.0.9 to 9.0.10, updates release stabilization properties, and refines build pipeline tasks and NuGet configurations. - **`eng/Version.Details.xml` and `eng/Versions.props`**: Dependency versions and SHA values have been updated to 9.0.10, with build properties modified to enable release stabilization and update LTS version numbers. - **`azure-pipelines.yml`**: The code coverage stage has been removed and stage dependencies adjusted. - **`NuGet.config`**: Package source mappings were removed, and internal feed sources have been appropriately disabled. - **`eng/pipelines/templates/BuildAndTest.yml` & `Directory.Build.props`**: New tasks were added to set up private feed credentials, integration test steps were commented out due to authentication requirements, and NU1507 warnings have been suppressed. --- Directory.Build.props | 5 + NuGet.config | 34 ++-- azure-pipelines.yml | 46 ------ eng/Version.Details.xml | 200 +++++++++++------------ eng/Versions.props | 132 +++++++-------- eng/pipelines/templates/BuildAndTest.yml | 32 +++- 6 files changed, 208 insertions(+), 241 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0af806af628..0c0fcf22bfd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,11 @@ $(NetCoreTargetFrameworks) + + + $(NoWarn);NU1507 + + false latest diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..0aaac621ef0 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,16 @@ + + + + + + @@ -18,35 +24,19 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ec5e3d1cdb..0052dc9f706 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -239,51 +239,6 @@ extends: isWindows: false warnAsError: 0 - # ---------------------------------------------------------------- - # This stage performs quality gates enforcements - # ---------------------------------------------------------------- - - stage: codecoverage - displayName: CodeCoverage - dependsOn: - - build - condition: and(succeeded('build'), ne(variables['SkipQualityGates'], 'true')) - variables: - - template: /eng/common/templates-official/variables/pool-providers.yml@self - jobs: - - template: /eng/common/templates-official/jobs/jobs.yml@self - parameters: - enableMicrobuild: true - enableTelemetry: true - runAsPublic: ${{ variables['runAsPublic'] }} - workspace: - clean: all - - # ---------------------------------------------------------------- - # This stage downloads the code coverage reports from the build jobs, - # merges those and validates the combined test coverage. - # ---------------------------------------------------------------- - jobs: - - job: CodeCoverageReport - timeoutInMinutes: 180 - - pool: - name: NetCore1ESPool-Internal - image: 1es-mariner-2 - os: linux - - preSteps: - - checkout: self - clean: true - persistCredentials: true - fetchDepth: 1 - - steps: - - script: $(Build.SourcesDirectory)/build.sh --ci --restore - displayName: Init toolset - - - template: /eng/pipelines/templates/VerifyCoverageReport.yml - - # ---------------------------------------------------------------- # This stage only performs a build treating warnings as errors # to detect any kind of code style violations @@ -339,7 +294,6 @@ extends: parameters: validateDependsOn: - build - - codecoverage - correctness publishingInfraVersion: 3 enableSymbolValidation: false diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3b5a0c53b49..de48574ba80 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,208 +1,208 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 893c2ebbd49952ca49e93298148af2d95a61a0a4 + e1f19886fe3354963a4a790c896b3f99689fd7a5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - ff66c263be7ed395794bdaf616322977b8ec897c + 5bae930797f60d2d04f3b1df6a33eaca85fc5f28 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 78871c83aac6c38eb5476c2f34aae98ef65314f5 + 5452ff90a79084afd23df379388ae8bca24284f3 diff --git a/eng/Versions.props b/eng/Versions.props index 22d61962791..770b745929b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -10,14 +10,14 @@ - false + true - + release true @@ -33,58 +33,58 @@ --> - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 - 9.0.9 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 + 9.0.10 - 9.0.9 + 9.0.10 10.0.0-beta.25476.2 @@ -111,8 +111,8 @@ 8.0.0 8.0.2 8.0.0 - 8.0.20 - 8.0.20 + 8.0.21 + 8.0.21 8.0.0 8.0.1 8.0.1 @@ -125,22 +125,22 @@ 8.0.1 8.0.2 8.0.0 - 8.0.0 + 8.0.1 8.0.6 8.0.0 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 - 8.0.20 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 + 8.0.21 - 8.0.20 + 8.0.21 true - 9.8.0 + 9.8.0 diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index b3a9f892419..38b1b29b32e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -4,13 +4,21 @@ $(TargetFrameworks);netstandard2.0 true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. - $(DefaultDotnetIconFullPath) + Open Microsoft.Extensions.ServiceDiscovery $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 enable + + ServiceDiscovery + normal + 9.5.1 + 75 + 75 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.json b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 890f8daab3e..6424c4b5c5e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,13 +4,21 @@ $(NetCoreTargetFrameworks) true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. - $(DefaultDotnetIconFullPath) + Open $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1515;SA1600;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false + + ServiceDiscovery + normal + 9.5.1 + 75 + 75 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.json b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index e990866bd16..2b4530077cd 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -6,12 +6,20 @@ enable true Provides extensions for service discovery for the YARP reverse proxy. - $(DefaultDotnetIconFullPath) + Open $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S2692;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1414;SA1515;SA1600;SA1615;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable + + ServiceDiscovery + normal + 9.5.1 + 75 + 75 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.json b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 8ff69baf576..d7380a32842 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,13 +4,21 @@ $(TargetFrameworks);netstandard2.0 true Provides extensions to HttpClient that enable service discovery based on configuration. - $(DefaultDotnetIconFullPath) + Open $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false + + ServiceDiscovery + normal + 9.5.1 + 75 + 75 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.json b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj index c291dff12c8..7a1e69033f9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -5,6 +5,7 @@ enable enable Exe + Open $(NoWarn);IDE0040;IDE0061;IDE1006;S5034;SA1400;VSTHRD002 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 4911d854007..d298b2c4699 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + Open $(NoWarn);IDE0004;IDE0017;IDE0040;IDE0055;IDE1006;CA1012;CA1031;CA1063;CA1816;CA2000;S103;S107;S1067;S1121;S1128;S1135;S1144;S1186;S2148;S3442;S3459;S4136;SA1106;SA1127;SA1204;SA1208;SA1210;SA1128;SA1316;SA1400;SA1402;SA1407;SA1414;SA1500;SA1513;SA1515;VSTHRD003 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 6a39ff1b9af..a589eccc256 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -3,6 +3,7 @@ enable enable + Open $(NoWarn);IDE0004;IDE0040;IDE0055;IDE1006;CA2000;S1121;S1128;SA1316;SA1500;SA1513 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 00211519268..714fe71f0ee 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + Open $(NoWarn);CA2000;S103;S1144;S3459;S4136;SA1208;SA1210;VSTHRD003 From 01a9b3225cae33616d171e2460c6c36faff019dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 15 Oct 2025 15:40:28 -0500 Subject: [PATCH 366/472] Add back Uri ctor to HostedMcpServerTool (#6926) --- src/Libraries/.editorconfig | 2 +- .../Contents/UriContent.cs | 2 +- .../Tools/HostedMcpServerTool.cs | 25 +++++++++++++++++++ .../Tools/HostedMcpServerToolTests.cs | 15 +++++++---- .../OpenAIResponseClientIntegrationTests.cs | 6 ++--- .../OpenAIResponseClientTests.cs | 6 ++--- 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/Libraries/.editorconfig b/src/Libraries/.editorconfig index 90ede887742..e6feaee1f0f 100644 --- a/src/Libraries/.editorconfig +++ b/src/Libraries/.editorconfig @@ -4533,7 +4533,7 @@ dotnet_diagnostic.S3994.severity = none # Title : URI return values should not be strings # Category : Major Code Smell # Help Link: https://rules.sonarsource.com/csharp/RSPEC-3995 -dotnet_diagnostic.S3995.severity = warning +dotnet_diagnostic.S3995.severity = suggestion # Title : URI properties should not be strings # Category : Major Code Smell diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UriContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UriContent.cs index 7beaa40efdf..73ed7f63aa7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UriContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UriContent.cs @@ -30,7 +30,7 @@ public class UriContent : AIContent /// is . /// is . /// is an invalid media type. - /// is an invalid URL. + /// is an invalid URL. /// /// A media type must be specified, so that consumers know what to do with the content. /// If an exact media type is not known, but the category (e.g. image) is known, a wildcard diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 7bf7c5ae731..aa33a581710 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -27,6 +27,31 @@ public HostedMcpServerTool(string serverName, string serverAddress) ServerAddress = Throw.IfNullOrWhitespace(serverAddress); } + /// + /// Initializes a new instance of the class. + /// + /// The name of the remote MCP server. + /// The URL of the remote MCP server. + /// or is . + /// is empty or composed entirely of whitespace. + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverUrl) + : this(serverName, ValidateUrl(serverUrl)) + { + } + + private static string ValidateUrl(Uri serverUrl) + { + _ = Throw.IfNull(serverUrl); + + if (!serverUrl.IsAbsoluteUri) + { + Throw.ArgumentException(nameof(serverUrl), "The provided URL is not absolute."); + } + + return serverUrl.AbsoluteUri; + } + /// public override string Name => "mcp"; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index fe826a26820..ec1dc407973 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -12,7 +12,7 @@ public class HostedMcpServerToolTests [Fact] public void Constructor_PropsDefault() { - HostedMcpServerTool tool = new("serverName", "https://localhost/"); + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")); Assert.Empty(tool.AdditionalProperties); @@ -70,9 +70,14 @@ public void Constructor_Roundtrips() [Fact] public void Constructor_Throws() { - Assert.Throws(() => new HostedMcpServerTool(string.Empty, "https://localhost/")); - Assert.Throws(() => new HostedMcpServerTool(null!, "https://localhost/")); - Assert.Throws(() => new HostedMcpServerTool("name", string.Empty)); - Assert.Throws(() => new HostedMcpServerTool("name", null!)); + Assert.Throws("serverName", () => new HostedMcpServerTool(string.Empty, "https://localhost/")); + Assert.Throws("serverName", () => new HostedMcpServerTool(string.Empty, new Uri("https://localhost/"))); + Assert.Throws("serverName", () => new HostedMcpServerTool(null!, "https://localhost/")); + Assert.Throws("serverName", () => new HostedMcpServerTool(null!, new Uri("https://localhost/"))); + + Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", string.Empty)); + Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", new Uri("/api/mcp", UriKind.Relative))); + Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", (string)null!)); + Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", (Uri)null!)); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 07e0cd94201..cd3993a521a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -74,7 +74,7 @@ public async Task RemoteMCP_ListTools() ChatOptions chatOptions = new() { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire }], + Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire }], }; ChatResponse response = await CreateChatClient()!.GetResponseAsync("Which tools are available on the wiki_tools MCP server?", chatOptions); @@ -96,7 +96,7 @@ async Task RunAsync(bool streaming, bool requireSpecific) { ChatOptions chatOptions = new() { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = requireSpecific ? HostedMcpServerToolApprovalMode.RequireSpecific(null, ["read_wiki_structure", "ask_question"]) : @@ -136,7 +136,7 @@ async Task RunAsync(bool streaming, bool requireSpecific, bool useConversationId { ChatOptions chatOptions = new() { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = requireSpecific ? HostedMcpServerToolApprovalMode.RequireSpecific(["read_wiki_structure", "ask_question"], null) : diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index fce0bab3ee5..15015e3b15c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -929,7 +929,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var chatOptions = new ChatOptions { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")] + Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))] }; McpServerToolApprovalRequestContent approvalRequest; @@ -1307,7 +1307,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) AITool mcpTool = rawTool ? ResponseTool.CreateMcpTool("deepwiki", serverUri: new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() : - new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, }; @@ -1723,7 +1723,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() ChatOptions chatOptions = new() { - Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, } From 417bdfa6dee9a31529d89a77dd4b6c0682624b0b Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 16 Oct 2025 13:00:37 -0500 Subject: [PATCH 367/472] Set DisableNETStandardCompatErrors (#6927) Without setting this property, the NuGet pacakges will raise an error saying the libraries don't support net462. These libraries work correctly on .NET Framework and the unit tests are passing. So disable the NuGet MSBuild error. --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 38b1b29b32e..b96d5ff7137 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -5,6 +5,7 @@ true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. Open + true Microsoft.Extensions.ServiceDiscovery $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index d7380a32842..51631d328c0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -5,6 +5,7 @@ true Provides extensions to HttpClient that enable service discovery based on configuration. Open + true $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable From 712e75b0f2ab39ce660955770cb22996df09a394 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:42:41 -0700 Subject: [PATCH 368/472] Update Package validation baseline version to 9.10.0 (#6922) * Initial plan * Update PackageValidationBaselineVersion to 9.10.0 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Adding Compatibility Suppressions file * Fix suppressions file to remove unnecessary items --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> Co-authored-by: Jose Perez Rodriguez --- eng/MSBuild/Packaging.targets | 2 +- .../CompatibilitySuppressions.xml | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml diff --git a/eng/MSBuild/Packaging.targets b/eng/MSBuild/Packaging.targets index bde796efff6..0e1a385d685 100644 --- a/eng/MSBuild/Packaging.targets +++ b/eng/MSBuild/Packaging.targets @@ -37,7 +37,7 @@ true - 9.8.0 + 9.10.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..8488d3969c4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,88 @@ + + + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + \ No newline at end of file From bde3078de062b1db8fdf658752aa2105980b6b8f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:44:09 +0000 Subject: [PATCH 369/472] [main] Update dependencies from dotnet/arcade (#6802) [main] Update dependencies from dotnet/arcade - Merge branch 'main' into darc-main-be87ffda-cd9f-4066-ae4a-f3f3c1eeada8 --- eng/Version.Details.xml | 12 +- eng/Versions.props | 2 +- eng/common/CIBuild.cmd | 2 +- eng/common/SetupNugetSources.ps1 | 2 +- eng/common/SetupNugetSources.sh | 2 +- eng/common/build.ps1 | 11 +- eng/common/build.sh | 33 +- eng/common/cibuild.sh | 2 +- eng/common/core-templates/job/job.yml | 48 ++- eng/common/core-templates/job/onelocbuild.yml | 35 +- .../job/publish-build-assets.yml | 66 +--- .../core-templates/job/source-build.yml | 9 +- .../job/source-index-stage1.yml | 47 ++- .../core-templates/jobs/codeql-build.yml | 1 + eng/common/core-templates/jobs/jobs.yml | 15 +- .../core-templates/jobs/source-build.yml | 23 +- .../core-templates/post-build/post-build.yml | 9 +- .../steps/cleanup-microbuild.yml | 28 -- .../core-templates/steps/generate-sbom.yml | 2 +- .../steps/get-delegation-sas.yml | 11 +- .../steps/install-microbuild.yml | 91 ----- .../core-templates/steps/publish-logs.yml | 3 - .../core-templates/steps/source-build.yml | 88 ++++- .../steps/source-index-stage1-publish.yml | 35 -- eng/common/cross/arm64/tizen/tizen.patch | 2 +- eng/common/cross/armel/armel.jessie.patch | 43 +++ eng/common/cross/build-android-rootfs.sh | 49 +-- eng/common/cross/build-rootfs.sh | 237 +++++-------- eng/common/cross/install-debs.py | 334 ------------------ eng/common/cross/tizen-fetch.sh | 9 +- eng/common/cross/toolchain.cmake | 82 +++-- eng/common/darc-init.sh | 2 +- eng/common/dotnet.cmd | 7 - eng/common/dotnet.ps1 | 11 - eng/common/dotnet.sh | 26 -- eng/common/generate-locproject.ps1 | 49 +-- eng/common/native/install-dependencies.sh | 62 ---- eng/common/post-build/publish-using-darc.ps1 | 7 +- eng/common/sdk-task.ps1 | 12 +- eng/common/sdk-task.sh | 121 ------- eng/common/sdl/packages.config | 2 +- eng/common/templates-official/job/job.yml | 4 +- .../steps/publish-build-artifacts.yml | 7 +- .../steps/source-index-stage1-publish.yml | 7 - eng/common/templates/job/job.yml | 4 +- .../steps/publish-build-artifacts.yml | 8 +- .../steps/source-index-stage1-publish.yml | 7 - eng/common/templates/steps/vmr-sync.yml | 207 ----------- eng/common/templates/vmr-build-pr.yml | 42 --- eng/common/tools.ps1 | 66 ++-- eng/common/tools.sh | 73 ++-- eng/common/vmr-sync.ps1 | 138 -------- eng/common/vmr-sync.sh | 207 ----------- global.json | 4 +- 54 files changed, 561 insertions(+), 1845 deletions(-) delete mode 100644 eng/common/core-templates/steps/cleanup-microbuild.yml delete mode 100644 eng/common/core-templates/steps/install-microbuild.yml delete mode 100644 eng/common/core-templates/steps/source-index-stage1-publish.yml create mode 100644 eng/common/cross/armel/armel.jessie.patch delete mode 100644 eng/common/cross/install-debs.py delete mode 100644 eng/common/dotnet.cmd delete mode 100644 eng/common/dotnet.ps1 delete mode 100644 eng/common/dotnet.sh delete mode 100644 eng/common/native/install-dependencies.sh delete mode 100644 eng/common/sdk-task.sh delete mode 100644 eng/common/templates-official/steps/source-index-stage1-publish.yml delete mode 100644 eng/common/templates/steps/source-index-stage1-publish.yml delete mode 100644 eng/common/templates/steps/vmr-sync.yml delete mode 100644 eng/common/templates/vmr-build-pr.yml delete mode 100644 eng/common/vmr-sync.ps1 delete mode 100644 eng/common/vmr-sync.sh diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index de48574ba80..2219a84d8da 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -206,17 +206,17 @@ - + https://github.com/dotnet/arcade - a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 + 6666973b629b24e259162dba03486c23af464bab - + https://github.com/dotnet/arcade - a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 + 6666973b629b24e259162dba03486c23af464bab - + https://github.com/dotnet/arcade - a4c9a07d978c070ef5c19d2ec9f811d6a5b20914 + 6666973b629b24e259162dba03486c23af464bab diff --git a/eng/Versions.props b/eng/Versions.props index 5298824711c..a06462fbcbf 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -86,7 +86,7 @@ 9.0.10 - 10.0.0-beta.25476.2 + 9.0.0-beta.25515.2 diff --git a/eng/common/CIBuild.cmd b/eng/common/CIBuild.cmd index ac1f72bf94e..56c2f25ac22 100644 --- a/eng/common/CIBuild.cmd +++ b/eng/common/CIBuild.cmd @@ -1,2 +1,2 @@ @echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" \ No newline at end of file diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index 9445c314325..792b60b49d4 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -157,7 +157,7 @@ if ($dotnet31Source -ne $null) { AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -pwd $Password } -$dotnetVersions = @('5','6','7','8','9','10') +$dotnetVersions = @('5','6','7','8','9') foreach ($dotnetVersion in $dotnetVersions) { $feedPrefix = "dotnet" + $dotnetVersion; diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index ddf4efc81a4..facb415ca6f 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -99,7 +99,7 @@ if [ "$?" == "0" ]; then PackageSources+=('dotnet3.1-internal-transport') fi -DotNetVersions=('5' '6' '7' '8' '9' '10') +DotNetVersions=('5' '6' '7' '8' '9') for DotNetVersion in ${DotNetVersions[@]} ; do FeedPrefix="dotnet${DotNetVersion}"; diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index 8cfee107e7a..438f9920c43 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -7,7 +7,6 @@ Param( [string] $msbuildEngine = $null, [bool] $warnAsError = $true, [bool] $nodeReuse = $true, - [switch] $buildCheck = $false, [switch][Alias('r')]$restore, [switch] $deployDeps, [switch][Alias('b')]$build, @@ -21,7 +20,6 @@ Param( [switch] $publish, [switch] $clean, [switch][Alias('pb')]$productBuild, - [switch]$fromVMR, [switch][Alias('bl')]$binaryLog, [switch][Alias('nobl')]$excludeCIBinarylog, [switch] $ci, @@ -73,9 +71,6 @@ function Print-Usage() { Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)" - Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" - Write-Host " -buildCheck Sets /check msbuild parameter" - Write-Host " -fromVMR Set when building from within the VMR" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." @@ -102,7 +97,6 @@ function Build { $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'Build.binlog') } else { '' } $platformArg = if ($platform) { "/p:Platform=$platform" } else { '' } - $check = if ($buildCheck) { '/check' } else { '' } if ($projects) { # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. @@ -119,7 +113,6 @@ function Build { MSBuild $toolsetBuildProj ` $bl ` $platformArg ` - $check ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` /p:Restore=$restore ` @@ -129,13 +122,11 @@ function Build { /p:Deploy=$deploy ` /p:Test=$test ` /p:Pack=$pack ` - /p:DotNetBuild=$productBuild ` - /p:DotNetBuildFromVMR=$fromVMR ` + /p:DotNetBuildRepo=$productBuild ` /p:IntegrationTest=$integrationTest ` /p:PerformanceTest=$performanceTest ` /p:Sign=$sign ` /p:Publish=$publish ` - /p:RestoreStaticGraphEnableBinaryLogger=$binaryLog ` @properties } diff --git a/eng/common/build.sh b/eng/common/build.sh index 9767bb411a4..ac1ee8620cd 100755 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -42,8 +42,6 @@ usage() echo " --prepareMachine Prepare machine for CI run, clean up processes after build" echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" - echo " --buildCheck Sets /check msbuild parameter" - echo " --fromVMR Set when building from within the VMR" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." @@ -65,7 +63,6 @@ restore=false build=false source_build=false product_build=false -from_vmr=false rebuild=false test=false integration_test=false @@ -79,7 +76,6 @@ clean=false warn_as_error=true node_reuse=true -build_check=false binary_log=false exclude_ci_binary_log=false pipelines_log=false @@ -91,7 +87,7 @@ verbosity='minimal' runtime_source_feed='' runtime_source_feed_key='' -properties=() +properties='' while [[ $# > 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in @@ -131,22 +127,19 @@ while [[ $# > 0 ]]; do -pack) pack=true ;; - -sourcebuild|-source-build|-sb) + -sourcebuild|-sb) build=true source_build=true product_build=true restore=true pack=true ;; - -productbuild|-product-build|-pb) + -productBuild|-pb) build=true product_build=true restore=true pack=true ;; - -fromvmr|-from-vmr) - from_vmr=true - ;; -test|-t) test=true ;; @@ -180,9 +173,6 @@ while [[ $# > 0 ]]; do node_reuse=$2 shift ;; - -buildcheck) - build_check=true - ;; -runtimesourcefeed) runtime_source_feed=$2 shift @@ -192,7 +182,7 @@ while [[ $# > 0 ]]; do shift ;; *) - properties+=("$1") + properties="$properties $1" ;; esac @@ -226,7 +216,7 @@ function Build { InitializeCustomToolset if [[ ! -z "$projects" ]]; then - properties+=("/p:Projects=$projects") + properties="$properties /p:Projects=$projects" fi local bl="" @@ -234,21 +224,15 @@ function Build { bl="/bl:\"$log_dir/Build.binlog\"" fi - local check="" - if [[ "$build_check" == true ]]; then - check="/check" - fi - MSBuild $_InitializeToolset \ $bl \ - $check \ /p:Configuration=$configuration \ /p:RepoRoot="$repo_root" \ /p:Restore=$restore \ /p:Build=$build \ - /p:DotNetBuild=$product_build \ + /p:DotNetBuildRepo=$product_build \ + /p:ArcadeBuildFromSource=$source_build \ /p:DotNetBuildSourceOnly=$source_build \ - /p:DotNetBuildFromVMR=$from_vmr \ /p:Rebuild=$rebuild \ /p:Test=$test \ /p:Pack=$pack \ @@ -256,8 +240,7 @@ function Build { /p:PerformanceTest=$performance_test \ /p:Sign=$sign \ /p:Publish=$publish \ - /p:RestoreStaticGraphEnableBinaryLogger=$binary_log \ - ${properties[@]+"${properties[@]}"} + $properties ExitWithExitCode 0 } diff --git a/eng/common/cibuild.sh b/eng/common/cibuild.sh index 66e3b0ac61c..1a02c0dec8f 100755 --- a/eng/common/cibuild.sh +++ b/eng/common/cibuild.sh @@ -13,4 +13,4 @@ while [[ -h $source ]]; do done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" -. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ +. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ \ No newline at end of file diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index 5ce51840619..8da43d3b583 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,11 +19,11 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false - enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false enablePublishBuildAssets: false enablePublishTestResults: false + enablePublishUsingPipelines: false enableBuildRetry: false mergeTestResults: false testRunTitle: '' @@ -74,6 +74,9 @@ jobs: - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE value: '$(Build.Repository.Uri)' + - ${{ if eq(parameters.enableRichCodeNavigation, 'true') }}: + - name: EnableRichCodeNavigation + value: 'true' # Retry signature validation up to three times, waiting 2 seconds between attempts. # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY @@ -125,12 +128,23 @@ jobs: - ${{ preStep }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - template: /eng/common/core-templates/steps/install-microbuild.yml - parameters: - enableMicrobuild: ${{ parameters.enableMicrobuild }} - enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} - microbuildUseESRP: ${{ parameters.microbuildUseESRP }} + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}: - task: NuGetAuthenticate@1 @@ -146,15 +160,27 @@ jobs: - ${{ each step in parameters.steps }}: - ${{ step }} + - ${{ if eq(parameters.enableRichCodeNavigation, true) }}: + - task: RichCodeNavIndexer@0 + displayName: RichCodeNav Upload + inputs: + languages: ${{ coalesce(parameters.richCodeNavigationLanguage, 'csharp') }} + environment: ${{ coalesce(parameters.richCodeNavigationEnvironment, 'internal') }} + richNavLogOutputDirectory: $(System.DefaultWorkingDirectory)/artifacts/bin + uploadRichNavArtifacts: ${{ coalesce(parameters.richCodeNavigationUploadArtifacts, false) }} + continueOnError: true + - ${{ each step in parameters.componentGovernanceSteps }}: - ${{ step }} - - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - template: /eng/common/core-templates/steps/cleanup-microbuild.yml - parameters: - enableMicrobuild: ${{ parameters.enableMicrobuild }} - enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) # Publish test results - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}: diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index c5788829a87..edefa789d36 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -4,7 +4,7 @@ parameters: # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool pool: '' - + CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) @@ -27,7 +27,7 @@ parameters: is1ESPipeline: '' jobs: - job: OneLocBuild${{ parameters.JobNameSuffix }} - + dependsOn: ${{ parameters.dependsOn }} displayName: OneLocBuild${{ parameters.JobNameSuffix }} @@ -86,7 +86,8 @@ jobs: isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} ${{ if eq(parameters.CreatePr, true) }}: isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} - isShouldReusePrSelected: ${{ parameters.ReusePr }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: @@ -99,20 +100,22 @@ jobs: mirrorBranch: ${{ parameters.MirrorBranch }} condition: ${{ parameters.condition }} - # Copy the locProject.json to the root of the Loc directory, then publish a pipeline artifact - - task: CopyFiles@2 - displayName: Copy LocProject.json - inputs: - SourceFolder: '$(System.DefaultWorkingDirectory)/eng/Localize/' - Contents: 'LocProject.json' - TargetFolder: '$(Build.ArtifactStagingDirectory)/loc' - condition: ${{ parameters.condition }} - - - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml + - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: - targetPath: '$(Build.ArtifactStagingDirectory)/loc' - artifactName: 'Loc' - displayName: 'Publish Localization Files' + displayName: Publish Localization Files + pathToPublish: '$(Build.ArtifactStagingDirectory)/loc' + publishLocation: Container + artifactName: Loc condition: ${{ parameters.condition }} + + - template: /eng/common/core-templates/steps/publish-build-artifacts.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} + args: + displayName: Publish LocProject.json + pathToPublish: '$(System.DefaultWorkingDirectory)/eng/Localize/' + publishLocation: Container + artifactName: Loc + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 37dff559fc1..a58c8a418e8 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -20,6 +20,9 @@ parameters: # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. runAsPublic: false + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishUsingPipelines: false + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing publishAssetsImmediately: false @@ -29,15 +32,6 @@ parameters: is1ESPipeline: '' - # Optional: 🌤️ or not the build has assets it wants to publish to BAR - isAssetlessBuild: false - - # Optional, publishing version - publishingVersion: 3 - - # Optional: A minimatch pattern for the asset manifests to publish to BAR - assetManifestsPattern: '*/manifests/**/*.xml' - repositoryAlias: self officialBuildId: '' @@ -90,33 +84,15 @@ jobs: - checkout: ${{ parameters.repositoryAlias }} fetchDepth: 3 clean: true - - - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: - - ${{ if eq(parameters.publishingVersion, 3) }}: - - task: DownloadPipelineArtifact@2 - displayName: Download Asset Manifests - inputs: - artifactName: AssetManifests - targetPath: '$(Build.StagingDirectory)/AssetManifests' - condition: ${{ parameters.condition }} - continueOnError: ${{ parameters.continueOnError }} - - ${{ if eq(parameters.publishingVersion, 4) }}: - - task: DownloadPipelineArtifact@2 - displayName: Download V4 asset manifests - inputs: - itemPattern: '*/manifests/**/*.xml' - targetPath: '$(Build.StagingDirectory)/AllAssetManifests' - condition: ${{ parameters.condition }} - continueOnError: ${{ parameters.continueOnError }} - - task: CopyFiles@2 - displayName: Copy V4 asset manifests to AssetManifests - inputs: - SourceFolder: '$(Build.StagingDirectory)/AllAssetManifests' - Contents: ${{ parameters.assetManifestsPattern }} - TargetFolder: '$(Build.StagingDirectory)/AssetManifests' - flattenFolders: true - condition: ${{ parameters.condition }} - continueOnError: ${{ parameters.continueOnError }} + + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + checkDownloadedFiles: true + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} - task: NuGetAuthenticate@1 @@ -128,9 +104,9 @@ jobs: scriptLocation: scriptPath scriptPath: $(System.DefaultWorkingDirectory)/eng/common/sdk-task.ps1 arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet - /p:ManifestsPath='$(Build.StagingDirectory)/AssetManifests' - /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' /p:MaestroApiEndpoint=https://maestro.dot.net + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} /p:OfficialBuildId=$(OfficialBuildId) condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} @@ -153,17 +129,6 @@ jobs: Copy-Item -Path $symbolExclusionfile -Destination "$(Build.StagingDirectory)/ReleaseConfigs" } - - ${{ if eq(parameters.publishingVersion, 4) }}: - - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} - args: - targetPath: '$(Build.ArtifactStagingDirectory)/MergedManifest.xml' - artifactName: AssetManifests - displayName: 'Publish Merged Manifest' - retryCountOnTaskFailure: 10 # for any logs being locked - sbomEnabled: false # we don't need SBOM for logs - - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} @@ -173,7 +138,7 @@ jobs: publishLocation: Container artifactName: ReleaseConfigs - - ${{ if or(eq(parameters.publishAssetsImmediately, 'true'), eq(parameters.isAssetlessBuild, 'true')) }}: + - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} @@ -194,7 +159,6 @@ jobs: -WaitPublishingFinish true -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' - -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - template: /eng/common/core-templates/steps/publish-logs.yml diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index d805d5faeb9..5baedac1e03 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -12,10 +12,9 @@ parameters: # The name of the job. This is included in the job ID. # targetRID: '' # The name of the target RID to use, instead of the one auto-detected by Arcade. - # portableBuild: false + # nonPortable: false # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than - # linux-x64), and compiling against distro-provided packages rather than portable ones. The - # default is portable mode. + # linux-x64), and compiling against distro-provided packages rather than portable ones. # skipPublishValidation: false # Disables publishing validation. By default, a check is performed to ensure no packages are # published by source-build. @@ -34,6 +33,9 @@ parameters: # container and pool. platform: {} + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: '' # If set to true and running on a non-public project, @@ -94,3 +96,4 @@ jobs: parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} platform: ${{ parameters.platform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} diff --git a/eng/common/core-templates/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml index 30530359a5d..662b9fcce15 100644 --- a/eng/common/core-templates/job/source-index-stage1.yml +++ b/eng/common/core-templates/job/source-index-stage1.yml @@ -1,5 +1,8 @@ parameters: runAsPublic: false + sourceIndexUploadPackageVersion: 2.0.0-20250425.2 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250425.2 + sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog @@ -13,6 +16,12 @@ jobs: dependsOn: ${{ parameters.dependsOn }} condition: ${{ parameters.condition }} variables: + - name: SourceIndexUploadPackageVersion + value: ${{ parameters.sourceIndexUploadPackageVersion }} + - name: SourceIndexProcessBinlogPackageVersion + value: ${{ parameters.sourceIndexProcessBinlogPackageVersion }} + - name: SourceIndexPackageSource + value: ${{ parameters.sourceIndexPackageSource }} - name: BinlogPath value: ${{ parameters.binlogPath }} - template: /eng/common/core-templates/variables/pool-providers.yml @@ -25,10 +34,12 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $(DncEngPublicBuildPool) - image: windows.vs2022.amd64.open + image: 1es-windows-2022-open + os: windows ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $(DncEngInternalBuildPool) - image: windows.vs2022.amd64 + image: 1es-windows-2022 + os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -36,9 +47,35 @@ jobs: - ${{ each preStep in parameters.preSteps }}: - ${{ preStep }} + + - task: UseDotNet@2 + displayName: Use .NET 8 SDK + inputs: + packageType: sdk + version: 8.0.x + installationPath: $(Agent.TempDirectory)/dotnet + workingDirectory: $(Agent.TempDirectory) + + - script: | + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version $(sourceIndexProcessBinlogPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version $(sourceIndexUploadPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + displayName: Download Tools + # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. + workingDirectory: $(Agent.TempDirectory) + - script: ${{ parameters.sourceIndexBuildCommand }} displayName: Build Repository - - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml - parameters: - binLogPath: ${{ parameters.binLogPath }} \ No newline at end of file + - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output + displayName: Process Binlog into indexable sln + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: AzureCLI@2 + displayName: Log in to Azure and upload stage1 artifacts to source index + inputs: + azureSubscription: 'SourceDotNet Stage1 Publish' + addSpnToEnvironment: true + scriptType: 'ps' + scriptLocation: 'inlineScript' + inlineScript: | + $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1 diff --git a/eng/common/core-templates/jobs/codeql-build.yml b/eng/common/core-templates/jobs/codeql-build.yml index dbc14ac580a..4571a7864df 100644 --- a/eng/common/core-templates/jobs/codeql-build.yml +++ b/eng/common/core-templates/jobs/codeql-build.yml @@ -15,6 +15,7 @@ jobs: enablePublishBuildArtifacts: false enablePublishTestResults: false enablePublishBuildAssets: false + enablePublishUsingPipelines: false enableTelemetry: true variables: diff --git a/eng/common/core-templates/jobs/jobs.yml b/eng/common/core-templates/jobs/jobs.yml index 01ada747665..bf33cdc2cc7 100644 --- a/eng/common/core-templates/jobs/jobs.yml +++ b/eng/common/core-templates/jobs/jobs.yml @@ -5,6 +5,9 @@ parameters: # Optional: Include PublishBuildArtifacts task enablePublishBuildArtifacts: false + # Optional: Enable publishing using release pipelines + enablePublishUsingPipelines: false + # Optional: Enable running the source-build jobs to build repo from source enableSourceBuild: false @@ -27,9 +30,6 @@ parameters: # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage. publishAssetsImmediately: false - # Optional: 🌤️ or not the build has assets it wants to publish to BAR - isAssetlessBuild: false - # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml) artifactsPublishingAdditionalParameters: '' signingValidationAdditionalParameters: '' @@ -85,6 +85,7 @@ jobs: - template: /eng/common/core-templates/jobs/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} + allCompletedJobId: Source_Build_Complete ${{ each parameter in parameters.sourceBuildParameters }}: ${{ parameter.key }}: ${{ parameter.value }} @@ -97,7 +98,7 @@ jobs: ${{ parameter.key }}: ${{ parameter.value }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, ''), eq(parameters.isAssetlessBuild, true)) }}: + - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, '')) }}: - template: ../job/publish-build-assets.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} @@ -109,10 +110,12 @@ jobs: - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: - ${{ each job in parameters.jobs }}: - ${{ job.job }} + - ${{ if eq(parameters.enableSourceBuild, true) }}: + - Source_Build_Complete runAsPublic: ${{ parameters.runAsPublic }} - publishAssetsImmediately: ${{ or(parameters.publishAssetsImmediately, parameters.isAssetlessBuild) }} - isAssetlessBuild: ${{ parameters.isAssetlessBuild }} + publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} + publishAssetsImmediately: ${{ parameters.publishAssetsImmediately }} enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} diff --git a/eng/common/core-templates/jobs/source-build.yml b/eng/common/core-templates/jobs/source-build.yml index d92860cba20..0b408a67bd5 100644 --- a/eng/common/core-templates/jobs/source-build.yml +++ b/eng/common/core-templates/jobs/source-build.yml @@ -2,19 +2,28 @@ parameters: # This template adds arcade-powered source-build to CI. A job is created for each platform, as # well as an optional server job that completes when all platform jobs complete. + # The name of the "join" job for all source-build platforms. If set to empty string, the job is + # not included. Existing repo pipelines can use this job depend on all source-build jobs + # completing without maintaining a separate list of every single job ID: just depend on this one + # server job. By default, not included. Recommended name if used: 'Source_Build_Complete'. + allCompletedJobId: '' + # See /eng/common/core-templates/job/source-build.yml jobNamePrefix: 'Source_Build' # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-10-amd64' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream9' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, # one job runs on 'defaultManagedPlatform'. platforms: [] + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: '' # If set to true and running on a non-public project, @@ -25,12 +34,23 @@ parameters: jobs: +- ${{ if ne(parameters.allCompletedJobId, '') }}: + - job: ${{ parameters.allCompletedJobId }} + displayName: Source-Build Complete + pool: server + dependsOn: + - ${{ each platform in parameters.platforms }}: + - ${{ parameters.jobNamePrefix }}_${{ platform.name }} + - ${{ if eq(length(parameters.platforms), 0) }}: + - ${{ parameters.jobNamePrefix }}_${{ parameters.defaultManagedPlatform.name }} + - ${{ each platform in parameters.platforms }}: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ platform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} - ${{ if eq(length(parameters.platforms), 0) }}: @@ -39,4 +59,5 @@ jobs: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ parameters.defaultManagedPlatform }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} enableInternalSources: ${{ parameters.enableInternalSources }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index f6f87fe5c67..2ee8bbfff54 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -60,11 +60,6 @@ parameters: artifactNames: '' downloadArtifacts: true - - name: isAssetlessBuild - type: boolean - displayName: Is Assetless Build - default: false - # These parameters let the user customize the call to sdk-task.ps1 for publishing # symbols & general artifacts as well as for signing validation - name: symbolPublishingAdditionalParameters @@ -193,6 +188,9 @@ stages: buildId: $(AzDOBuildId) artifactName: PackageArtifacts checkDownloadedFiles: true + itemPattern: | + ** + !**/Microsoft.SourceBuild.Intermediate.*.nupkg # This is necessary whenever we want to publish/restore to an AzDO private feed # Since sdk-task.ps1 tries to restore packages we need to do this authentication here @@ -322,4 +320,3 @@ stages: -RequireDefaultChannels ${{ parameters.requireDefaultChannels }} -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' - -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' diff --git a/eng/common/core-templates/steps/cleanup-microbuild.yml b/eng/common/core-templates/steps/cleanup-microbuild.yml deleted file mode 100644 index c0fdcd3379d..00000000000 --- a/eng/common/core-templates/steps/cleanup-microbuild.yml +++ /dev/null @@ -1,28 +0,0 @@ -parameters: - # Enable cleanup tasks for MicroBuild - enableMicrobuild: false - # Enable cleanup tasks for MicroBuild on Mac and Linux - # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' - enableMicrobuildForMacAndLinux: false - continueOnError: false - -steps: - - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - - task: MicroBuildCleanup@1 - displayName: Execute Microbuild cleanup tasks - condition: and( - always(), - or( - and( - eq(variables['Agent.Os'], 'Windows_NT'), - in(variables['_SignType'], 'real', 'test') - ), - and( - ${{ eq(parameters.enableMicrobuildForMacAndLinux, true) }}, - ne(variables['Agent.Os'], 'Windows_NT'), - eq(variables['_SignType'], 'real') - ) - )) - continueOnError: ${{ parameters.continueOnError }} - env: - TeamName: $(_TeamName) diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index c05f6502797..7f5b84c4cb8 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -5,7 +5,7 @@ # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: - PackageVersion: 10.0.0 + PackageVersion: 9.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom diff --git a/eng/common/core-templates/steps/get-delegation-sas.yml b/eng/common/core-templates/steps/get-delegation-sas.yml index d2901470a7f..9db5617ea7d 100644 --- a/eng/common/core-templates/steps/get-delegation-sas.yml +++ b/eng/common/core-templates/steps/get-delegation-sas.yml @@ -31,7 +31,16 @@ steps: # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + # Temporarily work around a helix issue where SAS tokens with / in them will cause incorrect downloads + # of correlation payloads. https://github.com/dotnet/dnceng/issues/3484 + $sas = "" + do { + $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to generate SAS token." + exit 1 + } + } while($sas.IndexOf('/') -ne -1) if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml deleted file mode 100644 index d6b9878f54d..00000000000 --- a/eng/common/core-templates/steps/install-microbuild.yml +++ /dev/null @@ -1,91 +0,0 @@ -parameters: - # Enable install tasks for MicroBuild - enableMicrobuild: false - # Enable install tasks for MicroBuild on Mac and Linux - # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' - enableMicrobuildForMacAndLinux: false - # Determines whether the ESRP service connection information should be passed to the signing plugin. - # This overlaps with _SignType to some degree. We only need the service connection for real signing. - # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. - # Doing so will cause the service connection to be authorized for the pipeline, which isn't allowed and won't work for non-prod. - # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The - # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough. - microbuildUseESRP: true - # Location of the MicroBuild output folder - # NOTE: There's something that relies on this being in the "default" source directory for tasks such as Signing to work properly. - microBuildOutputFolder: '$(Build.SourcesDirectory)' - - continueOnError: false - -steps: - - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, 'true') }}: - # Needed to download the MicroBuild plugin nupkgs on Mac and Linux when nuget.exe is unavailable - - task: UseDotNet@2 - displayName: Install .NET 8.0 SDK for MicroBuild Plugin - inputs: - packageType: sdk - version: 8.0.x - installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet - workingDirectory: ${{ parameters.microBuildOutputFolder }} - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - - - script: | - REM Check if ESRP is disabled while SignType is real - if /I "${{ parameters.microbuildUseESRP }}"=="false" if /I "$(_SignType)"=="real" ( - echo Error: ESRP must be enabled when SignType is real. - exit /b 1 - ) - displayName: 'Validate ESRP usage (Windows)' - condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT')) - - script: | - # Check if ESRP is disabled while SignType is real - if [ "${{ parameters.microbuildUseESRP }}" = "false" ] && [ "$(_SignType)" = "real" ]; then - echo "Error: ESRP must be enabled when SignType is real." - exit 1 - fi - displayName: 'Validate ESRP usage (Non-Windows)' - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - - # Two different MB install steps. This is due to not being able to use the agent OS during - # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, - # we can avoid including the MB install step if not enabled at all. This avoids a bunch of - # extra pipeline authorizations, since most pipelines do not sign on non-Windows. - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin (Windows) - inputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea - ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca - env: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) - - - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin (non-Windows) - inputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 - ${{ else }}: - ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc - env: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 10f825e270a..0623ac6e112 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -34,9 +34,7 @@ steps: '$(akams-client-id)' '$(microsoft-symbol-server-pat)' '$(symweb-symbol-server-pat)' - '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' - '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} continueOnError: true condition: always() @@ -47,7 +45,6 @@ steps: SourceFolder: '$(System.DefaultWorkingDirectory)/PostBuildLogs' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/PostBuildLogs' - condition: always() - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index acf16ed3496..0718e4ba902 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -11,6 +11,10 @@ parameters: # for details. The entire object is described in the 'job' template for simplicity, even though # the usage of the properties on this object is split between the 'job' and 'steps' templates. platform: {} + + # Optional list of directories to ignore for component governance scans. + componentGovernanceIgnoreDirectories: [] + is1ESPipeline: false steps: @@ -19,12 +23,25 @@ steps: set -x df -h + # If file changes are detected, set CopyWipIntoInnerSourceBuildRepo to copy the WIP changes into the inner source build repo. + internalRestoreArgs= + if ! git diff --quiet; then + internalRestoreArgs='/p:CopyWipIntoInnerSourceBuildRepo=true' + # The 'Copy WIP' feature of source build uses git stash to apply changes from the original repo. + # This only works if there is a username/email configured, which won't be the case in most CI runs. + git config --get user.email + if [ $? -ne 0 ]; then + git config user.email dn-bot@microsoft.com + git config user.name dn-bot + fi + fi + # If building on the internal project, the internal storage variable may be available (usually only if needed) # In that case, add variables to allow the download of internal runtimes if the specified versions are not found # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release @@ -33,33 +50,88 @@ steps: buildConfig='$(_BuildConfig)' fi + officialBuildArgs= + if [ '${{ and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}' = 'True' ]; then + officialBuildArgs='/p:DotNetPublishUsingPipelines=true /p:OfficialBuildId=$(BUILD.BUILDNUMBER)' + fi + targetRidArgs= if [ '${{ parameters.platform.targetRID }}' != '' ]; then targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' fi - portableBuildArgs= - if [ '${{ parameters.platform.portableBuild }}' != '' ]; then - portableBuildArgs='/p:PortableBuild=${{ parameters.platform.portableBuild }}' + runtimeOsArgs= + if [ '${{ parameters.platform.runtimeOS }}' != '' ]; then + runtimeOsArgs='/p:RuntimeOS=${{ parameters.platform.runtimeOS }}' + fi + + baseOsArgs= + if [ '${{ parameters.platform.baseOS }}' != '' ]; then + baseOsArgs='/p:BaseOS=${{ parameters.platform.baseOS }}' + fi + + publishArgs= + if [ '${{ parameters.platform.skipPublishValidation }}' != 'true' ]; then + publishArgs='--publish' + fi + + assetManifestFileName=SourceBuild_RidSpecific.xml + if [ '${{ parameters.platform.name }}' != '' ]; then + assetManifestFileName=SourceBuild_${{ parameters.platform.name }}.xml fi ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ --configuration $buildConfig \ - --restore --build --pack -bl \ - --source-build \ + --restore --build --pack $publishArgs -bl \ ${{ parameters.platform.buildArguments }} \ + $officialBuildArgs \ $internalRuntimeDownloadArgs \ + $internalRestoreArgs \ $targetRidArgs \ - $portableBuildArgs \ + $runtimeOsArgs \ + $baseOsArgs \ + /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ + /p:ArcadeBuildFromSource=true \ + /p:DotNetBuildSourceOnly=true \ + /p:DotNetBuildRepo=true \ + /p:AssetManifestFileName=$assetManifestFileName displayName: Build +# Upload build logs for diagnosis. +- task: CopyFiles@2 + displayName: Prepare BuildLogs staging directory + inputs: + SourceFolder: '$(System.DefaultWorkingDirectory)' + Contents: | + **/*.log + **/*.binlog + artifacts/sb/prebuilt-report/** + TargetFolder: '$(Build.StagingDirectory)/BuildLogs' + CleanTargetFolder: true + continueOnError: true + condition: succeededOrFailed() + - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish BuildLogs - targetPath: artifacts/log/${{ coalesce(variables._BuildConfig, 'Release') }} + targetPath: '$(Build.StagingDirectory)/BuildLogs' artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt) continueOnError: true condition: succeededOrFailed() sbomEnabled: false # we don't need SBOM for logs + +# Manually inject component detection so that we can ignore the source build upstream cache, which contains +# a nupkg cache of input packages (a local feed). +# This path must match the upstream cache path in property 'CurrentRepoSourceBuiltNupkgCacheDir' +# in src\Microsoft.DotNet.Arcade.Sdk\tools\SourceBuild\SourceBuildArcade.targets +- template: /eng/common/core-templates/steps/component-governance.yml + parameters: + displayName: Component Detection (Exclude upstream cache) + is1ESPipeline: ${{ parameters.is1ESPipeline }} + ${{ if eq(length(parameters.componentGovernanceIgnoreDirectories), 0) }}: + componentGovernanceIgnoreDirectories: '$(System.DefaultWorkingDirectory)/artifacts/sb/src/artifacts/obj/source-built-upstream-cache' + ${{ else }}: + componentGovernanceIgnoreDirectories: ${{ join(',', parameters.componentGovernanceIgnoreDirectories) }} + disableComponentGovernance: ${{ eq(variables['System.TeamProject'], 'public') }} diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml deleted file mode 100644 index e9a694afa58..00000000000 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ /dev/null @@ -1,35 +0,0 @@ -parameters: - sourceIndexUploadPackageVersion: 2.0.0-20250818.1 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20250818.1 - sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json - binlogPath: artifacts/log/Debug/Build.binlog - -steps: -- task: UseDotNet@2 - displayName: "Source Index: Use .NET 9 SDK" - inputs: - packageType: sdk - version: 9.0.x - installationPath: $(Agent.TempDirectory)/dotnet - workingDirectory: $(Agent.TempDirectory) - -- script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - displayName: "Source Index: Download netsourceindex Tools" - # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. - workingDirectory: $(Agent.TempDirectory) - -- script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i ${{parameters.BinlogPath}} -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output - displayName: "Source Index: Process Binlog into indexable sln" - -- ${{ if and(ne(parameters.runAsPublic, 'true'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - task: AzureCLI@2 - displayName: "Source Index: Upload Source Index stage1 artifacts to Azure" - inputs: - azureSubscription: 'SourceDotNet Stage1 Publish' - addSpnToEnvironment: true - scriptType: 'ps' - scriptLocation: 'inlineScript' - inlineScript: | - $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1 diff --git a/eng/common/cross/arm64/tizen/tizen.patch b/eng/common/cross/arm64/tizen/tizen.patch index 2cebc547382..af7c8be0590 100644 --- a/eng/common/cross/arm64/tizen/tizen.patch +++ b/eng/common/cross/arm64/tizen/tizen.patch @@ -5,5 +5,5 @@ diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf64-littleaarch64) --GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib64/ld-linux-aarch64.so.1 ) ) +-GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-aarch64.so.1 ) ) +GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux-aarch64.so.1 ) ) diff --git a/eng/common/cross/armel/armel.jessie.patch b/eng/common/cross/armel/armel.jessie.patch new file mode 100644 index 00000000000..2d261561935 --- /dev/null +++ b/eng/common/cross/armel/armel.jessie.patch @@ -0,0 +1,43 @@ +diff -u -r a/usr/include/urcu/uatomic/generic.h b/usr/include/urcu/uatomic/generic.h +--- a/usr/include/urcu/uatomic/generic.h 2014-10-22 15:00:58.000000000 -0700 ++++ b/usr/include/urcu/uatomic/generic.h 2020-10-30 21:38:28.550000000 -0700 +@@ -69,10 +69,10 @@ + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_val_compare_and_swap_2(addr, old, _new); ++ return __sync_val_compare_and_swap_2((uint16_t*) addr, old, _new); + #endif + case 4: +- return __sync_val_compare_and_swap_4(addr, old, _new); ++ return __sync_val_compare_and_swap_4((uint32_t*) addr, old, _new); + #if (CAA_BITS_PER_LONG == 64) + case 8: + return __sync_val_compare_and_swap_8(addr, old, _new); +@@ -109,7 +109,7 @@ + return; + #endif + case 4: +- __sync_and_and_fetch_4(addr, val); ++ __sync_and_and_fetch_4((uint32_t*) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +@@ -148,7 +148,7 @@ + return; + #endif + case 4: +- __sync_or_and_fetch_4(addr, val); ++ __sync_or_and_fetch_4((uint32_t*) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +@@ -187,7 +187,7 @@ + return __sync_add_and_fetch_2(addr, val); + #endif + case 4: +- return __sync_add_and_fetch_4(addr, val); ++ return __sync_add_and_fetch_4((uint32_t*) addr, val); + #if (CAA_BITS_PER_LONG == 64) + case 8: + return __sync_add_and_fetch_8(addr, val); diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh index fbd8d80848a..7e9ba2b75ed 100755 --- a/eng/common/cross/build-android-rootfs.sh +++ b/eng/common/cross/build-android-rootfs.sh @@ -6,11 +6,10 @@ usage() { echo "Creates a toolchain and sysroot used for cross-compiling for Android." echo - echo "Usage: $0 [BuildArch] [ApiLevel] [--ndk NDKVersion]" + echo "Usage: $0 [BuildArch] [ApiLevel]" echo echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" - echo "NDKVersion is the version of Android NDK. The default is r21. See https://developer.android.com/ndk/downloads/revision_history" echo echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" echo "by setting the TOOLCHAIN_DIR environment variable" @@ -26,15 +25,10 @@ __BuildArch=arm64 __AndroidArch=aarch64 __AndroidToolchain=aarch64-linux-android -while :; do - if [[ "$#" -le 0 ]]; then - break - fi - - i=$1 - - lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" - case $lowerI in +for i in "$@" + do + lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" + case $lowerI in -?|-h|--help) usage exit 1 @@ -49,10 +43,6 @@ while :; do __AndroidArch=arm __AndroidToolchain=arm-linux-androideabi ;; - --ndk) - shift - __NDK_Version=$1 - ;; *[0-9]) __ApiLevel=$i ;; @@ -60,17 +50,8 @@ while :; do __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" ;; esac - shift done -if [[ "$__NDK_Version" == "r21" ]] || [[ "$__NDK_Version" == "r22" ]]; then - __NDK_File_Arch_Spec=-x86_64 - __SysRoot=sysroot -else - __NDK_File_Arch_Spec= - __SysRoot=toolchains/llvm/prebuilt/linux-x86_64/sysroot -fi - # Obtain the location of the bash script to figure out where the root of the repo is. __ScriptBaseDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -97,7 +78,6 @@ fi echo "Target API level: $__ApiLevel" echo "Target architecture: $__BuildArch" -echo "NDK version: $__NDK_Version" echo "NDK location: $__NDK_Dir" echo "Target Toolchain location: $__ToolchainDir" @@ -105,8 +85,8 @@ echo "Target Toolchain location: $__ToolchainDir" if [ ! -d $__NDK_Dir ]; then echo Downloading the NDK into $__NDK_Dir mkdir -p $__NDK_Dir - wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux$__NDK_File_Arch_Spec.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux.zip - unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux.zip -d $__CrossDir + wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux-x86_64.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip + unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip -d $__CrossDir fi if [ ! -d $__lldb_Dir ]; then @@ -136,11 +116,16 @@ for path in $(wget -qO- https://packages.termux.dev/termux-main-21/dists/stable/ fi done -cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/$__SysRoot/usr/" +cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/sysroot/usr/" # Generate platform file for build.sh script to assign to __DistroRid echo "Generating platform file..." -echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/$__SysRoot/android_platform - -echo "Now to build coreclr, libraries and host; run:" -echo ROOTFS_DIR=$(realpath $__ToolchainDir/$__SysRoot) ./build.sh clr+libs+host --cross --arch $__BuildArch +echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/sysroot/android_platform + +echo "Now to build coreclr, libraries and installers; run:" +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory coreclr +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory libraries +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory installer diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 8abfb71f727..4b5e8d7166b 100755 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -5,7 +5,7 @@ set -e usage() { echo "Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir ]" - echo "BuildArch can be: arm(default), arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64, x86" + echo "BuildArch can be: arm(default), arm64, armel, armv6, ppc64le, riscv64, s390x, x64, x86" echo "CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine" echo " for alpine can be specified with version: alpineX.YY or alpineedge" echo " for FreeBSD can be: freebsd13, freebsd14" @@ -15,7 +15,6 @@ usage() echo "llvmx[.y] - optional, LLVM version for LLVM related packages." echo "--skipunmount - optional, will skip the unmount of rootfs folder." echo "--skipsigcheck - optional, will skip package signature checks (allowing untrusted packages)." - echo "--skipemulation - optional, will skip qemu and debootstrap requirement when building environment for debian based systems." echo "--use-mirror - optional, use mirror URL to fetch resources, when available." echo "--jobs N - optional, restrict to N jobs." exit 1 @@ -53,27 +52,28 @@ __UbuntuPackages+=" symlinks" __UbuntuPackages+=" libicu-dev" __UbuntuPackages+=" liblttng-ust-dev" __UbuntuPackages+=" libunwind8-dev" +__UbuntuPackages+=" libnuma-dev" __AlpinePackages+=" gettext-dev" __AlpinePackages+=" icu-dev" __AlpinePackages+=" libunwind-dev" __AlpinePackages+=" lttng-ust-dev" __AlpinePackages+=" compiler-rt" +__AlpinePackages+=" numactl-dev" # runtime libraries' dependencies __UbuntuPackages+=" libcurl4-openssl-dev" __UbuntuPackages+=" libkrb5-dev" __UbuntuPackages+=" libssl-dev" __UbuntuPackages+=" zlib1g-dev" -__UbuntuPackages+=" libbrotli-dev" __AlpinePackages+=" curl-dev" __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" -__FreeBSDBase="13.4-RELEASE" -__FreeBSDPkg="1.21.3" +__FreeBSDBase="13.3-RELEASE" +__FreeBSDPkg="1.17.0" __FreeBSDABI="13" __FreeBSDPackages="libunwind" __FreeBSDPackages+=" icu" @@ -91,18 +91,18 @@ __HaikuPackages="gcc_syslibs" __HaikuPackages+=" gcc_syslibs_devel" __HaikuPackages+=" gmp" __HaikuPackages+=" gmp_devel" -__HaikuPackages+=" icu[0-9]+" -__HaikuPackages+=" icu[0-9]*_devel" +__HaikuPackages+=" icu66" +__HaikuPackages+=" icu66_devel" __HaikuPackages+=" krb5" __HaikuPackages+=" krb5_devel" __HaikuPackages+=" libiconv" __HaikuPackages+=" libiconv_devel" -__HaikuPackages+=" llvm[0-9]*_libunwind" -__HaikuPackages+=" llvm[0-9]*_libunwind_devel" +__HaikuPackages+=" llvm12_libunwind" +__HaikuPackages+=" llvm12_libunwind_devel" __HaikuPackages+=" mpfr" __HaikuPackages+=" mpfr_devel" -__HaikuPackages+=" openssl3" -__HaikuPackages+=" openssl3_devel" +__HaikuPackages+=" openssl" +__HaikuPackages+=" openssl_devel" __HaikuPackages+=" zlib" __HaikuPackages+=" zlib_devel" @@ -128,12 +128,10 @@ __AlpineKeys=' 616adfeb:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0BFD1D4lIxQcsqEpQzU\npNCYM3aP1V/fxxVdT4DWvSI53JHTwHQamKdMWtEXetWVbP5zSROniYKFXd/xrD9X\n0jiGHey3lEtylXRIPxe5s+wXoCmNLcJVnvTcDtwx/ne2NLHxp76lyc25At+6RgE6\nADjLVuoD7M4IFDkAsd8UQ8zM0Dww9SylIk/wgV3ZkifecvgUQRagrNUdUjR56EBZ\nraQrev4hhzOgwelT0kXCu3snbUuNY/lU53CoTzfBJ5UfEJ5pMw1ij6X0r5S9IVsy\nKLWH1hiO0NzU2c8ViUYCly4Fe9xMTFc6u2dy/dxf6FwERfGzETQxqZvSfrRX+GLj\n/QZAXiPg5178hT/m0Y3z5IGenIC/80Z9NCi+byF1WuJlzKjDcF/TU72zk0+PNM/H\nKuppf3JT4DyjiVzNC5YoWJT2QRMS9KLP5iKCSThwVceEEg5HfhQBRT9M6KIcFLSs\nmFjx9kNEEmc1E8hl5IR3+3Ry8G5/bTIIruz14jgeY9u5jhL8Vyyvo41jgt9sLHR1\n/J1TxKfkgksYev7PoX6/ZzJ1ksWKZY5NFoDXTNYUgzFUTOoEaOg3BAQKadb3Qbbq\nXIrxmPBdgrn9QI7NCgfnAY3Tb4EEjs3ON/BNyEhUENcXOH6I1NbcuBQ7g9P73kE4\nVORdoc8MdJ5eoKBpO8Ww8HECAwEAAQ== 616ae350:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyduVzi1mWm+lYo2Tqt/0\nXkCIWrDNP1QBMVPrE0/ZlU2bCGSoo2Z9FHQKz/mTyMRlhNqTfhJ5qU3U9XlyGOPJ\npiM+b91g26pnpXJ2Q2kOypSgOMOPA4cQ42PkHBEqhuzssfj9t7x47ppS94bboh46\nxLSDRff/NAbtwTpvhStV3URYkxFG++cKGGa5MPXBrxIp+iZf9GnuxVdST5PGiVGP\nODL/b69sPJQNbJHVquqUTOh5Ry8uuD2WZuXfKf7/C0jC/ie9m2+0CttNu9tMciGM\nEyKG1/Xhk5iIWO43m4SrrT2WkFlcZ1z2JSf9Pjm4C2+HovYpihwwdM/OdP8Xmsnr\nDzVB4YvQiW+IHBjStHVuyiZWc+JsgEPJzisNY0Wyc/kNyNtqVKpX6dRhMLanLmy+\nf53cCSI05KPQAcGj6tdL+D60uKDkt+FsDa0BTAobZ31OsFVid0vCXtsbplNhW1IF\nHwsGXBTVcfXg44RLyL8Lk/2dQxDHNHzAUslJXzPxaHBLmt++2COa2EI1iWlvtznk\nOk9WP8SOAIj+xdqoiHcC4j72BOVVgiITIJNHrbppZCq6qPR+fgXmXa+sDcGh30m6\n9Wpbr28kLMSHiENCWTdsFij+NQTd5S47H7XTROHnalYDuF1RpS+DpQidT5tUimaT\nJZDr++FjKrnnijbyNF8b98UCAwEAAQ== 616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\n8CM1S15HxV78s9dFntEqIokCAwEAAQ== -66ba20fe:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfB12w4ZgqsXWZDfUAV/\n6Y4aHUKIu3q4SXrNZ7CXF9nXoAVYrS7NAxJdAodsY3vPCN0g5O8DFXR+390LdOuQ\n+HsGKCc1k5tX5ZXld37EZNTNSbR0k+NKhd9h6X3u6wqPOx7SIKxwAQR8qeeFq4pP\nrt9GAGlxtuYgzIIcKJPwE0dZlcBCg+GnptCUZXp/38BP1eYC+xTXSL6Muq1etYfg\nodXdb7Yl+2h1IHuOwo5rjgY5kpY7GcAs8AjGk3lDD/av60OTYccknH0NCVSmPoXK\nvrxDBOn0LQRNBLcAfnTKgHrzy0Q5h4TNkkyTgxkoQw5ObDk9nnabTxql732yy9BY\ns+hM9+dSFO1HKeVXreYSA2n1ndF18YAvAumzgyqzB7I4pMHXq1kC/8bONMJxwSkS\nYm6CoXKyavp7RqGMyeVpRC7tV+blkrrUml0BwNkxE+XnwDRB3xDV6hqgWe0XrifD\nYTfvd9ScZQP83ip0r4IKlq4GMv/R5shcCRJSkSZ6QSGshH40JYSoiwJf5FHbj9ND\n7do0UAqebWo4yNx63j/wb2ULorW3AClv0BCFSdPsIrCStiGdpgJDBR2P2NZOCob3\nG9uMj+wJD6JJg2nWqNJxkANXX37Qf8plgzssrhrgOvB0fjjS7GYhfkfmZTJ0wPOw\nA8+KzFseBh4UFGgue78KwgkCAwEAAQ== ' __Keyring= __KeyringFile="/usr/share/keyrings/ubuntu-archive-keyring.gpg" __SkipSigCheck=0 -__SkipEmulation=0 __UseMirror=0 __UnprocessedBuildArgs= @@ -164,13 +162,9 @@ while :; do armel) __BuildArch=armel __UbuntuArch=armel - __UbuntuRepo="http://archive.debian.org/debian/" - __CodeName=buster + __UbuntuRepo="http://ftp.debian.org/debian/" + __CodeName=jessie __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" - __LLDB_Package="liblldb-6.0-dev" - __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" - __UbuntuPackages="${__UbuntuPackages// libomp5/}" - __UbuntuSuites= ;; armv6) __BuildArch=armv6 @@ -186,18 +180,6 @@ while :; do __Keyring="--keyring $__KeyringFile" fi ;; - loongarch64) - __BuildArch=loongarch64 - __AlpineArch=loongarch64 - __QEMUArch=loongarch64 - __UbuntuArch=loong64 - __UbuntuSuites=unreleased - __LLDB_Package="liblldb-19-dev" - - if [[ "$__CodeName" == "sid" ]]; then - __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" - fi - ;; riscv64) __BuildArch=riscv64 __AlpineArch=riscv64 @@ -282,21 +264,44 @@ while :; do ;; xenial) # Ubuntu 16.04 - __CodeName=xenial + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=xenial + fi + ;; + zesty) # Ubuntu 17.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=zesty + fi ;; bionic) # Ubuntu 18.04 - __CodeName=bionic + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=bionic + fi ;; focal) # Ubuntu 20.04 - __CodeName=focal + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=focal + fi ;; jammy) # Ubuntu 22.04 - __CodeName=jammy + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=jammy + fi ;; noble) # Ubuntu 24.04 - __CodeName=noble - if [[ -z "$__LLDB_Package" ]]; then - __LLDB_Package="liblldb-19-dev" + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=noble + fi + if [[ -n "$__LLDB_Package" ]]; then + __LLDB_Package="liblldb-18-dev" + fi + ;; + jessie) # Debian 8 + __CodeName=jessie + __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; stretch) # Debian 9 @@ -314,7 +319,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://archive.debian.org/debian/" + __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; bullseye) # Debian 11 @@ -335,28 +340,10 @@ while :; do ;; sid) # Debian sid __CodeName=sid - __UbuntuSuites= - - # Debian-Ports architectures need different values - case "$__UbuntuArch" in - amd64|arm64|armel|armhf|i386|mips64el|ppc64el|riscv64|s390x) - __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" - - if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" - fi - ;; - *) - __KeyringFile="/usr/share/keyrings/debian-ports-archive-keyring.gpg" - - if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" - fi - ;; - esac + __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" - if [[ -e "$__KeyringFile" ]]; then - __Keyring="--keyring $__KeyringFile" + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; tizen) @@ -383,7 +370,7 @@ while :; do ;; freebsd14) __CodeName=freebsd - __FreeBSDBase="14.2-RELEASE" + __FreeBSDBase="14.0-RELEASE" __FreeBSDABI="14" __SkipUnmount=1 ;; @@ -401,9 +388,6 @@ while :; do --skipsigcheck) __SkipSigCheck=1 ;; - --skipemulation) - __SkipEmulation=1 - ;; --rootfsdir|-rootfsdir) shift __RootfsDir="$1" @@ -436,15 +420,16 @@ case "$__AlpineVersion" in elif [[ "$__AlpineArch" == "x86" ]]; then __AlpineVersion=3.17 # minimum version that supports lldb-dev __AlpinePackages+=" llvm15-libs" - elif [[ "$__AlpineArch" == "riscv64" || "$__AlpineArch" == "loongarch64" ]]; then - __AlpineVersion=3.21 # minimum version that supports lldb-dev - __AlpinePackages+=" llvm19-libs" - elif [[ -n "$__AlpineMajorVersion" ]]; then - # use whichever alpine version is provided and select the latest toolchain libs + elif [[ "$__AlpineArch" == "riscv64" ]]; then __AlpineLlvmLibsLookup=1 + __AlpineVersion=edge # minimum version with APKINDEX.tar.gz (packages archive) else __AlpineVersion=3.13 # 3.13 to maximize compatibility __AlpinePackages+=" llvm10-libs" + + if [[ "$__AlpineArch" == "armv7" ]]; then + __AlpinePackages="${__AlpinePackages//numactl-dev/}" + fi fi esac @@ -454,6 +439,15 @@ if [[ "$__AlpineVersion" =~ 3\.1[345] ]]; then __AlpinePackages="${__AlpinePackages/compiler-rt/compiler-rt-static}" fi +if [[ "$__BuildArch" == "armel" ]]; then + __LLDB_Package="lldb-3.5-dev" +fi + +if [[ "$__CodeName" == "xenial" && "$__UbuntuArch" == "armhf" ]]; then + # libnuma-dev is not available on armhf for xenial + __UbuntuPackages="${__UbuntuPackages//libnuma-dev/}" +fi + __UbuntuPackages+=" ${__LLDB_Package:-}" if [[ -z "$__UbuntuRepo" ]]; then @@ -502,7 +496,7 @@ if [[ "$__CodeName" == "alpine" ]]; then arch="$(uname -m)" ensureDownloadTool - + if [[ "$__hasWget" == 1 ]]; then wget -P "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static" else @@ -518,6 +512,11 @@ if [[ "$__CodeName" == "alpine" ]]; then echo "$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static" | sha512sum -c chmod +x "$__ApkToolsDir/apk.static" + if [[ -f "/usr/bin/qemu-$__QEMUArch-static" ]]; then + mkdir -p "$__RootfsDir"/usr/bin + cp -v "/usr/bin/qemu-$__QEMUArch-static" "$__RootfsDir/usr/bin" + fi + if [[ "$__AlpineVersion" == "edge" ]]; then version=edge else @@ -537,10 +536,6 @@ if [[ "$__CodeName" == "alpine" ]]; then __ApkSignatureArg="--keys-dir $__ApkKeysDir" fi - if [[ "$__SkipEmulation" == "1" ]]; then - __NoEmulationArg="--no-scripts" - fi - # initialize DB # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ @@ -562,7 +557,7 @@ if [[ "$__CodeName" == "alpine" ]]; then "$__ApkToolsDir/apk.static" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ - -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" $__NoEmulationArg \ + -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ add $__AlpinePackages rm -r "$__ApkToolsDir" @@ -578,7 +573,7 @@ elif [[ "$__CodeName" == "freebsd" ]]; then curl -SL "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version fi echo "ABI = \"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\"; FINGERPRINTS = \"${__RootfsDir}/usr/share/keys\"; REPOS_DIR = [\"${__RootfsDir}/etc/pkg\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;" > "${__RootfsDir}"/usr/local/etc/pkg.conf - echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf + echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"${__RootfsDir}/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf mkdir -p "$__RootfsDir"/tmp # get and build package manager if [[ "$__hasWget" == 1 ]]; then @@ -686,7 +681,7 @@ elif [[ "$__CodeName" == "haiku" ]]; then ensureDownloadTool - echo "Downloading Haiku package tools" + echo "Downloading Haiku package tool" git clone https://github.com/haiku/haiku-toolchains-ubuntu --depth 1 "$__RootfsDir/tmp/script" if [[ "$__hasWget" == 1 ]]; then wget -O "$__RootfsDir/tmp/download/hosttools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --hosttools)" @@ -696,42 +691,34 @@ elif [[ "$__CodeName" == "haiku" ]]; then unzip -o "$__RootfsDir/tmp/download/hosttools.zip" -d "$__RootfsDir/tmp/bin" - HaikuBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" - HaikuPortsBaseUrl="https://eu.hpkg.haiku-os.org/haikuports/master/$__HaikuArch/current" - - echo "Downloading HaikuPorts package repository index..." - if [[ "$__hasWget" == 1 ]]; then - wget -P "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" - else - curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" - fi + DepotBaseUrl="https://depot.haiku-os.org/__api/v2/pkg/get-pkg" + HpkgBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" + # Download Haiku packages echo "Downloading Haiku packages" read -ra array <<<"$__HaikuPackages" for package in "${array[@]}"; do echo "Downloading $package..." - hpkgFilename="$(LD_LIBRARY_PATH="$__RootfsDir/tmp/bin" "$__RootfsDir/tmp/bin/package_repo" list -f "$__RootfsDir/tmp/download/repo" | - grep -E "${package}-" | sort -V | tail -n 1 | xargs)" - if [ -z "$hpkgFilename" ]; then - >&2 echo "ERROR: package $package missing." - exit 1 - fi - echo "Resolved filename: $hpkgFilename..." - hpkgDownloadUrl="$HaikuPortsBaseUrl/packages/$hpkgFilename" + # API documented here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L60 + # The schema here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L598 if [[ "$__hasWget" == 1 ]]; then + hpkgDownloadUrl="$(wget -qO- --post-data '{"name":"'"$package"'","repositorySourceCode":"haikuports_'$__HaikuArch'","versionType":"LATEST","naturalLanguageCode":"en"}' \ + --header 'Content-Type:application/json' "$DepotBaseUrl" | jq -r '.result.versions[].hpkgDownloadURL')" wget -P "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" else + hpkgDownloadUrl="$(curl -sSL -XPOST --data '{"name":"'"$package"'","repositorySourceCode":"haikuports_'$__HaikuArch'","versionType":"LATEST","naturalLanguageCode":"en"}' \ + --header 'Content-Type:application/json' "$DepotBaseUrl" | jq -r '.result.versions[].hpkgDownloadURL')" curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" fi done for package in haiku haiku_devel; do echo "Downloading $package..." if [[ "$__hasWget" == 1 ]]; then - hpkgVersion="$(wget -qO- "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" - wget -P "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" + hpkgVersion="$(wget -qO- "$HpkgBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" + wget -P "$__RootfsDir/tmp/download" "$HpkgBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" else - hpkgVersion="$(curl -sSL "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" - curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" + hpkgVersion="$(curl -sSL "$HpkgBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" + curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HpkgBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" fi done @@ -757,67 +744,25 @@ elif [[ "$__CodeName" == "haiku" ]]; then popd rm -rf "$__RootfsDir/tmp" elif [[ -n "$__CodeName" ]]; then - __Suites="$__CodeName $(for suite in $__UbuntuSuites; do echo -n "$__CodeName-$suite "; done)" - - if [[ "$__SkipEmulation" == "1" ]]; then - if [[ -z "$AR" ]]; then - if command -v ar &>/dev/null; then - AR="$(command -v ar)" - elif command -v llvm-ar &>/dev/null; then - AR="$(command -v llvm-ar)" - else - echo "Unable to find ar or llvm-ar on PATH, add them to PATH or set AR environment variable pointing to the available AR tool" - exit 1 - fi - fi - - PYTHON=${PYTHON_EXECUTABLE:-python3} - - # shellcheck disable=SC2086,SC2046 - echo running "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ - $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ - $__UbuntuPackages - - # shellcheck disable=SC2086,SC2046 - "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ - $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ - $__UbuntuPackages - exit 0 - fi - - __UpdateOptions= if [[ "$__SkipSigCheck" == "0" ]]; then __Keyring="$__Keyring --force-check-gpg" - else - __Keyring= - __UpdateOptions="--allow-unauthenticated --allow-insecure-repositories" fi # shellcheck disable=SC2086 echo running debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" + debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" - # shellcheck disable=SC2086 - if ! debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo"; then - echo "debootstrap failed! dumping debootstrap.log" - cat "$__RootfsDir/debootstrap/debootstrap.log" - exit 1 - fi - - rm -rf "$__RootfsDir"/etc/apt/*.{sources,list} "$__RootfsDir"/etc/apt/sources.list.d mkdir -p "$__RootfsDir/etc/apt/sources.list.d/" - - # shellcheck disable=SC2086 cat > "$__RootfsDir/etc/apt/sources.list.d/$__CodeName.sources" < token2) - (token1 < token2) - else: - return -1 if isinstance(token1, str) else 1 - - return len(tokens1) - len(tokens2) - -def compare_debian_versions(version1, version2): - """Compare two Debian package versions.""" - epoch1, upstream1, revision1 = parse_debian_version(version1) - epoch2, upstream2, revision2 = parse_debian_version(version2) - - if epoch1 != epoch2: - return epoch1 - epoch2 - - result = compare_upstream_version(upstream1, upstream2) - if result != 0: - return result - - return compare_upstream_version(revision1, revision2) - -def resolve_dependencies(packages, aliases, desired_packages): - """Recursively resolves dependencies for the desired packages.""" - resolved = [] - to_process = deque(desired_packages) - - while to_process: - current = to_process.popleft() - resolved_package = current if current in packages else aliases.get(current, [None])[0] - - if not resolved_package: - print(f"Error: Package '{current}' was not found in the available packages.") - sys.exit(1) - - if resolved_package not in resolved: - resolved.append(resolved_package) - - deps = packages.get(resolved_package, {}).get("Depends", "") - if deps: - deps = [dep.split(' ')[0] for dep in deps.split(', ') if dep] - for dep in deps: - if dep not in resolved and dep not in to_process and dep in packages: - to_process.append(dep) - - return resolved - -def parse_package_index(content): - """Parses the Packages.gz file and returns package information.""" - packages = {} - aliases = {} - entries = re.split(r'\n\n+', content) - - for entry in entries: - fields = dict(re.findall(r'^(\S+): (.+)$', entry, re.MULTILINE)) - if "Package" in fields: - package_name = fields["Package"] - version = fields.get("Version") - filename = fields.get("Filename") - depends = fields.get("Depends") - provides = fields.get("Provides", None) - - # Only update if package_name is not in packages or if the new version is higher - if package_name not in packages or compare_debian_versions(version, packages[package_name]["Version"]) > 0: - packages[package_name] = { - "Version": version, - "Filename": filename, - "Depends": depends - } - - # Update aliases if package provides any alternatives - if provides: - provides_list = [x.strip() for x in provides.split(",")] - for alias in provides_list: - # Strip version specifiers - alias_name = re.sub(r'\s*\(=.*\)', '', alias) - if alias_name not in aliases: - aliases[alias_name] = [] - if package_name not in aliases[alias_name]: - aliases[alias_name].append(package_name) - - return packages, aliases - -def install_packages(mirror, packages_info, aliases, tmp_dir, extract_dir, ar_tool, desired_packages): - """Downloads .deb files and extracts them.""" - resolved_packages = resolve_dependencies(packages_info, aliases, desired_packages) - print(f"Resolved packages (including dependencies): {resolved_packages}") - - packages_to_download = {} - - for pkg in resolved_packages: - if pkg in packages_info: - packages_to_download[pkg] = packages_info[pkg] - - if pkg in aliases: - for alias in aliases[pkg]: - if alias in packages_info: - packages_to_download[alias] = packages_info[alias] - - asyncio.run(download_deb_files_parallel(mirror, packages_to_download, tmp_dir)) - - package_to_deb_file_map = {} - for pkg in resolved_packages: - pkg_info = packages_info.get(pkg) - if pkg_info: - deb_filename = pkg_info.get("Filename") - if deb_filename: - deb_file_path = os.path.join(tmp_dir, os.path.basename(deb_filename)) - package_to_deb_file_map[pkg] = deb_file_path - - for pkg in reversed(resolved_packages): - deb_file = package_to_deb_file_map.get(pkg) - if deb_file and os.path.exists(deb_file): - extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool) - - print("All done!") - -def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): - """Extract .deb file contents""" - - os.makedirs(extract_dir, exist_ok=True) - - with tempfile.TemporaryDirectory(dir=tmp_dir) as tmp_subdir: - result = subprocess.run(f"{ar_tool} t {os.path.abspath(deb_file)}", cwd=tmp_subdir, check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - tar_filename = None - for line in result.stdout.decode().splitlines(): - if line.startswith("data.tar"): - tar_filename = line.strip() - break - - if not tar_filename: - raise FileNotFoundError(f"Could not find 'data.tar.*' in {deb_file}.") - - tar_file_path = os.path.join(tmp_subdir, tar_filename) - print(f"Extracting {tar_filename} from {deb_file}..") - - subprocess.run(f"{ar_tool} p {os.path.abspath(deb_file)} {tar_filename} > {tar_file_path}", check=True, shell=True) - - file_extension = os.path.splitext(tar_file_path)[1].lower() - - if file_extension == ".xz": - mode = "r:xz" - elif file_extension == ".gz": - mode = "r:gz" - elif file_extension == ".zst": - # zstd is not supported by standard library yet - decompressed_tar_path = tar_file_path.replace(".zst", "") - with open(tar_file_path, "rb") as zst_file, open(decompressed_tar_path, "wb") as decompressed_file: - dctx = zstandard.ZstdDecompressor() - dctx.copy_stream(zst_file, decompressed_file) - - tar_file_path = decompressed_tar_path - mode = "r" - else: - raise ValueError(f"Unsupported compression format: {file_extension}") - - with tarfile.open(tar_file_path, mode) as tar: - tar.extractall(path=extract_dir, filter='fully_trusted') - -def finalize_setup(rootfsdir): - lib_dir = os.path.join(rootfsdir, 'lib') - usr_lib_dir = os.path.join(rootfsdir, 'usr', 'lib') - - if os.path.exists(lib_dir): - if os.path.islink(lib_dir): - os.remove(lib_dir) - else: - os.makedirs(usr_lib_dir, exist_ok=True) - - for item in os.listdir(lib_dir): - src = os.path.join(lib_dir, item) - dest = os.path.join(usr_lib_dir, item) - - if os.path.isdir(src): - shutil.copytree(src, dest, dirs_exist_ok=True) - else: - shutil.copy2(src, dest) - - shutil.rmtree(lib_dir) - - os.symlink(usr_lib_dir, lib_dir) - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Generate rootfs for .NET runtime on Debian-like OS") - parser.add_argument("--distro", required=False, help="Distro name (e.g., debian, ubuntu, etc.)") - parser.add_argument("--arch", required=True, help="Architecture (e.g., amd64, loong64, etc.)") - parser.add_argument("--rootfsdir", required=True, help="Destination directory.") - parser.add_argument('--suite', required=True, action='append', help='Specify one or more repository suites to collect index data.') - parser.add_argument("--mirror", required=False, help="Mirror (e.g., http://ftp.debian.org/debian-ports etc.)") - parser.add_argument("--artool", required=False, default="ar", help="ar tool to extract debs (e.g., ar, llvm-ar etc.)") - parser.add_argument("packages", nargs="+", help="List of package names to be installed.") - - args = parser.parse_args() - - if args.mirror is None: - if args.distro == "ubuntu": - args.mirror = "http://archive.ubuntu.com/ubuntu" if args.arch in ["amd64", "i386"] else "http://ports.ubuntu.com/ubuntu-ports" - elif args.distro == "debian": - args.mirror = "http://ftp.debian.org/debian-ports" - else: - raise Exception("Unsupported distro") - - DESIRED_PACKAGES = args.packages + [ # base packages - "dpkg", - "busybox", - "libc-bin", - "base-files", - "base-passwd", - "debianutils" - ] - - print(f"Creating rootfs. rootfsdir: {args.rootfsdir}, distro: {args.distro}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}") - - package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite)) - - packages_info, aliases = parse_package_index(package_index_content) - - with tempfile.TemporaryDirectory() as tmp_dir: - install_packages(args.mirror, packages_info, aliases, tmp_dir, args.rootfsdir, args.artool, DESIRED_PACKAGES) - - finalize_setup(args.rootfsdir) diff --git a/eng/common/cross/tizen-fetch.sh b/eng/common/cross/tizen-fetch.sh index 37c3a61f1de..28936ceef3a 100755 --- a/eng/common/cross/tizen-fetch.sh +++ b/eng/common/cross/tizen-fetch.sh @@ -156,8 +156,13 @@ fetch_tizen_pkgs() done } -BASE="Tizen-Base" -UNIFIED="Tizen-Unified" +if [ "$TIZEN_ARCH" == "riscv64" ]; then + BASE="Tizen-Base-RISCV" + UNIFIED="Tizen-Unified-RISCV" +else + BASE="Tizen-Base" + UNIFIED="Tizen-Unified" +fi Inform "Initialize ${TIZEN_ARCH} base" fetch_tizen_pkgs_init standard $BASE diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index 0ff85cf0367..9a7ecfbd42c 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -67,13 +67,6 @@ elseif(TARGET_ARCH_NAME STREQUAL "armv6") else() set(TOOLCHAIN "arm-linux-gnueabihf") endif() -elseif(TARGET_ARCH_NAME STREQUAL "loongarch64") - set(CMAKE_SYSTEM_PROCESSOR "loongarch64") - if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/loongarch64-alpine-linux-musl) - set(TOOLCHAIN "loongarch64-alpine-linux-musl") - else() - set(TOOLCHAIN "loongarch64-linux-gnu") - endif() elseif(TARGET_ARCH_NAME STREQUAL "ppc64le") set(CMAKE_SYSTEM_PROCESSOR ppc64le) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl) @@ -125,7 +118,7 @@ elseif(TARGET_ARCH_NAME STREQUAL "x86") set(TIZEN_TOOLCHAIN "i586-tizen-linux-gnu") endif() else() - message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64 and x86 are supported!") + message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, ppc64le, riscv64, s390x, x64 and x86 are supported!") endif() if(DEFINED ENV{TOOLCHAIN}) @@ -155,25 +148,6 @@ if(TIZEN) include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++/${TIZEN_TOOLCHAIN}) endif() -function(locate_toolchain_exec exec var) - set(TOOLSET_PREFIX ${TOOLCHAIN}-) - string(TOUPPER ${exec} EXEC_UPPERCASE) - if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") - set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) - return() - endif() - - find_program(EXEC_LOCATION_${exec} - NAMES - "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" - "${TOOLSET_PREFIX}${exec}") - - if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") - message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") - endif() - set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) -endfunction() - if(ANDROID) if(TARGET_ARCH_NAME STREQUAL "arm") set(ANDROID_ABI armeabi-v7a) @@ -204,24 +178,66 @@ elseif(FREEBSD) set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld") elseif(ILLUMOS) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") - set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") - set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") - set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") include_directories(SYSTEM ${CROSS_ROOTFS}/include) + set(TOOLSET_PREFIX ${TOOLCHAIN}-) + function(locate_toolchain_exec exec var) + string(TOUPPER ${exec} EXEC_UPPERCASE) + if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") + set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) + return() + endif() + + find_program(EXEC_LOCATION_${exec} + NAMES + "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" + "${TOOLSET_PREFIX}${exec}") + + if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") + message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") + endif() + set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) + endfunction() + + set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") + locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") elseif(HAIKU) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_PROGRAM_PATH "${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin") + + set(TOOLSET_PREFIX ${TOOLCHAIN}-) + function(locate_toolchain_exec exec var) + string(TOUPPER ${exec} EXEC_UPPERCASE) + if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") + set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) + return() + endif() + + find_program(EXEC_LOCATION_${exec} + NAMES + "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" + "${TOOLSET_PREFIX}${exec}") + + if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") + message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") + endif() + set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) + endfunction() + set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") - set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") - set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") + # let CMake set up the correct search paths include(Platform/Haiku) else() @@ -291,7 +307,7 @@ endif() # Specify compile options -if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) +if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index e889f439b8d..36dbd45e1ce 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -68,7 +68,7 @@ function InstallDarcCli { fi fi - local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" + local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." diff --git a/eng/common/dotnet.cmd b/eng/common/dotnet.cmd deleted file mode 100644 index 527fa4bb38f..00000000000 --- a/eng/common/dotnet.cmd +++ /dev/null @@ -1,7 +0,0 @@ -@echo off - -:: This script is used to install the .NET SDK. -:: It will also invoke the SDK with any provided arguments. - -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet.ps1""" %*" -exit /b %ErrorLevel% diff --git a/eng/common/dotnet.ps1 b/eng/common/dotnet.ps1 deleted file mode 100644 index 45e5676c9eb..00000000000 --- a/eng/common/dotnet.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -# This script is used to install the .NET SDK. -# It will also invoke the SDK with any provided arguments. - -. $PSScriptRoot\tools.ps1 -$dotnetRoot = InitializeDotNetCli -install:$true - -# Invoke acquired SDK with args if they are provided -if ($args.count -gt 0) { - $env:DOTNET_NOLOGO=1 - & "$dotnetRoot\dotnet.exe" $args -} diff --git a/eng/common/dotnet.sh b/eng/common/dotnet.sh deleted file mode 100644 index 2ef68235675..00000000000 --- a/eng/common/dotnet.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# This script is used to install the .NET SDK. -# It will also invoke the SDK with any provided arguments. - -source="${BASH_SOURCE[0]}" -# resolve $SOURCE until the file is no longer a symlink -while [[ -h $source ]]; do - scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - source="$(readlink "$source")" - - # if $source was a relative symlink, we need to resolve it relative to the path where the - # symlink file was located - [[ $source != /* ]] && source="$scriptroot/$source" -done -scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - -source $scriptroot/tools.sh -InitializeDotNetCli true # install - -# Invoke acquired SDK with args if they are provided -if [[ $# > 0 ]]; then - __dotnetDir=${_InitializeDotNetCli} - dotnetPath=${__dotnetDir}/dotnet - ${dotnetPath} "$@" -fi diff --git a/eng/common/generate-locproject.ps1 b/eng/common/generate-locproject.ps1 index fa1cdc2b300..524aaa57f2b 100644 --- a/eng/common/generate-locproject.ps1 +++ b/eng/common/generate-locproject.ps1 @@ -33,27 +33,15 @@ $jsonTemplateFiles | ForEach-Object { $jsonWinformsTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\\strings\.json" } # current winforms pattern -$wxlFilesV3 = @() -$wxlFilesV5 = @() $wxlFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\.+\.wxl" -And -Not( $_.Directory.Name -Match "\d{4}" ) } # localized files live in four digit lang ID directories; this excludes them if (-not $wxlFiles) { $wxlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\1033\\.+\.wxl" } # pick up en files (1033 = en) specifically so we can copy them to use as the neutral xlf files if ($wxlEnFiles) { - $wxlFiles = @() - $wxlEnFiles | ForEach-Object { - $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" - $content = Get-Content $_.FullName -Raw - - # Split files on schema to select different parser settings in the generated project. - if ($content -like "*http://wixtoolset.org/schemas/v4/wxl*") - { - $wxlFilesV5 += Copy-Item $_.FullName -Destination $destinationFile -PassThru - } - elseif ($content -like "*http://schemas.microsoft.com/wix/2006/localization*") - { - $wxlFilesV3 += Copy-Item $_.FullName -Destination $destinationFile -PassThru - } - } + $wxlFiles = @() + $wxlEnFiles | ForEach-Object { + $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" + $wxlFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru + } } } @@ -126,32 +114,7 @@ $locJson = @{ CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "wxl_loc.lss" ) LocItems = @( - $wxlFilesV3 | ForEach-Object { - $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" - $continue = $true - foreach ($exclusion in $exclusions.Exclusions) { - if ($_.FullName.Contains($exclusion)) { - $continue = $false - } - } - $sourceFile = ($_.FullName | Resolve-Path -Relative) - if ($continue) - { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnPath" - OutputPath = $outputPath - } - } - } - ) - }, - @{ - LanguageSet = $LanguageSet - CloneLanguageSet = "WiX_CloneLanguages" - LssFiles = @( "P210WxlSchemaV4.lss" ) - LocItems = @( - $wxlFilesV5 | ForEach-Object { + $wxlFiles | ForEach-Object { $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh deleted file mode 100644 index 477a44f335b..00000000000 --- a/eng/common/native/install-dependencies.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh - -set -e - -# This is a simple script primarily used for CI to install necessary dependencies -# -# Usage: -# -# ./install-dependencies.sh - -os="$(echo "$1" | tr "[:upper:]" "[:lower:]")" - -if [ -z "$os" ]; then - . "$(dirname "$0")"/init-os-and-arch.sh -fi - -case "$os" in - linux) - if [ -e /etc/os-release ]; then - . /etc/os-release - fi - - if [ "$ID" = "debian" ] || [ "$ID_LIKE" = "debian" ]; then - apt update - - apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \ - libssl-dev libkrb5-dev pigz cpio - - localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then - pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" - $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio - elif [ "$ID" = "alpine" ]; then - apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio - else - echo "Unsupported distro. distro: $ID" - exit 1 - fi - ;; - - osx|maccatalyst|ios|iossimulator|tvos|tvossimulator) - echo "Installed xcode version: $(xcode-select -p)" - - export HOMEBREW_NO_INSTALL_CLEANUP=1 - export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 - # Skip brew update for now, see https://github.com/actions/setup-python/issues/577 - # brew update --preinstall - brew bundle --no-upgrade --file=- < Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." - Write-Host " -excludeCIBinaryLog When running on CI, allow no binary log (short: -nobl)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." } @@ -37,11 +34,10 @@ function Print-Usage() { function Build([string]$target) { $logSuffix = if ($target -eq 'Execute') { '' } else { ".$target" } $log = Join-Path $LogDir "$task$logSuffix.binlog" - $binaryLogArg = if ($binaryLog) { "/bl:$log" } else { "" } $outputPath = Join-Path $ToolsetDir "$task\" MSBuild $taskProject ` - $binaryLogArg ` + /bl:$log ` /t:$target ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` @@ -68,7 +64,7 @@ try { $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty } if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { - $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.13.0" -MemberType NoteProperty + $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.12.0" -MemberType NoteProperty } if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true diff --git a/eng/common/sdk-task.sh b/eng/common/sdk-task.sh deleted file mode 100644 index 3270f83fa9a..00000000000 --- a/eng/common/sdk-task.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash - -show_usage() { - echo "Common settings:" - echo " --task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" - echo " --restore Restore dependencies" - echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" - echo " --help Print help and exit" - echo "" - - echo "Advanced settings:" - echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" - echo " --noWarnAsError Do not warn as error" - echo "" - echo "Command line arguments not listed above are passed thru to msbuild." -} - -source="${BASH_SOURCE[0]}" - -# resolve $source until the file is no longer a symlink -while [[ -h "$source" ]]; do - scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the - # symlink file was located - [[ $source != /* ]] && source="$scriptroot/$source" -done -scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - -Build() { - local target=$1 - local log_suffix="" - [[ "$target" != "Execute" ]] && log_suffix=".$target" - local log="$log_dir/$task$log_suffix.binlog" - local binaryLogArg="" - [[ $binary_log == true ]] && binaryLogArg="/bl:$log" - local output_path="$toolset_dir/$task/" - - MSBuild "$taskProject" \ - $binaryLogArg \ - /t:"$target" \ - /p:Configuration="$configuration" \ - /p:RepoRoot="$repo_root" \ - /p:BaseIntermediateOutputPath="$output_path" \ - /v:"$verbosity" \ - $properties -} - -binary_log=true -configuration="Debug" -verbosity="minimal" -exclude_ci_binary_log=false -restore=false -help=false -properties='' -warnAsError=true - -while (($# > 0)); do - lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" - case $lowerI in - --task) - task=$2 - shift 2 - ;; - --restore) - restore=true - shift 1 - ;; - --verbosity) - verbosity=$2 - shift 2 - ;; - --excludecibinarylog|--nobl) - binary_log=false - exclude_ci_binary_log=true - shift 1 - ;; - --noWarnAsError) - warnAsError=false - shift 1 - ;; - --help) - help=true - shift 1 - ;; - *) - properties="$properties $1" - shift 1 - ;; - esac -done - -ci=true - -if $help; then - show_usage - exit 0 -fi - -. "$scriptroot/tools.sh" -InitializeToolset - -if [[ -z "$task" ]]; then - Write-PipelineTelemetryError -Category 'Task' -Name 'MissingTask' -Message "Missing required parameter '-task '" - ExitWithExitCode 1 -fi - -taskProject=$(GetSdkTaskProject "$task") -if [[ ! -e "$taskProject" ]]; then - Write-PipelineTelemetryError -Category 'Task' -Name 'UnknownTask' -Message "Unknown task: $task" - ExitWithExitCode 1 -fi - -if $restore; then - Build "Restore" -fi - -Build "Execute" - - -ExitWithExitCode 0 diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config index e5f543ea68c..4585cfd6bba 100644 --- a/eng/common/sdl/packages.config +++ b/eng/common/sdl/packages.config @@ -1,4 +1,4 @@ - + diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml index 92a0664f564..81ea7a261f2 100644 --- a/eng/common/templates-official/job/job.yml +++ b/eng/common/templates-official/job/job.yml @@ -31,7 +31,6 @@ jobs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} condition: always() - retryCountOnTaskFailure: 10 # for any logs being locked continueOnError: true - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - output: pipelineArtifact @@ -40,7 +39,6 @@ jobs: displayName: 'Publish logs' continueOnError: true condition: always() - retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if eq(parameters.enablePublishBuildArtifacts, true) }}: @@ -48,7 +46,7 @@ jobs: displayName: Publish Logs PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container - ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} + ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} continueOnError: true condition: always() sbomEnabled: false # we don't need SBOM for logs diff --git a/eng/common/templates-official/steps/publish-build-artifacts.yml b/eng/common/templates-official/steps/publish-build-artifacts.yml index fcf6637b2eb..100a3fc9849 100644 --- a/eng/common/templates-official/steps/publish-build-artifacts.yml +++ b/eng/common/templates-official/steps/publish-build-artifacts.yml @@ -24,10 +24,6 @@ parameters: - name: is1ESPipeline type: boolean default: true - -- name: retryCountOnTaskFailure - type: string - default: 10 steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: @@ -42,5 +38,4 @@ steps: PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: ArtifactName: ${{ parameters.artifactName }} - ${{ if parameters.retryCountOnTaskFailure }}: - retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} + diff --git a/eng/common/templates-official/steps/source-index-stage1-publish.yml b/eng/common/templates-official/steps/source-index-stage1-publish.yml deleted file mode 100644 index 9b8b80942b5..00000000000 --- a/eng/common/templates-official/steps/source-index-stage1-publish.yml +++ /dev/null @@ -1,7 +0,0 @@ -steps: -- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml - parameters: - is1ESPipeline: true - - ${{ each parameter in parameters }}: - ${{ parameter.key }}: ${{ parameter.value }} diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index 238fa0818f7..5bdd3dd85fd 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -46,7 +46,6 @@ jobs: artifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} continueOnError: true condition: always() - retryCountOnTaskFailure: 10 # for any logs being locked - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: @@ -57,7 +56,6 @@ jobs: displayName: 'Publish logs' continueOnError: true condition: always() - retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}: @@ -68,7 +66,7 @@ jobs: displayName: Publish Logs pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container - artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} + artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} continueOnError: true condition: always() diff --git a/eng/common/templates/steps/publish-build-artifacts.yml b/eng/common/templates/steps/publish-build-artifacts.yml index 605e602e94d..6428a98dfef 100644 --- a/eng/common/templates/steps/publish-build-artifacts.yml +++ b/eng/common/templates/steps/publish-build-artifacts.yml @@ -25,10 +25,6 @@ parameters: type: string default: 'Container' -- name: retryCountOnTaskFailure - type: string - default: 10 - steps: - ${{ if eq(parameters.is1ESPipeline, true) }}: - 'eng/common/templates cannot be referenced from a 1ES managed template': error @@ -41,6 +37,4 @@ steps: PublishLocation: ${{ parameters.publishLocation }} PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: - ArtifactName: ${{ parameters.artifactName }} - ${{ if parameters.retryCountOnTaskFailure }}: - retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} + ArtifactName: ${{ parameters.artifactName }} \ No newline at end of file diff --git a/eng/common/templates/steps/source-index-stage1-publish.yml b/eng/common/templates/steps/source-index-stage1-publish.yml deleted file mode 100644 index 182cec33a7b..00000000000 --- a/eng/common/templates/steps/source-index-stage1-publish.yml +++ /dev/null @@ -1,7 +0,0 @@ -steps: -- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml - parameters: - is1ESPipeline: false - - ${{ each parameter in parameters }}: - ${{ parameter.key }}: ${{ parameter.value }} diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml deleted file mode 100644 index 599afb6186b..00000000000 --- a/eng/common/templates/steps/vmr-sync.yml +++ /dev/null @@ -1,207 +0,0 @@ -### These steps synchronize new code from product repositories into the VMR (https://github.com/dotnet/dotnet). -### They initialize the darc CLI and pull the new updates. -### Changes are applied locally onto the already cloned VMR (located in $vmrPath). - -parameters: -- name: targetRef - displayName: Target revision in dotnet/ to synchronize - type: string - default: $(Build.SourceVersion) - -- name: vmrPath - displayName: Path where the dotnet/dotnet is checked out to - type: string - default: $(Agent.BuildDirectory)/vmr - -- name: additionalSyncs - displayName: Optional list of package names whose repo's source will also be synchronized in the local VMR, e.g. NuGet.Protocol - type: object - default: [] - -steps: -- checkout: vmr - displayName: Clone dotnet/dotnet - path: vmr - clean: true - -- checkout: self - displayName: Clone $(Build.Repository.Name) - path: repo - fetchDepth: 0 - -# This step is needed so that when we get a detached HEAD / shallow clone, -# we still pull the commit into the temporary repo clone to use it during the sync. -# Also unshallow the clone so that forwardflow command would work. -- script: | - git branch repo-head - git rev-parse HEAD - displayName: Label PR commit - workingDirectory: $(Agent.BuildDirectory)/repo - -- script: | - vmr_sha=$(grep -oP '(?<=Sha=")[^"]*' $(Agent.BuildDirectory)/repo/eng/Version.Details.xml) - echo "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Unix) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- powershell: | - [xml]$xml = Get-Content -Path $(Agent.BuildDirectory)/repo/eng/Version.Details.xml - $vmr_sha = $xml.SelectSingleNode("//Source").Sha - Write-Output "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Windows) - condition: eq(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- script: | - git fetch --all - git checkout $(vmr_sha) - displayName: Checkout VMR at correct sha for repo flow - workingDirectory: ${{ parameters.vmrPath }} - -- script: | - git config --global user.name "dotnet-maestro[bot]" - git config --global user.email "dotnet-maestro[bot]@users.noreply.github.com" - displayName: Set git author to dotnet-maestro[bot] - workingDirectory: ${{ parameters.vmrPath }} - -- script: | - ./eng/common/vmr-sync.sh \ - --vmr ${{ parameters.vmrPath }} \ - --tmp $(Agent.TempDirectory) \ - --azdev-pat '$(dn-bot-all-orgs-code-r)' \ - --ci \ - --debug - - if [ "$?" -ne 0 ]; then - echo "##vso[task.logissue type=error]Failed to synchronize the VMR" - exit 1 - fi - displayName: Sync repo into VMR (Unix) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- script: | - git config --global diff.astextplain.textconv echo - git config --system core.longpaths true - displayName: Configure Windows git (longpaths, astextplain) - condition: eq(variables['Agent.OS'], 'Windows_NT') - -- powershell: | - ./eng/common/vmr-sync.ps1 ` - -vmr ${{ parameters.vmrPath }} ` - -tmp $(Agent.TempDirectory) ` - -azdevPat '$(dn-bot-all-orgs-code-r)' ` - -ci ` - -debugOutput - - if ($LASTEXITCODE -ne 0) { - echo "##vso[task.logissue type=error]Failed to synchronize the VMR" - exit 1 - } - displayName: Sync repo into VMR (Windows) - condition: eq(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: - - task: CopyFiles@2 - displayName: Collect failed patches - condition: failed() - inputs: - SourceFolder: '$(Agent.TempDirectory)' - Contents: '*.patch' - TargetFolder: '$(Build.ArtifactStagingDirectory)/FailedPatches' - - - publish: '$(Build.ArtifactStagingDirectory)/FailedPatches' - artifact: $(System.JobDisplayName)_FailedPatches - displayName: Upload failed patches - condition: failed() - -- ${{ each assetName in parameters.additionalSyncs }}: - # The vmr-sync script ends up staging files in the local VMR so we have to commit those - - script: - git commit --allow-empty -am "Forward-flow $(Build.Repository.Name)" - displayName: Commit local VMR changes - workingDirectory: ${{ parameters.vmrPath }} - - - script: | - set -ex - - echo "Searching for details of asset ${{ assetName }}..." - - # Use darc to get dependencies information - dependencies=$(./.dotnet/dotnet darc get-dependencies --name '${{ assetName }}' --ci) - - # Extract repository URL and commit hash - repository=$(echo "$dependencies" | grep 'Repo:' | sed 's/Repo:[[:space:]]*//' | head -1) - - if [ -z "$repository" ]; then - echo "##vso[task.logissue type=error]Asset ${{ assetName }} not found in the dependency list" - exit 1 - fi - - commit=$(echo "$dependencies" | grep 'Commit:' | sed 's/Commit:[[:space:]]*//' | head -1) - - echo "Updating the VMR from $repository / $commit..." - cd .. - git clone $repository ${{ assetName }} - cd ${{ assetName }} - git checkout $commit - git branch "sync/$commit" - - ./eng/common/vmr-sync.sh \ - --vmr ${{ parameters.vmrPath }} \ - --tmp $(Agent.TempDirectory) \ - --azdev-pat '$(dn-bot-all-orgs-code-r)' \ - --ci \ - --debug - - if [ "$?" -ne 0 ]; then - echo "##vso[task.logissue type=error]Failed to synchronize the VMR" - exit 1 - fi - displayName: Sync ${{ assetName }} into (Unix) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - - - powershell: | - $ErrorActionPreference = 'Stop' - - Write-Host "Searching for details of asset ${{ assetName }}..." - - $dependencies = .\.dotnet\dotnet darc get-dependencies --name '${{ assetName }}' --ci - - $repository = $dependencies | Select-String -Pattern 'Repo:\s+([^\s]+)' | Select-Object -First 1 - $repository -match 'Repo:\s+([^\s]+)' | Out-Null - $repository = $matches[1] - - if ($repository -eq $null) { - Write-Error "Asset ${{ assetName }} not found in the dependency list" - exit 1 - } - - $commit = $dependencies | Select-String -Pattern 'Commit:\s+([^\s]+)' | Select-Object -First 1 - $commit -match 'Commit:\s+([^\s]+)' | Out-Null - $commit = $matches[1] - - Write-Host "Updating the VMR from $repository / $commit..." - cd .. - git clone $repository ${{ assetName }} - cd ${{ assetName }} - git checkout $commit - git branch "sync/$commit" - - .\eng\common\vmr-sync.ps1 ` - -vmr ${{ parameters.vmrPath }} ` - -tmp $(Agent.TempDirectory) ` - -azdevPat '$(dn-bot-all-orgs-code-r)' ` - -ci ` - -debugOutput - - if ($LASTEXITCODE -ne 0) { - echo "##vso[task.logissue type=error]Failed to synchronize the VMR" - exit 1 - } - displayName: Sync ${{ assetName }} into (Windows) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml deleted file mode 100644 index ce3c29a62fa..00000000000 --- a/eng/common/templates/vmr-build-pr.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This pipeline is used for running the VMR verification of the PR changes in repo-level PRs. -# -# It will run a full set of verification jobs defined in: -# https://github.com/dotnet/dotnet/blob/10060d128e3f470e77265f8490f5e4f72dae738e/eng/pipelines/templates/stages/vmr-build.yml#L27-L38 -# -# For repos that do not need to run the full set, you would do the following: -# -# 1. Copy this YML file to a repo-specific location, i.e. outside of eng/common. -# -# 2. Add `verifications` parameter to VMR template reference -# -# Examples: -# - For source-build stage 1 verification, add the following: -# verifications: [ "source-build-stage1" ] -# -# - For Windows only verifications, add the following: -# verifications: [ "unified-build-windows-x64", "unified-build-windows-x86" ] - -trigger: none -pr: none - -variables: -- template: /eng/common/templates/variables/pool-providers.yml@self - -- name: skipComponentGovernanceDetection # we run CG on internal builds only - value: true - -- name: Codeql.Enabled # we run CodeQL on internal builds only - value: false - -resources: - repositories: - - repository: vmr - type: github - name: dotnet/dotnet - endpoint: dotnet - -stages: -- template: /eng/pipelines/templates/stages/vmr-build.yml@vmr - parameters: - isBuiltFromVmr: false - scope: lite diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 06b44de7870..9b3ad8840fd 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -65,8 +65,10 @@ $ErrorActionPreference = 'Stop' # Base-64 encoded SAS token that has permission to storage container described by $runtimeSourceFeed [string]$runtimeSourceFeedKey = if (Test-Path variable:runtimeSourceFeedKey) { $runtimeSourceFeedKey } else { $null } -# True when the build is running within the VMR. -[bool]$fromVMR = if (Test-Path variable:fromVMR) { $fromVMR } else { $false } +# True if the build is a product build +[bool]$productBuild = if (Test-Path variable:productBuild) { $productBuild } else { $false } + +[String[]]$properties = if (Test-Path variable:properties) { $properties } else { @() } function Create-Directory ([string[]] $path) { New-Item -Path $path -Force -ItemType 'Directory' | Out-Null @@ -257,20 +259,7 @@ function Retry($downloadBlock, $maxRetries = 5) { function GetDotNetInstallScript([string] $dotnetRoot) { $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1' - $shouldDownload = $false - if (!(Test-Path $installScript)) { - $shouldDownload = $true - } else { - # Check if the script is older than 30 days - $fileAge = (Get-Date) - (Get-Item $installScript).LastWriteTime - if ($fileAge.Days -gt 30) { - Write-Host "Existing install script is too old, re-downloading..." - $shouldDownload = $true - } - } - - if ($shouldDownload) { Create-Directory $dotnetRoot $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit $uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" @@ -394,8 +383,8 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # If the version of msbuild is going to be xcopied, # use this version. Version matches a package here: - # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.13.0 - $defaultXCopyMSBuildVersion = '17.13.0' + # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.12.0 + $defaultXCopyMSBuildVersion = '17.12.0' if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { @@ -544,8 +533,7 @@ function LocateVisualStudio([object]$vsRequirements = $null){ if (Get-Member -InputObject $GlobalJson.tools -Name 'vswhere') { $vswhereVersion = $GlobalJson.tools.vswhere } else { - # keep this in sync with the VSWhereVersion in DefaultVersions.props - $vswhereVersion = '3.1.7' + $vswhereVersion = '2.5.2' } $vsWhereDir = Join-Path $ToolsDir "vswhere\$vswhereVersion" @@ -553,8 +541,7 @@ function LocateVisualStudio([object]$vsRequirements = $null){ if (!(Test-Path $vsWhereExe)) { Create-Directory $vsWhereDir - Write-Host "Downloading vswhere $vswhereVersion" - $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit + Write-Host 'Downloading vswhere' Retry({ Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -OutFile $vswhereExe }) @@ -617,7 +604,14 @@ function InitializeBuildTool() { } $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') - $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'net' } + # Use override if it exists - commonly set by source-build + if ($null -eq $env:_OverrideArcadeInitializeBuildToolFramework) { + $initializeBuildToolFramework="net9.0" + } else { + $initializeBuildToolFramework=$env:_OverrideArcadeInitializeBuildToolFramework + } + + $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = $initializeBuildToolFramework } } elseif ($msbuildEngine -eq "vs") { try { $msbuildPath = InitializeVisualStudioMSBuild -install:$restore @@ -626,7 +620,7 @@ function InitializeBuildTool() { ExitWithExitCode 1 } - $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "netframework"; ExcludePrereleaseVS = $excludePrereleaseVS } + $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "net472"; ExcludePrereleaseVS = $excludePrereleaseVS } } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unexpected value of -msbuildEngine: '$msbuildEngine'." ExitWithExitCode 1 @@ -659,6 +653,7 @@ function GetNuGetPackageCachePath() { $env:NUGET_PACKAGES = Join-Path $env:UserProfile '.nuget\packages\' } else { $env:NUGET_PACKAGES = Join-Path $RepoRoot '.packages\' + $env:RESTORENOHTTPCACHE = $true } } @@ -780,13 +775,26 @@ function MSBuild() { $toolsetBuildProject = InitializeToolset $basePath = Split-Path -parent $toolsetBuildProject - $selectedPath = Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll') - + $possiblePaths = @( + # new scripts need to work with old packages, so we need to look for the old names/versions + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.Arcade.Sdk.dll')), + (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.Arcade.Sdk.dll')), + (Join-Path $basePath (Join-Path net8.0 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path net8.0 'Microsoft.DotNet.Arcade.Sdk.dll')) + ) + $selectedPath = $null + foreach ($path in $possiblePaths) { + if (Test-Path $path -PathType Leaf) { + $selectedPath = $path + break + } + } if (-not $selectedPath) { - Write-PipelineTelemetryError -Category 'Build' -Message "Unable to find arcade sdk logger assembly: $selectedPath" + Write-PipelineTelemetryError -Category 'Build' -Message 'Unable to find arcade sdk logger assembly.' ExitWithExitCode 1 } - $args += "/logger:$selectedPath" } @@ -849,8 +857,8 @@ function MSBuild-Core() { } # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR build. - if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) { + # Skip this when the build is a child of the VMR orchestrator build. + if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$productBuild -and -not($properties -like "*DotNetBuildRepo=true*")) { Write-PipelineSetResult -Result "Failed" -Message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error diff --git a/eng/common/tools.sh b/eng/common/tools.sh index c1841c9dfd0..01b09b65796 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -5,9 +5,6 @@ # CI mode - set to true on CI server for PR validation build or official build. ci=${ci:-false} -# Build mode -source_build=${source_build:-false} - # Set to true to use the pipelines logger which will enable Azure logging output. # https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md # This flag is meant as a temporary opt-opt for the feature while validate it across @@ -61,8 +58,7 @@ use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} # True to use global NuGet cache instead of restoring packages to repository-local directory. -# Keep in sync with NuGetPackageroot in Arcade SDK's RepositoryLayout.props. -if [[ "$ci" == true || "$source_build" == true ]]; then +if [[ "$ci" == true ]]; then use_global_nuget_cache=${use_global_nuget_cache:-false} else use_global_nuget_cache=${use_global_nuget_cache:-true} @@ -72,8 +68,8 @@ fi runtime_source_feed=${runtime_source_feed:-''} runtime_source_feed_key=${runtime_source_feed_key:-''} -# True when the build is running within the VMR. -from_vmr=${from_vmr:-false} +# True if the build is a product build +product_build=${product_build:-false} # Resolve any symlinks in the given path. function ResolvePath { @@ -300,29 +296,8 @@ function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" local install_script_url="https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" - local timestamp_file="$root/.dotnet-install.timestamp" - local should_download=false if [[ ! -a "$install_script" ]]; then - should_download=true - elif [[ -f "$timestamp_file" ]]; then - # Check if the script is older than 30 days using timestamp file - local download_time=$(cat "$timestamp_file" 2>/dev/null || echo "0") - local current_time=$(date +%s) - local age_seconds=$((current_time - download_time)) - - # 30 days = 30 * 24 * 60 * 60 = 2592000 seconds - if [[ $age_seconds -gt 2592000 ]]; then - echo "Existing install script is too old, re-downloading..." - should_download=true - fi - else - # No timestamp file exists, assume script is old and re-download - echo "No timestamp found for existing install script, re-downloading..." - should_download=true - fi - - if [[ "$should_download" == true ]]; then mkdir -p "$root" echo "Downloading '$install_script_url'" @@ -349,9 +324,6 @@ function GetDotNetInstallScript { ExitWithExitCode $exit_code } fi - - # Create timestamp file to track download time in seconds from epoch - date +%s > "$timestamp_file" fi # return value _GetDotNetInstallScript="$install_script" @@ -367,14 +339,22 @@ function InitializeBuildTool { # return values _InitializeBuildTool="$_InitializeDotNetCli/dotnet" _InitializeBuildToolCommand="msbuild" + # use override if it exists - commonly set by source-build + if [[ "${_OverrideArcadeInitializeBuildToolFramework:-x}" == "x" ]]; then + _InitializeBuildToolFramework="net9.0" + else + _InitializeBuildToolFramework="${_OverrideArcadeInitializeBuildToolFramework}" + fi } +# Set RestoreNoHttpCache as a workaround for https://github.com/NuGet/Home/issues/3116 function GetNuGetPackageCachePath { if [[ -z ${NUGET_PACKAGES:-} ]]; then if [[ "$use_global_nuget_cache" == true ]]; then export NUGET_PACKAGES="$HOME/.nuget/packages/" else export NUGET_PACKAGES="$repo_root/.packages/" + export RESTORENOHTTPCACHE=true fi fi @@ -471,13 +451,25 @@ function MSBuild { fi local toolset_dir="${_InitializeToolset%/*}" - local selectedPath="$toolset_dir/net/Microsoft.DotNet.ArcadeLogging.dll" - + # new scripts need to work with old packages, so we need to look for the old names/versions + local selectedPath= + local possiblePaths=() + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/net8.0/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/net8.0/Microsoft.DotNet.Arcade.Sdk.dll" ) + for path in "${possiblePaths[@]}"; do + if [[ -f $path ]]; then + selectedPath=$path + break + fi + done if [[ -z "$selectedPath" ]]; then - Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly: $selectedPath" + Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly." ExitWithExitCode 1 fi - args+=( "-logger:$selectedPath" ) fi @@ -514,8 +506,8 @@ function MSBuild-Core { echo "Build failed with exit code $exit_code. Check errors above." # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR build. - if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true ]]; then + # Skip this when the build is a child of the VMR orchestrator build. + if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$product_build" != true && "$properties" != *"DotNetBuildRepo=true"* ]]; then Write-PipelineSetResult -result "Failed" -message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error @@ -538,13 +530,6 @@ function GetDarc { fi "$eng_root/common/darc-init.sh" --toolpath "$darc_path" $version - darc_tool="$darc_path/darc" -} - -# Returns a full path to an Arcade SDK task project file. -function GetSdkTaskProject { - taskName=$1 - echo "$(dirname $_InitializeToolset)/SdkTasks/$taskName.proj" } ResolvePath "${BASH_SOURCE[0]}" diff --git a/eng/common/vmr-sync.ps1 b/eng/common/vmr-sync.ps1 deleted file mode 100644 index 97302f3205b..00000000000 --- a/eng/common/vmr-sync.ps1 +++ /dev/null @@ -1,138 +0,0 @@ -<# -.SYNOPSIS - -This script is used for synchronizing the current repository into a local VMR. -It pulls the current repository's code into the specified VMR directory for local testing or -Source-Build validation. - -.DESCRIPTION - -The tooling used for synchronization will clone the VMR repository into a temporary folder if -it does not already exist. These clones can be reused in future synchronizations, so it is -recommended to dedicate a folder for this to speed up re-runs. - -.EXAMPLE - Synchronize current repository into a local VMR: - ./vmr-sync.ps1 -vmrDir "$HOME/repos/dotnet" -tmpDir "$HOME/repos/tmp" - -.PARAMETER tmpDir -Required. Path to the temporary folder where repositories will be cloned - -.PARAMETER vmrBranch -Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch - -.PARAMETER azdevPat -Optional. Azure DevOps PAT to use for cloning private repositories. - -.PARAMETER vmrDir -Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder - -.PARAMETER debugOutput -Optional. Enables debug logging in the darc vmr command. - -.PARAMETER ci -Optional. Denotes that the script is running in a CI environment. -#> -param ( - [Parameter(Mandatory=$true, HelpMessage="Path to the temporary folder where repositories will be cloned")] - [string][Alias('t', 'tmp')]$tmpDir, - [string][Alias('b', 'branch')]$vmrBranch, - [string]$remote, - [string]$azdevPat, - [string][Alias('v', 'vmr')]$vmrDir, - [switch]$ci, - [switch]$debugOutput -) - -function Fail { - Write-Host "> $($args[0])" -ForegroundColor 'Red' -} - -function Highlight { - Write-Host "> $($args[0])" -ForegroundColor 'Cyan' -} - -$verbosity = 'verbose' -if ($debugOutput) { - $verbosity = 'debug' -} -# Validation - -if (-not $tmpDir) { - Fail "Missing -tmpDir argument. Please specify the path to the temporary folder where the repositories will be cloned" - exit 1 -} - -# Sanitize the input - -if (-not $vmrDir) { - $vmrDir = Join-Path $tmpDir 'dotnet' -} - -if (-not (Test-Path -Path $tmpDir -PathType Container)) { - New-Item -ItemType Directory -Path $tmpDir | Out-Null -} - -# Prepare the VMR - -if (-not (Test-Path -Path $vmrDir -PathType Container)) { - Highlight "Cloning 'dotnet/dotnet' into $vmrDir.." - git clone https://github.com/dotnet/dotnet $vmrDir - - if ($vmrBranch) { - git -C $vmrDir switch -c $vmrBranch - } -} -else { - if ((git -C $vmrDir diff --quiet) -eq $false) { - Fail "There are changes in the working tree of $vmrDir. Please commit or stash your changes" - exit 1 - } - - if ($vmrBranch) { - Highlight "Preparing $vmrDir" - git -C $vmrDir checkout $vmrBranch - git -C $vmrDir pull - } -} - -Set-StrictMode -Version Latest - -# Prepare darc - -Highlight 'Installing .NET, preparing the tooling..' -. .\eng\common\tools.ps1 -$dotnetRoot = InitializeDotNetCli -install:$true -$darc = Get-Darc -$dotnet = "$dotnetRoot\dotnet.exe" - -Highlight "Starting the synchronization of VMR.." - -# Synchronize the VMR -$darcArgs = ( - "vmr", "forwardflow", - "--tmp", $tmpDir, - "--$verbosity", - $vmrDir -) - -if ($ci) { - $darcArgs += ("--ci") -} - -if ($azdevPat) { - $darcArgs += ("--azdev-pat", $azdevPat) -} - -& "$darc" $darcArgs - -if ($LASTEXITCODE -eq 0) { - Highlight "Synchronization succeeded" -} -else { - Fail "Synchronization of repo to VMR failed!" - Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." - Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." - Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." - exit 1 -} diff --git a/eng/common/vmr-sync.sh b/eng/common/vmr-sync.sh deleted file mode 100644 index 44239e331c0..00000000000 --- a/eng/common/vmr-sync.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/bin/bash - -### This script is used for synchronizing the current repository into a local VMR. -### It pulls the current repository's code into the specified VMR directory for local testing or -### Source-Build validation. -### -### The tooling used for synchronization will clone the VMR repository into a temporary folder if -### it does not already exist. These clones can be reused in future synchronizations, so it is -### recommended to dedicate a folder for this to speed up re-runs. -### -### USAGE: -### Synchronize current repository into a local VMR: -### ./vmr-sync.sh --tmp "$HOME/repos/tmp" "$HOME/repos/dotnet" -### -### Options: -### -t, --tmp, --tmp-dir PATH -### Required. Path to the temporary folder where repositories will be cloned -### -### -b, --branch, --vmr-branch BRANCH_NAME -### Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch -### -### --debug -### Optional. Turns on the most verbose logging for the VMR tooling -### -### --remote name:URI -### Optional. Additional remote to use during the synchronization -### This can be used to synchronize to a commit from a fork of the repository -### Example: 'runtime:https://github.com/yourfork/runtime' -### -### --azdev-pat -### Optional. Azure DevOps PAT to use for cloning private repositories. -### -### -v, --vmr, --vmr-dir PATH -### Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder - -source="${BASH_SOURCE[0]}" - -# resolve $source until the file is no longer a symlink -while [[ -h "$source" ]]; do - scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the - # symlink file was located - [[ $source != /* ]] && source="$scriptroot/$source" -done -scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" - -function print_help () { - sed -n '/^### /,/^$/p' "$source" | cut -b 5- -} - -COLOR_RED=$(tput setaf 1 2>/dev/null || true) -COLOR_CYAN=$(tput setaf 6 2>/dev/null || true) -COLOR_CLEAR=$(tput sgr0 2>/dev/null || true) -COLOR_RESET=uniquesearchablestring -FAILURE_PREFIX='> ' - -function fail () { - echo "${COLOR_RED}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_RED}}${COLOR_CLEAR}" >&2 -} - -function highlight () { - echo "${COLOR_CYAN}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_CYAN}}${COLOR_CLEAR}" -} - -tmp_dir='' -vmr_dir='' -vmr_branch='' -additional_remotes='' -verbosity=verbose -azdev_pat='' -ci=false - -while [[ $# -gt 0 ]]; do - opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" - case "$opt" in - -t|--tmp|--tmp-dir) - tmp_dir=$2 - shift - ;; - -v|--vmr|--vmr-dir) - vmr_dir=$2 - shift - ;; - -b|--branch|--vmr-branch) - vmr_branch=$2 - shift - ;; - --remote) - additional_remotes="$additional_remotes $2" - shift - ;; - --azdev-pat) - azdev_pat=$2 - shift - ;; - --ci) - ci=true - ;; - -d|--debug) - verbosity=debug - ;; - -h|--help) - print_help - exit 0 - ;; - *) - fail "Invalid argument: $1" - print_help - exit 1 - ;; - esac - - shift -done - -# Validation - -if [[ -z "$tmp_dir" ]]; then - fail "Missing --tmp-dir argument. Please specify the path to the temporary folder where the repositories will be cloned" - exit 1 -fi - -# Sanitize the input - -if [[ -z "$vmr_dir" ]]; then - vmr_dir="$tmp_dir/dotnet" -fi - -if [[ ! -d "$tmp_dir" ]]; then - mkdir -p "$tmp_dir" -fi - -if [[ "$verbosity" == "debug" ]]; then - set -x -fi - -# Prepare the VMR - -if [[ ! -d "$vmr_dir" ]]; then - highlight "Cloning 'dotnet/dotnet' into $vmr_dir.." - git clone https://github.com/dotnet/dotnet "$vmr_dir" - - if [[ -n "$vmr_branch" ]]; then - git -C "$vmr_dir" switch -c "$vmr_branch" - fi -else - if ! git -C "$vmr_dir" diff --quiet; then - fail "There are changes in the working tree of $vmr_dir. Please commit or stash your changes" - exit 1 - fi - - if [[ -n "$vmr_branch" ]]; then - highlight "Preparing $vmr_dir" - git -C "$vmr_dir" checkout "$vmr_branch" - git -C "$vmr_dir" pull - fi -fi - -set -e - -# Prepare darc - -highlight 'Installing .NET, preparing the tooling..' -source "./eng/common/tools.sh" -InitializeDotNetCli true -GetDarc -dotnetDir=$( cd ./.dotnet/; pwd -P ) -dotnet=$dotnetDir/dotnet - -highlight "Starting the synchronization of VMR.." -set +e - -if [[ -n "$additional_remotes" ]]; then - additional_remotes="--additional-remotes $additional_remotes" -fi - -if [[ -n "$azdev_pat" ]]; then - azdev_pat="--azdev-pat $azdev_pat" -fi - -ci_arg='' -if [[ "$ci" == "true" ]]; then - ci_arg="--ci" -fi - -# Synchronize the VMR - -export DOTNET_ROOT="$dotnetDir" - -"$darc_tool" vmr forwardflow \ - --tmp "$tmp_dir" \ - $azdev_pat \ - --$verbosity \ - $ci_arg \ - $additional_remotes \ - "$vmr_dir" - -if [[ $? == 0 ]]; then - highlight "Synchronization succeeded" -else - fail "Synchronization of repo to VMR failed!" - fail "'$vmr_dir' is left in its last state (re-run of this script will reset it)." - fail "Please inspect the logs which contain path to the failing patch file (use --debug to get all the details)." - fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." - exit 1 -fi diff --git a/global.json b/global.json index fd61e068f3a..b0500c7f83e 100644 --- a/global.json +++ b/global.json @@ -18,7 +18,7 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25476.2", - "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25476.2" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25515.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25515.2" } } From d39da143f85933b270b16bae1dd8bf483ef0e30d Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:06:24 +0200 Subject: [PATCH 370/472] Extend service discovery to support Consul-based DNS lookups: (#6914) * Extend service discovery to support Consul-based DNS lookups: - Enable specifying how to construct the DNS SRV query - Enable adding the Consul DNS server host/port * Address review feedback * Review feedback: Move DnsResolverOptions to parent namespace * Review feedback: Use options pattern for DnsResolverOptions * Simplify Configure calls, fix broken tests --- .../DnsResolverOptions.cs | 33 +++++++ .../DnsResolverOptionsValidator.cs | 41 +++++++++ .../DnsSrvServiceEndpointProviderFactory.cs | 15 ++- .../DnsSrvServiceEndpointProviderOptions.cs | 5 + .../Resolver/DnsResolver.cs | 46 +++++----- .../Resolver/NetworkInfo.cs | 6 +- .../Resolver/ResolvConf.cs | 10 +- .../Resolver/ResolverOptions.cs | 31 ------- ...DiscoveryDnsServiceCollectionExtensions.cs | 67 ++++++++++---- .../Fuzzers/DnsResponseFuzzer.cs | 7 +- .../DnsServicePublicApiTests.cs | 17 +++- .../Resolver/LoopbackDnsTestBase.cs | 12 ++- .../Resolver/ResolvConfTests.cs | 6 +- .../Resolver/RetryTests.cs | 6 +- .../Resolver/TcpFailoverTests.cs | 4 +- ...veryDnsServiceCollectionExtensionsTests.cs | 92 +++++++++++++++++++ .../YarpServiceDiscoveryTests.cs | 14 +-- 17 files changed, 303 insertions(+), 109 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs new file mode 100644 index 00000000000..0cc256bd3c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides configuration options for DNS resolution, including server endpoints, retry attempts, and timeout settings. +/// +public class DnsResolverOptions +{ + /// + /// Gets or sets the collection of server endpoints used for network connections. + /// + public IList Servers { get; set; } = new List(); + + /// + /// Gets or sets the maximum number of attempts per server. + /// + public int MaxAttempts { get; set; } = 2; + + /// + /// Gets or sets the maximum duration per attempt to wait before timing out. + /// + /// + /// The maximum time for resolving a query is * count * . + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3); + + // override for testing purposes + internal Func, int, int>? _transportOverride; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs new file mode 100644 index 00000000000..d61da5e5e0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs @@ -0,0 +1,41 @@ +// 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.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed class DnsResolverOptionsValidator : IValidateOptions +{ + // CancellationTokenSource.CancelAfter has a maximum timeout of Int32.MaxValue milliseconds. + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + + public ValidateOptionsResult Validate(string? name, DnsResolverOptions options) + { + if (options.Servers is null) + { + return ValidateOptionsResult.Fail($"{nameof(options.Servers)} must not be null."); + } + + if (options.MaxAttempts < 1) + { + return ValidateOptionsResult.Fail($"{nameof(options.MaxAttempts)} must be one or greater."); + } + + if (options.Timeout != Timeout.InfiniteTimeSpan) + { + if (options.Timeout <= TimeSpan.Zero) + { + return ValidateOptionsResult.Fail($"{nameof(options.Timeout)} must not be negative or zero."); + } + + if (options.Timeout > s_maxTimeout) + { + return ValidateOptionsResult.Fail($"{nameof(options.Timeout)} must not be greater than {s_maxTimeout.TotalMilliseconds} milliseconds."); + } + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 57820560a63..ef593a7340c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -22,6 +22,8 @@ internal sealed partial class DnsSrvServiceEndpointProviderFactory( /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { + var optionsValue = options.CurrentValue; + // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md // SRV records are available for headless services with named ports. @@ -30,19 +32,26 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". - if (string.IsNullOrWhiteSpace(_querySuffix)) + if (optionsValue.ServiceDomainNameCallback == null && string.IsNullOrWhiteSpace(_querySuffix)) { DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); provider = default; return false; } - var portName = query.EndpointName ?? "default"; - var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; + var srvQuery = optionsValue.ServiceDomainNameCallback != null + ? optionsValue.ServiceDomainNameCallback(query) + : DefaultServiceDomainNameCallback(query, optionsValue); provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } + private static string DefaultServiceDomainNameCallback(ServiceEndpointQuery query, DnsSrvServiceEndpointProviderOptions options) + { + var portName = query.EndpointName ?? "default"; + return $"_{portName}._tcp.{query.ServiceName}.{options.QuerySuffix}"; + } + private static string? GetKubernetesHostDomain() { // Check that we are running in Kubernetes first. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index c908c56d770..c1d64136cc9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -36,6 +36,11 @@ public class DnsSrvServiceEndpointProviderOptions /// public string? QuerySuffix { get; set; } + /// + /// Gets or sets a delegate that generates a DNS SRV query from a specified instance. + /// + public Func? ServiceDomainNameCallback { get; set; } + /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs index bc290c6b907..511e8fdb1c9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; @@ -19,43 +20,40 @@ internal sealed partial class DnsResolver : IDnsResolver, IDisposable private const int IPv4Length = 4; private const int IPv6Length = 16; - // CancellationTokenSource.CancelAfter has a maximum timeout of Int32.MaxValue milliseconds. - private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); - private bool _disposed; - private readonly ResolverOptions _options; + private readonly DnsResolverOptions _options; private readonly CancellationTokenSource _pendingRequestsCts = new(); private readonly TimeProvider _timeProvider; private readonly ILogger _logger; - public DnsResolver(TimeProvider timeProvider, ILogger logger) : this(timeProvider, logger, OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ? ResolvConf.GetOptions() : NetworkInfo.GetOptions()) - { - } - - internal DnsResolver(TimeProvider timeProvider, ILogger logger, ResolverOptions options) + public DnsResolver(TimeProvider timeProvider, ILogger logger, IOptions options) { _timeProvider = timeProvider; _logger = logger; - _options = options; - Debug.Assert(_options.Servers.Count > 0); + _options = options.Value; - if (options.Timeout != Timeout.InfiniteTimeSpan) + if (_options.Servers.Count == 0) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); - ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Timeout, s_maxTimeout); - } - } - - internal DnsResolver(ResolverOptions options) : this(TimeProvider.System, NullLogger.Instance, options) - { - } + foreach (var server in OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() + ? ResolvConf.GetServers() + : NetworkInfo.GetServers()) + { + _options.Servers.Add(server); + } - internal DnsResolver(IEnumerable servers) : this(new ResolverOptions(servers.ToArray())) - { + if (_options.Servers.Count == 0) + { + throw new ArgumentException("At least one DNS server is required.", nameof(options)); + } + } } - internal DnsResolver(IPEndPoint server) : this(new ResolverOptions(server)) + // This constructor is for unit testing only. Does not auto-add system DNS servers. + internal DnsResolver(DnsResolverOptions options) { + _timeProvider = TimeProvider.System; + _logger = NullLogger.Instance; + _options = options; } public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) @@ -365,7 +363,7 @@ internal struct SendQueryResult { IPEndPoint serverEndPoint = _options.Servers[index]; - for (int attempt = 1; attempt <= _options.Attempts; attempt++) + for (int attempt = 1; attempt <= _options.MaxAttempts; attempt++) { DnsResponse response = default; try diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs index c2ef13f922e..24b5155a1c8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs @@ -8,8 +8,8 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; internal static class NetworkInfo { - // basic option to get DNS serves via NetworkInfo. We may get it directly later via proper APIs. - public static ResolverOptions GetOptions() + // basic option to get DNS servers via NetworkInfo. We may get it directly later via proper APIs. + public static IList GetServers() { List servers = new List(); @@ -31,6 +31,6 @@ public static ResolverOptions GetOptions() } } - return new ResolverOptions(servers); + return servers; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs index fbfdc5ae027..de68e88c18d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs @@ -10,12 +10,12 @@ internal static class ResolvConf { [SupportedOSPlatform("linux")] [SupportedOSPlatform("osx")] - public static ResolverOptions GetOptions() + public static IList GetServers() { - return GetOptions(new StreamReader("/etc/resolv.conf")); + return GetServers(new StreamReader("/etc/resolv.conf")); } - public static ResolverOptions GetOptions(TextReader reader) + public static IList GetServers(TextReader reader) { List serverList = new(); @@ -40,9 +40,9 @@ public static ResolverOptions GetOptions(TextReader reader) if (serverList.Count == 0) { // If no nameservers are configured, fall back to the default behavior of using the system resolver configuration. - return NetworkInfo.GetOptions(); + return NetworkInfo.GetServers(); } - return new ResolverOptions(serverList); + return serverList; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs deleted file mode 100644 index 51d03f64bfd..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs +++ /dev/null @@ -1,31 +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.Net; - -namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; - -internal sealed class ResolverOptions -{ - public IReadOnlyList Servers; - public int Attempts = 2; - public TimeSpan Timeout = TimeSpan.FromSeconds(3); - - // override for testing purposes - internal Func, int, int>? _transportOverride; - - public ResolverOptions(IReadOnlyList servers) - { - if (servers.Count == 0) - { - throw new ArgumentException("At least one DNS server is required.", nameof(servers)); - } - - Servers = servers; - } - - public ResolverOptions(IPEndPoint server) - { - Servers = new IPEndPoint[] { server }; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 42f220445b1..313e68b3b8a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; @@ -59,24 +60,10 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC services.AddSingleton(); var options = services.AddOptions(); - options.Configure(o => configureOptions?.Invoke(o)); + options.Configure(configureOptions); + return services; - static bool GetDnsClientFallbackFlag() - { - if (AppContext.TryGetSwitch("Microsoft.Extensions.ServiceDiscovery.Dns.UseDnsClientFallback", out var value)) - { - return value; - } - - var envVar = Environment.GetEnvironmentVariable("MICROSOFT_EXTENSIONS_SERVICE_DISCOVERY_DNS_USE_DNSCLIENT_FALLBACK"); - if (envVar is not null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1"))) - { - return true; - } - - return false; - } } /// @@ -109,9 +96,55 @@ public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceColl ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); + + if (!GetDnsClientFallbackFlag()) + { + services.TryAddSingleton(); + } + else + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } + services.AddSingleton(); var options = services.AddOptions(); - options.Configure(o => configureOptions?.Invoke(o)); + options.Configure(configureOptions); + + return services; + } + + /// + /// Configures the DNS resolver used for service discovery. + /// + /// The service collection. + /// The DNS resolver options. + /// The provided . + public static IServiceCollection ConfigureDnsResolver(this IServiceCollection services, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + + var options = services.AddOptions(); + options.Configure(configureOptions); + services.AddTransient, DnsResolverOptionsValidator>(); return services; } + + private static bool GetDnsClientFallbackFlag() + { + if (AppContext.TryGetSwitch("Microsoft.Extensions.ServiceDiscovery.Dns.UseDnsClientFallback", out var value)) + { + return value; + } + + var envVar = Environment.GetEnvironmentVariable("MICROSOFT_EXTENSIONS_SERVICE_DISCOVERY_DNS_USE_DNSCLIENT_FALLBACK"); + if (envVar is not null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1"))) + { + return true; + } + + return false; + } + } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs index 1b180d74b9d..2e4658e3ba2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs @@ -19,10 +19,11 @@ public void FuzzTarget(ReadOnlySpan data) if (_resolver == null) { _buffer = new byte[4096]; - _resolver = new DnsResolver(new ResolverOptions(new IPEndPoint(IPAddress.Loopback, 53)) + _resolver = new DnsResolver(new DnsResolverOptions { + Servers = [new IPEndPoint(IPAddress.Loopback, 53)], Timeout = TimeSpan.FromSeconds(5), - Attempts = 1, + MaxAttempts = 1, _transportOverride = (buffer, length) => { // the first two bytes are the random transaction ID, so we keep that @@ -41,4 +42,4 @@ public void FuzzTarget(ReadOnlySpan data) Debug.Assert(task.IsCompleted, "Task should be completed synchronously"); task.GetAwaiter().GetResult(); } -} \ No newline at end of file +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs index e347deb9822..69bb6e0e510 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs @@ -68,12 +68,23 @@ public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServ } [Fact] - public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + public void ConfigureDnsResolverShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.ConfigureDnsResolver(_ => { }); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenConfigureOptionsIsNull() { IServiceCollection services = new ServiceCollection(); - Action configureOptions = null!; + Action configureOptions = null!; - var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + var action = () => services.ConfigureDnsResolver(configureOptions); var exception = Assert.Throws(action); Assert.Equal(nameof(configureOptions), exception.ParamName); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs index 14abd659029..6d2aba6cb64 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -5,6 +5,8 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Dns.Tests; using Microsoft.Extensions.Time.Testing; using Xunit.Abstractions; @@ -18,7 +20,7 @@ public abstract class LoopbackDnsTestBase : IDisposable internal LoopbackDnsServer DnsServer { get; } private readonly Lazy _resolverLazy; internal DnsResolver Resolver => _resolverLazy.Value; - internal ResolverOptions Options { get; } + internal DnsResolverOptions Options { get; } protected readonly FakeTimeProvider TimeProvider; public LoopbackDnsTestBase(ITestOutputHelper output) @@ -26,10 +28,11 @@ public LoopbackDnsTestBase(ITestOutputHelper output) Output = output; DnsServer = new(); TimeProvider = new(); - Options = new([DnsServer.DnsEndPoint]) + Options = new() { + Servers = [DnsServer.DnsEndPoint], Timeout = TimeSpan.FromSeconds(5), - Attempts = 1, + MaxAttempts = 1, }; _resolverLazy = new(InitializeResolver); } @@ -39,8 +42,7 @@ DnsResolver InitializeResolver() ServiceCollection services = new(); services.AddXunitLogging(Output); - // construct DnsResolver manually via internal constructor which accepts ResolverOptions - var resolver = new DnsResolver(TimeProvider, services.BuildServiceProvider().GetRequiredService>(), Options); + var resolver = new DnsResolver(TimeProvider, NullLogger.Instance, new OptionsWrapper(Options)); return resolver; } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs index 281ffbecd24..4c2bcadd8a5 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; public class ResolvConfTests { [Fact] - public void GetOptions() + public void GetServers() { var contents = @" nameserver 10.96.0.10 @@ -18,9 +18,9 @@ search default.svc.cluster.local svc.cluster.local cluster.local @"; var reader = new StringReader(contents); - ResolverOptions options = ResolvConf.GetOptions(reader); + var servers = ResolvConf.GetServers(reader); - IPEndPoint ipAddress = Assert.Single(options.Servers); + IPEndPoint ipAddress = Assert.Single(servers); Assert.Equal(new IPEndPoint(IPAddress.Parse("10.96.0.10"), 53), ipAddress); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs index 49985846570..3d6f3724484 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -11,7 +11,7 @@ public class RetryTests : LoopbackDnsTestBase { public RetryTests(ITestOutputHelper output) : base(output) { - Options.Attempts = 3; + Options.MaxAttempts = 3; } private Task SetupUdpProcessFunction(LoopbackDnsServer server, Func func) @@ -49,7 +49,7 @@ public async Task Retry_Simple_Success() Task t = SetupUdpProcessFunction(builder => { attempt++; - if (attempt == Options.Attempts) + if (attempt == Options.MaxAttempts) { builder.Answers.AddAddress(hostName, 3600, address); } @@ -214,7 +214,7 @@ public async Task ExhaustedRetries_FailoverToNextServer() return Task.CompletedTask; }); - Assert.Equal(Options.Attempts, primaryAttempt); + Assert.Equal(Options.MaxAttempts, primaryAttempt); Assert.Equal(1, secondaryAttempt); AddressResult res = Assert.Single(results); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs index cbdb5e282e9..b2891cfb512 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -42,7 +42,7 @@ public async Task TcpFailover_Simple_Success() public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() { string hostName = "tcp-server-closes.test"; - Options.Attempts = 1; + Options.MaxAttempts = 1; Options.Timeout = TimeSpan.FromSeconds(60); _ = DnsServer.ProcessUdpRequest(builder => @@ -66,7 +66,7 @@ public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() public async Task TcpFailover_TcpNotAvailable_EmptyResult() { string hostName = "tcp-not-available.test"; - Options.Attempts = 1; + Options.MaxAttempts = 1; Options.Timeout = TimeSpan.FromMilliseconds(100000); _ = DnsServer.ProcessUdpRequest(builder => diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000000..3631c2e8085 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs @@ -0,0 +1,92 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class ServiceDiscoveryDnsServiceCollectionExtensionsTests +{ + [Fact] + public void AddDnsServiceEndpointProviderShouldRegisterDependentServices() + { + var services = new ServiceCollection(); + services.AddDnsServiceEndpointProvider(); + + using var serviceProvider = services.BuildServiceProvider(true); + + var exception = Record.Exception(() => serviceProvider.GetServices()); + Assert.Null(exception); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderShouldRegisterDependentServices() + { + var services = new ServiceCollection(); + services.AddDnsSrvServiceEndpointProvider(); + + using var serviceProvider = services.BuildServiceProvider(true); + + var exception = Record.Exception(() => serviceProvider.GetServices()); + Assert.Null(exception); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenServersIsNull() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Servers = null!); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Servers must not be null.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenMaxAttemptsIsZero() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.MaxAttempts = 0); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("MaxAttempts must be one or greater.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenTimeoutIsZero() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Timeout = TimeSpan.Zero); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Timeout must not be negative or zero.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenTimeoutExceedsMaximum() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Timeout = TimeSpan.FromMilliseconds(1L + int.MaxValue)); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Timeout must not be greater than 2147483647 milliseconds.", exception.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index c2751823c65..d38814621c4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -1,16 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Xunit; -using Yarp.ReverseProxy.Configuration; using System.Net; using System.Net.Sockets; -using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; +using Yarp.ReverseProxy.Configuration; namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; @@ -232,7 +232,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Dns() { - DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance); + DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance, new OptionsWrapper(new DnsResolverOptions())); await using var services = new ServiceCollection() .AddSingleton(resolver) From 1dc5b3173d53e4e40c17d3775f370cd084aa0d64 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 18 Oct 2025 08:09:29 -0400 Subject: [PATCH 371/472] Update AsOpenAIResponseItems to roundtrip User AIContent ResponseItems (#6931) * Update AsOpenAIResponseItems to roundtrip User AIContent ResponseItems For Assistant and Tool messages we're directly roundtripping RawRepresentations that are ResponseItems, but not for User messages. Fix that. * Ensure ordering of AIContent-to-ResponseItem mapping Previously a ResponseItem between two TextContents, for example, would end up being yielded before the text content that came before it. Instead, yield a response item for each group between directly-mapped items. Also fix missing RawRepresentation on McpServerToolApprovalResponseContent. --- .../OpenAIResponsesChatClient.cs | 65 ++++++++++++++----- .../OpenAIConversionTests.cs | 37 +++++++++++ 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index cd7f1e46971..1a627a253f0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -202,7 +202,7 @@ internal static IEnumerable ToChatMessages(IEnumerable ToOpenAIResponseItems(IEnumerable parts = []; + // Some AIContent items may map to ResponseItems directly. Others map to ResponseContentParts that need to be grouped together. + // In order to preserve ordering, we yield ResponseItems as we find them, grouping ResponseContentParts between those yielded + // items together into their own yielded item. + + List? parts = null; + bool responseItemYielded = false; + foreach (AIContent item in input.Contents) { + // Items that directly map to a ResponseItem. + ResponseItem? directItem = item switch + { + { RawRepresentation: ResponseItem rawRep } => rawRep, + McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved), + _ => null + }; + + if (directItem is not null) + { + // Yield any parts already accumulated. + if (parts is not null) + { + yield return ResponseItem.CreateUserMessageItem(parts); + parts = null; + } + + // Now yield the directly mapped item. + yield return directItem; + + responseItemYielded = true; + continue; + } + + // Items that map into ResponseContentParts and are grouped. switch (item) { case AIContent when item.RawRepresentation is ResponseContentPart rawRep: - parts.Add(rawRep); + (parts ??= []).Add(rawRep); break; case TextContent textContent: - parts.Add(ResponseContentPart.CreateInputTextPart(textContent.Text)); + (parts ??= []).Add(ResponseContentPart.CreateInputTextPart(textContent.Text)); break; case UriContent uriContent when uriContent.HasTopLevelMediaType("image"): - parts.Add(ResponseContentPart.CreateInputImagePart(uriContent.Uri)); + (parts ??= []).Add(ResponseContentPart.CreateInputImagePart(uriContent.Uri)); break; case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): - parts.Add(ResponseContentPart.CreateInputImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType)); + (parts ??= []).Add(ResponseContentPart.CreateInputImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType)); break; case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): - parts.Add(ResponseContentPart.CreateInputFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf")); + (parts ??= []).Add(ResponseContentPart.CreateInputFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf")); break; case HostedFileContent fileContent: - parts.Add(ResponseContentPart.CreateInputFilePart(fileContent.FileId)); + (parts ??= []).Add(ResponseContentPart.CreateInputFilePart(fileContent.FileId)); break; case ErrorContent errorContent when errorContent.ErrorCode == nameof(ResponseContentPartKind.Refusal): - parts.Add(ResponseContentPart.CreateRefusalPart(errorContent.Message)); - break; - - case McpServerToolApprovalResponseContent mcpApprovalResponseContent: - handleEmptyMessage = false; - yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved); + (parts ??= []).Add(ResponseContentPart.CreateRefusalPart(errorContent.Message)); break; } } - if (parts.Count == 0 && handleEmptyMessage) + // If we haven't accumulated any parts nor have we yielded any items, manufacture an empty input text part + // to guarantee that every user message results in at least one ResponseItem. + if (parts is null && !responseItemYielded) { + parts = []; parts.Add(ResponseContentPart.CreateInputTextPart(string.Empty)); + responseItemYielded = true; } - if (parts.Count > 0) + // Final yield of any accumulated parts. + if (parts is not null) { yield return ResponseItem.CreateUserMessageItem(parts); + parts = null; } continue; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 20b9c2b92f9..844fb5618ed 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -275,6 +275,43 @@ public void AsOpenAIResponseItems_ProducesExpectedOutput() Assert.Equal("The answer is 42.", Assert.Single(m5.Content).Text); } + [Fact] + public void AsOpenAIResponseItems_RoundtripsRawRepresentation() + { + List messages = + [ + new(ChatRole.User, + [ + new TextContent("Hello, "), + new AIContent { RawRepresentation = ResponseItem.CreateWebSearchCallItem() }, + new AIContent { RawRepresentation = ResponseItem.CreateReferenceItem("123") }, + new TextContent("World"), + new TextContent("!"), + ]), + new(ChatRole.Assistant, + [ + new TextContent("Hi!"), + new AIContent { RawRepresentation = ResponseItem.CreateReasoningItem("text") }, + ]), + new(ChatRole.User, + [ + new AIContent { RawRepresentation = ResponseItem.CreateSystemMessageItem("test") }, + ]), + ]; + + var items = messages.AsOpenAIResponseItems().ToArray(); + + Assert.Equal(7, items.Length); + Assert.Equal("Hello, ", ((MessageResponseItem)items[0]).Content[0].Text); + Assert.Same(messages[0].Contents[1].RawRepresentation, items[1]); + Assert.Same(messages[0].Contents[2].RawRepresentation, items[2]); + Assert.Equal("World", ((MessageResponseItem)items[3]).Content[0].Text); + Assert.Equal("!", ((MessageResponseItem)items[3]).Content[1].Text); + Assert.Equal("Hi!", ((MessageResponseItem)items[4]).Content[0].Text); + Assert.Same(messages[1].Contents[1].RawRepresentation, items[5]); + Assert.Same(messages[2].Contents[0].RawRepresentation, items[6]); + } + [Fact] public void AsChatResponse_ConvertsOpenAIChatCompletion() { From aec14a5752a0dc104fce7eb9ba610d55ab77332c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 20 Oct 2025 16:11:31 -0400 Subject: [PATCH 372/472] Special-case AIContent returned from AIFunctionFactory.Create AIFunctions to not be serialized (#6935) * Special-case AIContent returned from AIFunctionFactory.Create AIFunctions to not be serialized They'll likely end up being serialized, anyway, by leaf IChatClients, but this gives those IChatClients the opportunity to do something different, such as by treating DataContent differently. * Update XML comments --- .../Functions/AIFunctionFactory.cs | 91 ++++++++++++++++++- .../Utilities/AIJsonUtilities.Defaults.cs | 86 +++++++++++------- .../OpenAIResponsesChatClient.cs | 26 ++++-- .../VerbatimHttpHandler.cs | 24 +++-- .../OpenAIChatClientTests.cs | 12 +-- .../Functions/AIFunctionFactoryTest.cs | 59 ++++++++++++ 6 files changed, 239 insertions(+), 59 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 7df48182e52..bd382079c80 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -99,6 +99,11 @@ public static partial class AIFunctionFactory /// /// By default, return values are serialized to using 's /// if provided, or else using . + /// However, return values whose declared type is , a derived type of , or + /// any type assignable from (e.g. AIContent[], List<AIContent>) are + /// special-cased and are not serialized: the created function returns the original instance(s) directly to enable + /// callers (such as an IChatClient) to perform type tests and implement specialized handling. If + /// is supplied, that delegate governs the behavior instead. /// Handling of return values can be overridden via . /// /// @@ -172,7 +177,9 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio /// /// /// Return values are serialized to using if provided, - /// or else using . + /// or else using . However, return values whose declared type is , a + /// derived type of , or any type assignable from are not serialized; + /// they are returned as-is to facilitate specialized handling. /// /// /// is . @@ -262,6 +269,8 @@ public static AIFunction Create(Delegate method, string? name = null, string? de /// /// By default, return values are serialized to using 's /// if provided, or else using . + /// However, return values whose declared type is , a derived type of , or + /// any type assignable from are not serialized and are instead returned directly. /// Handling of return values can be overridden via . /// /// @@ -345,7 +354,9 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// /// /// Return values are serialized to using if provided, - /// or else using . + /// or else using . However, return values whose declared type is , a + /// derived type of , or any type assignable from are returned + /// without serialization to enable specialized handling. /// /// /// is . @@ -448,6 +459,8 @@ public static AIFunction Create(MethodInfo method, object? target, string? name /// /// By default, return values are serialized to using 's /// if provided, or else using . + /// However, return values whose declared type is , a derived type of , or any type + /// assignable from are returned directly without serialization. /// Handling of return values can be overridden via . /// /// @@ -720,7 +733,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; JsonSerializerOptions = serializerOptions; ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema( - returnType, + NormalizeReturnType(returnType, serializerOptions), description: key.Method.ReturnParameter.GetCustomAttribute(inherit: true)?.Description, serializerOptions: serializerOptions, inferenceOptions: schemaOptions); @@ -978,6 +991,7 @@ static void ThrowNullServices(string parameterName) => MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition(returnType, _taskGetResult); returnType = taskResultGetter.ReturnType; + // If a MarshalResult delegate is provided, use it. if (marshalResult is not null) { return async (taskObj, cancellationToken) => @@ -988,6 +1002,18 @@ static void ThrowNullServices(string parameterName) => }; } + // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them + // specially, such as by returning content to the model/service in a manner appropriate to the content type. + if (IsAIContentRelatedType(returnType)) + { + return async (taskObj, cancellationToken) => + { + await ((Task)ThrowIfNullResult(taskObj)).ConfigureAwait(true); + return ReflectionInvoke(taskResultGetter, taskObj, null); + }; + } + + // For everything else, just serialize the result as-is. returnTypeInfo = serializerOptions.GetTypeInfo(returnType); return async (taskObj, cancellationToken) => { @@ -1004,6 +1030,7 @@ static void ThrowNullServices(string parameterName) => MethodInfo asTaskResultGetter = GetMethodFromGenericMethodDefinition(valueTaskAsTask.ReturnType, _taskGetResult); returnType = asTaskResultGetter.ReturnType; + // If a MarshalResult delegate is provided, use it. if (marshalResult is not null) { return async (taskObj, cancellationToken) => @@ -1015,6 +1042,19 @@ static void ThrowNullServices(string parameterName) => }; } + // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them + // specially, such as by returning content to the model/service in a manner appropriate to the content type. + if (IsAIContentRelatedType(returnType)) + { + return async (taskObj, cancellationToken) => + { + var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(taskObj), null)!; + await task.ConfigureAwait(true); + return ReflectionInvoke(asTaskResultGetter, task, null); + }; + } + + // For everything else, just serialize the result as-is. returnTypeInfo = serializerOptions.GetTypeInfo(returnType); return async (taskObj, cancellationToken) => { @@ -1026,13 +1066,21 @@ static void ThrowNullServices(string parameterName) => } } - // For everything else, just serialize the result as-is. + // If a MarshalResult delegate is provided, use it. if (marshalResult is not null) { Type returnTypeCopy = returnType; return (result, cancellationToken) => marshalResult(result, returnTypeCopy, cancellationToken); } + // Special-case AIContent results to not be serialized, so that IChatClients can type test and handle them + // specially, such as by returning content to the model/service in a manner appropriate to the content type. + if (IsAIContentRelatedType(returnType)) + { + return static (result, _) => new ValueTask(result); + } + + // For everything else, just serialize the result as-is. returnTypeInfo = serializerOptions.GetTypeInfo(returnType); return (result, cancellationToken) => SerializeResultAsync(result, returnTypeInfo, cancellationToken); @@ -1069,6 +1117,41 @@ private static MethodInfo GetMethodFromGenericMethodDefinition(Type specializedT #endif } + private static bool IsAIContentRelatedType(Type type) => + typeof(AIContent).IsAssignableFrom(type) || + typeof(IEnumerable).IsAssignableFrom(type); + + private static Type NormalizeReturnType(Type type, JsonSerializerOptions? options) + { + options ??= AIJsonUtilities.DefaultOptions; + + if (options == AIJsonUtilities.DefaultOptions && !options.TryGetTypeInfo(type, out _)) + { + // GetTypeInfo is not polymorphic, so attempts to look up derived types will fail even if the + // base type is registered. In some cases, though, we can fall back to using interfaces + // we know we have contracts for in AIJsonUtilities.DefaultOptions where the semantics of using + // that interface will be reasonable. This should really only affect situations where + // reflection-based serialization is disabled. + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return typeof(IEnumerable); + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return typeof(IEnumerable); + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return typeof(IEnumerable); + } + } + + return type; + } + private record struct DescriptorKey( MethodInfo Method, string? Name, diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index f1a6f7b0a1e..14f5601f1b1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -75,39 +75,23 @@ private static JsonSerializerOptions CreateDefaultOptions() UseStringEnumConverter = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)] - [JsonSerializable(typeof(SpeechToTextOptions))] - [JsonSerializable(typeof(SpeechToTextClientMetadata))] - [JsonSerializable(typeof(SpeechToTextResponse))] - [JsonSerializable(typeof(SpeechToTextResponseUpdate))] - [JsonSerializable(typeof(IReadOnlyList))] - [JsonSerializable(typeof(ImageGenerationOptions))] - [JsonSerializable(typeof(ImageGenerationResponse))] - [JsonSerializable(typeof(IList))] - [JsonSerializable(typeof(IEnumerable))] - [JsonSerializable(typeof(ChatMessage[]))] - [JsonSerializable(typeof(ChatOptions))] - [JsonSerializable(typeof(EmbeddingGenerationOptions))] - [JsonSerializable(typeof(ChatClientMetadata))] - [JsonSerializable(typeof(EmbeddingGeneratorMetadata))] - [JsonSerializable(typeof(ChatResponse))] - [JsonSerializable(typeof(ChatResponseUpdate))] - [JsonSerializable(typeof(IReadOnlyList))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(IDictionary))] + + // JSON [JsonSerializable(typeof(JsonDocument))] [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(JsonNode))] [JsonSerializable(typeof(JsonObject))] [JsonSerializable(typeof(JsonValue))] [JsonSerializable(typeof(JsonArray))] - [JsonSerializable(typeof(IEnumerable))] - [JsonSerializable(typeof(char))] + + // Primitives [JsonSerializable(typeof(string))] - [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(char))] [JsonSerializable(typeof(short))] - [JsonSerializable(typeof(long))] - [JsonSerializable(typeof(uint))] [JsonSerializable(typeof(ushort))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(uint))] + [JsonSerializable(typeof(long))] [JsonSerializable(typeof(ulong))] [JsonSerializable(typeof(float))] [JsonSerializable(typeof(double))] @@ -116,19 +100,27 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(TimeSpan))] [JsonSerializable(typeof(DateTime))] [JsonSerializable(typeof(DateTimeOffset))] - [JsonSerializable(typeof(Embedding))] - [JsonSerializable(typeof(Embedding))] - [JsonSerializable(typeof(Embedding))] -#if NET - [JsonSerializable(typeof(Embedding))] -#endif - [JsonSerializable(typeof(Embedding))] - [JsonSerializable(typeof(Embedding))] - [JsonSerializable(typeof(AIContent))] + + // AIFunction [JsonSerializable(typeof(AIFunctionArguments))] - // Temporary workaround: - // These should be added in once they're no longer [Experimental] and included via [JsonDerivedType] on AIContent. + // IChatClient + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(IList))] + [JsonSerializable(typeof(ChatMessage[]))] + [JsonSerializable(typeof(ChatOptions))] + [JsonSerializable(typeof(ChatClientMetadata))] + [JsonSerializable(typeof(ChatResponse))] + [JsonSerializable(typeof(ChatResponseUpdate))] + [JsonSerializable(typeof(IReadOnlyList))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(IEnumerable))] + [JsonSerializable(typeof(AIContent))] + [JsonSerializable(typeof(IEnumerable))] + + // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] + // and are included via [JsonDerivedType] on AIContent. [JsonSerializable(typeof(FunctionApprovalRequestContent))] [JsonSerializable(typeof(FunctionApprovalResponseContent))] [JsonSerializable(typeof(McpServerToolCallContent))] @@ -136,6 +128,30 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(McpServerToolApprovalRequestContent))] [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] [JsonSerializable(typeof(ResponseContinuationToken))] + + // IEmbeddingGenerator + [JsonSerializable(typeof(EmbeddingGenerationOptions))] + [JsonSerializable(typeof(EmbeddingGeneratorMetadata))] + [JsonSerializable(typeof(Embedding))] + [JsonSerializable(typeof(Embedding))] + [JsonSerializable(typeof(Embedding))] +#if NET + [JsonSerializable(typeof(Embedding))] +#endif + [JsonSerializable(typeof(Embedding))] + [JsonSerializable(typeof(Embedding))] + + // ISpeechToTextClient + [JsonSerializable(typeof(SpeechToTextOptions))] + [JsonSerializable(typeof(SpeechToTextClientMetadata))] + [JsonSerializable(typeof(SpeechToTextResponse))] + [JsonSerializable(typeof(SpeechToTextResponseUpdate))] + [JsonSerializable(typeof(IReadOnlyList))] + + // IImageGenerator + [JsonSerializable(typeof(ImageGenerationOptions))] + [JsonSerializable(typeof(ImageGenerationResponse))] + [EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead. private sealed partial class JsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 1a627a253f0..039d04c1f8e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -760,15 +760,27 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable + // etc. + + default: + try + { + result = JsonSerializer.Serialize(resultContent.Result, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); + } + catch (NotSupportedException) + { + // If the type can't be serialized, skip it. + } + break; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimHttpHandler.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimHttpHandler.cs index dd3ea7ea199..5c7e6ee2ecb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimHttpHandler.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/VerbatimHttpHandler.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text; using System.Text.Json.Nodes; @@ -85,12 +86,24 @@ protected override async Task SendAsync(HttpRequestMessage return new() { Content = new StringContent(_expectedOutput) }; } - public static string? RemoveWhiteSpace(string? text) => - text is null ? null : - Regex.Replace(text, @"\s*", string.Empty); + [return: NotNullIfNotNull(nameof(text))] + public static string? RemoveWhiteSpace(string? text) + { + if (text is null) + { + return null; + } + + text = text.Replace("\\r", "").Replace("\\n", "").Replace("\\t", ""); + + return Regex.Replace(text, @"\s*", string.Empty); + } private static void AssertEqualNormalized(string expected, string actual) { + expected = RemoveWhiteSpace(expected); + actual = RemoveWhiteSpace(actual); + // First try to compare as JSON. JsonNode? expectedNode = null; JsonNode? actualNode = null; @@ -114,10 +127,7 @@ private static void AssertEqualNormalized(string expected, string actual) } // Legitimately may not have been JSON. Fall back to whitespace normalization. - if (RemoveWhiteSpace(expected) != RemoveWhiteSpace(actual)) - { - FailNotEqual(expected, actual); - } + FailNotEqual(expected, actual); } private static void FailNotEqual(string expected, string actual) => diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index b7765f83949..5b407e5084a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -1383,26 +1383,26 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() "tool_calls": [ { "id": "12345", + "type": "function", "function": { "name": "SayHello", "arguments": "null" - }, - "type": "function" + } }, { "id": "12346", + "type": "function", "function": { "name": "SayHi", "arguments": "null" - }, - "type": "function" + } } ] }, { "role": "tool", "tool_call_id": "12345", - "content": "Said hello" + "content": "{ \"$type\": \"text\", \"text\": \"Said hello\" }" }, { "role":"tool", @@ -1471,7 +1471,7 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() ]), new (ChatRole.Tool, [ - new FunctionResultContent("12345", "Said hello"), + new FunctionResultContent("12345", new TextContent("Said hello")), new FunctionResultContent("12346", "Said hi"), ]), new(ChatRole.Assistant, "You are great."), diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index f99f9f94bc9..8094d4230ef 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -15,10 +15,12 @@ using Xunit; #pragma warning disable IDE0004 // Remove Unnecessary Cast +#pragma warning disable S103 // Lines should not be too long #pragma warning disable S107 // Methods should not have too many parameters #pragma warning disable S2760 // Sequential tests should not check the same condition #pragma warning disable S3358 // Ternary operators should not be nested #pragma warning disable S5034 // "ValueTask" should be consumed correctly +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously namespace Microsoft.Extensions.AI; @@ -844,6 +846,63 @@ public async Task MarshalResult_TypeIsDeclaredTypeEvenWhenDerivedTypeReturned() Assert.Equal("marshalResultInvoked", result); } + [Fact] + public async Task AIContentReturnType_NotSerializedByDefault() + { + await ValidateAsync( + [ + AIFunctionFactory.Create(() => (AIContent)new TextContent("text")), + AIFunctionFactory.Create(async () => (AIContent)new TextContent("text")), + AIFunctionFactory.Create(async ValueTask () => (AIContent)new TextContent("text")), + AIFunctionFactory.Create(() => new TextContent("text")), + AIFunctionFactory.Create(async () => new TextContent("text")), + AIFunctionFactory.Create(async ValueTask () => new TextContent("text")), + ]); + + await ValidateAsync( + [ + AIFunctionFactory.Create(() => new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")), + AIFunctionFactory.Create(async () => new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")), + AIFunctionFactory.Create(async ValueTask () => new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")), + ]); + + await ValidateAsync>( + [ + AIFunctionFactory.Create(() => (IEnumerable)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async () => (IEnumerable)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async ValueTask> () => (IEnumerable)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + ]); + + await ValidateAsync( + [ + AIFunctionFactory.Create(() => (AIContent[])[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async () => (AIContent[])[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async ValueTask () => (AIContent[])[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + ]); + + await ValidateAsync>( + [ + AIFunctionFactory.Create(() => (List)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async () => (List)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async ValueTask> () => (List)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + ]); + + await ValidateAsync>( + [ + AIFunctionFactory.Create(() => (IList)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async () => (IList)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + AIFunctionFactory.Create(async ValueTask> () => (List)[new TextContent("text"), new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream")]), + ]); + + static async Task ValidateAsync(IEnumerable functions) + { + foreach (var f in functions) + { + Assert.IsAssignableFrom(await f.InvokeAsync()); + } + } + } + [Fact] public async Task AIFunctionFactory_DefaultDefaultParameter() { From 4a0bf094d8494f484cd7af92f9c715978bf45274 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 20 Oct 2025 17:19:34 -0400 Subject: [PATCH 373/472] Preserve function content in `SummarizingChatReducer` (#6908) --- .../ChatReduction/SummarizingChatReducer.cs | 107 +++++++++--- .../SummarizingChatReducerTests.cs | 156 ++++++++++++++++-- 2 files changed, 229 insertions(+), 34 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs index f097c1c9a35..28f05ed9e5f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -73,18 +74,24 @@ public async Task> ReduceAsync(IEnumerable { _ = Throw.IfNull(messages); - var summarizedConversion = SummarizedConversation.FromChatMessages(messages); - if (summarizedConversion.ShouldResummarize(_targetCount, _thresholdCount)) + var summarizedConversation = SummarizedConversation.FromChatMessages(messages); + var indexOfFirstMessageToKeep = summarizedConversation.FindIndexOfFirstMessageToKeep(_targetCount, _thresholdCount); + if (indexOfFirstMessageToKeep > 0) { - summarizedConversion = await summarizedConversion.ResummarizeAsync( - _chatClient, _targetCount, SummarizationPrompt, cancellationToken); + summarizedConversation = await summarizedConversation.ResummarizeAsync( + _chatClient, + indexOfFirstMessageToKeep, + SummarizationPrompt, + cancellationToken); } - return summarizedConversion.ToChatMessages(); + return summarizedConversation.ToChatMessages(); } + /// Represents a conversation with an optional summary. private readonly struct SummarizedConversation(string? summary, ChatMessage? systemMessage, IList unsummarizedMessages) { + /// Creates a from a list of chat messages. public static SummarizedConversation FromChatMessages(IEnumerable messages) { string? summary = null; @@ -102,7 +109,7 @@ public static SummarizedConversation FromChatMessages(IEnumerable m unsummarizedMessages.Clear(); summary = summaryValue; } - else if (!message.Contents.Any(m => m is FunctionCallContent or FunctionResultContent)) + else { unsummarizedMessages.Add(message); } @@ -111,31 +118,68 @@ public static SummarizedConversation FromChatMessages(IEnumerable m return new(summary, systemMessage, unsummarizedMessages); } - public bool ShouldResummarize(int targetCount, int thresholdCount) - => unsummarizedMessages.Count > targetCount + thresholdCount; - - public async Task ResummarizeAsync( - IChatClient chatClient, int targetCount, string summarizationPrompt, CancellationToken cancellationToken) + /// Performs summarization by calling the chat client and updating the conversation state. + public async ValueTask ResummarizeAsync( + IChatClient chatClient, int indexOfFirstMessageToKeep, string summarizationPrompt, CancellationToken cancellationToken) { - var messagesToResummarize = unsummarizedMessages.Count - targetCount; - if (messagesToResummarize <= 0) - { - // We're at or below the target count - no need to resummarize. - return this; - } + Debug.Assert(indexOfFirstMessageToKeep > 0, "Expected positive index for first message to keep."); - var summarizerChatMessages = ToSummarizerChatMessages(messagesToResummarize, summarizationPrompt); + // Generate the summary by sending unsummarized messages to the chat client + var summarizerChatMessages = ToSummarizerChatMessages(indexOfFirstMessageToKeep, summarizationPrompt); var response = await chatClient.GetResponseAsync(summarizerChatMessages, cancellationToken: cancellationToken); var newSummary = response.Text; - var lastSummarizedMessage = unsummarizedMessages[messagesToResummarize - 1]; + // Attach the summary metadata to the last message being summarized + // This is what allows us to build on previously-generated summaries + var lastSummarizedMessage = unsummarizedMessages[indexOfFirstMessageToKeep - 1]; var additionalProperties = lastSummarizedMessage.AdditionalProperties ??= []; additionalProperties[SummaryKey] = newSummary; - var newUnsummarizedMessages = unsummarizedMessages.Skip(messagesToResummarize).ToList(); + // Compute the new list of unsummarized messages + var newUnsummarizedMessages = unsummarizedMessages.Skip(indexOfFirstMessageToKeep).ToList(); return new SummarizedConversation(newSummary, systemMessage, newUnsummarizedMessages); } + /// Determines the index of the first message to keep (not summarize) based on target and threshold counts. + public int FindIndexOfFirstMessageToKeep(int targetCount, int thresholdCount) + { + var earliestAllowedIndex = unsummarizedMessages.Count - thresholdCount - targetCount; + if (earliestAllowedIndex <= 0) + { + // Not enough messages to warrant summarization + return 0; + } + + // Start at the ideal cut point (keeping exactly targetCount messages) + var indexOfFirstMessageToKeep = unsummarizedMessages.Count - targetCount; + + // Move backward to skip over function call/result content at the boundary + // We want to keep complete function call sequences together with their responses + while (indexOfFirstMessageToKeep > 0) + { + if (!unsummarizedMessages[indexOfFirstMessageToKeep - 1].Contents.Any(IsToolRelatedContent)) + { + break; + } + + indexOfFirstMessageToKeep--; + } + + // Search backward within the threshold window to find a User message + // If found, cut right before it to avoid orphaning user questions from responses + for (var i = indexOfFirstMessageToKeep; i >= earliestAllowedIndex; i--) + { + if (unsummarizedMessages[i].Role == ChatRole.User) + { + return i; + } + } + + // No User message found within threshold - use the adjusted cut point + return indexOfFirstMessageToKeep; + } + + /// Converts the summarized conversation back into a collection of chat messages. public IEnumerable ToChatMessages() { if (systemMessage is not null) @@ -154,16 +198,33 @@ public IEnumerable ToChatMessages() } } - private IEnumerable ToSummarizerChatMessages(int messagesToResummarize, string summarizationPrompt) + /// Returns whether the given relates to tool calling capabilities. + /// + /// This method returns for content types whose meaning depends on other related + /// instances in the conversation, such as function calls that require corresponding results, or other tool interactions that span + /// multiple messages. Such content should be kept together during summarization. + /// + private static bool IsToolRelatedContent(AIContent content) => content + is FunctionCallContent + or FunctionResultContent + or UserInputRequestContent + or UserInputResponseContent; + + /// Builds the list of messages to send to the chat client for summarization. + private IEnumerable ToSummarizerChatMessages(int indexOfFirstMessageToKeep, string summarizationPrompt) { if (summary is not null) { yield return new ChatMessage(ChatRole.Assistant, summary); } - for (var i = 0; i < messagesToResummarize; i++) + for (var i = 0; i < indexOfFirstMessageToKeep; i++) { - yield return unsummarizedMessages[i]; + var message = unsummarizedMessages[i]; + if (!message.Contents.Any(IsToolRelatedContent)) + { + yield return message; + } } yield return new ChatMessage(ChatRole.System, summarizationPrompt); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs index 985b097ece8..8d5901996aa 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs @@ -84,27 +84,145 @@ public async Task ReduceAsync_PreservesSystemMessage() } [Fact] - public async Task ReduceAsync_IgnoresFunctionCallsAndResults() + public async Task ReduceAsync_PreservesCompleteToolCallSequence() { using var chatClient = new TestChatClient(); - var reducer = new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0); + + // Target 2 messages, but this would split a function call sequence + var reducer = new SummarizingChatReducer(chatClient, targetCount: 2, threshold: 0); List messages = [ + new ChatMessage(ChatRole.User, "What's the time?"), + new ChatMessage(ChatRole.Assistant, "Let me check"), new ChatMessage(ChatRole.User, "What's the weather?"), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" })]), - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), - new ChatMessage(ChatRole.Assistant, "The weather in Seattle is sunny and 72°F."), - new ChatMessage(ChatRole.User, "Thanks!"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather"), new TestUserInputRequestContent("uir1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny")]), + new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir1")]), + new ChatMessage(ChatRole.Assistant, "It's sunny"), ]; + chatClient.GetResponseAsyncCallback = (msgs, _, _) => + { + Assert.DoesNotContain(msgs, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Asked about time"))); + }; + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); - // Function calls/results should be ignored, which means there aren't enough messages to generate a summary. + // Should have: summary + function call + function result + user input response + last reply + Assert.Equal(5, resultList.Count); + + // Verify the complete sequence is preserved + Assert.Collection(resultList, + m => Assert.Contains("Asked about time", m.Text), + m => + { + Assert.Contains(m.Contents, c => c is FunctionCallContent); + Assert.Contains(m.Contents, c => c is TestUserInputRequestContent); + }, + m => Assert.Contains(m.Contents, c => c is FunctionResultContent), + m => Assert.Contains(m.Contents, c => c is TestUserInputResponseContent), + m => Assert.Contains("sunny", m.Text)); + } + + [Fact] + public async Task ReduceAsync_PreservesUserMessageWhenWithinThreshold() + { + using var chatClient = new TestChatClient(); + + // Target 3 messages with threshold of 2 + // This allows us to keep anywhere from 3 to 5 messages + var reducer = new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 2); + + List messages = + [ + new ChatMessage(ChatRole.User, "First question"), + new ChatMessage(ChatRole.Assistant, "First answer"), + new ChatMessage(ChatRole.User, "Second question"), + new ChatMessage(ChatRole.Assistant, "Second answer"), + new ChatMessage(ChatRole.User, "Third question"), + new ChatMessage(ChatRole.Assistant, "Third answer"), + ]; + + chatClient.GetResponseAsyncCallback = (msgs, _, _) => + { + var msgList = msgs.ToList(); + + // Should summarize messages 0-1 (First question and answer) + // The reducer should find the User message at index 2 within the threshold + Assert.Equal(3, msgList.Count); // 2 messages to summarize + system prompt + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of first exchange"))); + }; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); var resultList = result.ToList(); - Assert.Equal(3, resultList.Count); // Function calls get removed in the summarized chat. - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent)); - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent)); + + // Should have: summary + 4 kept messages (from "Second question" onward) + Assert.Equal(5, resultList.Count); + + // Verify the summary is first + Assert.Contains("Summary", resultList[0].Text); + + // Verify we kept the User message at index 2 and everything after + Assert.Collection(resultList.Skip(1), + m => Assert.Contains("Second question", m.Text), + m => Assert.Contains("Second answer", m.Text), + m => Assert.Contains("Third question", m.Text), + m => Assert.Contains("Third answer", m.Text)); + } + + [Fact] + public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() + { + using var chatClient = new TestChatClient(); + + // Target 3 messages - this will cause function calls in older messages to be summarized (excluded) + // while function calls in recent messages are kept + var reducer = new SummarizingChatReducer(chatClient, targetCount: 3, threshold: 0); + + List messages = + [ + new ChatMessage(ChatRole.User, "What's the weather in Seattle?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" }), new TestUserInputRequestContent("uir2")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), + new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir2")]), + new ChatMessage(ChatRole.Assistant, "It's sunny and 72°F in Seattle."), + new ChatMessage(ChatRole.User, "What about New York?"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call2", "get_weather", new Dictionary { ["location"] = "New York" })]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call2", "Rainy, 65°F")]), + new ChatMessage(ChatRole.Assistant, "It's rainy and 65°F in New York."), + ]; + + chatClient.GetResponseAsyncCallback = (msgs, _, _) => + { + var msgList = msgs.ToList(); + + Assert.Equal(4, msgList.Count); // 3 non-function messages + system prompt + Assert.DoesNotContain(msgList, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + Assert.Contains(msgList, m => m.Text.Contains("What's the weather in Seattle?")); + Assert.Contains(msgList, m => m.Text.Contains("sunny and 72°F in Seattle")); + Assert.Contains(msgList, m => m.Text.Contains("What about New York?")); + Assert.Contains(msgList, m => m.Role == ChatRole.System); + + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather in Seattle and New York."))); + }; + + var result = await reducer.ReduceAsync(messages, CancellationToken.None); + var resultList = result.ToList(); + + // Should have: summary + 3 kept messages (the last 3 messages with function calls) + Assert.Equal(4, resultList.Count); + + Assert.Contains("User asked about weather", resultList[0].Text); + Assert.Contains(resultList, m => m.Contents.Any(c => c is FunctionCallContent fc && fc.CallId == "call2")); + Assert.Contains(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call2")); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent fc && fc.CallId == "call1")); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call1")); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputRequestContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputResponseContent)); + Assert.DoesNotContain(resultList, m => m.Text.Contains("sunny and 72°F in Seattle")); } [Theory] @@ -121,7 +239,7 @@ public async Task ReduceAsync_RespectsTargetAndThresholdCounts(int targetCount, var messages = new List(); for (int i = 0; i < messageCount; i++) { - messages.Add(new ChatMessage(i % 2 == 0 ? ChatRole.User : ChatRole.Assistant, $"Message {i}")); + messages.Add(new ChatMessage(ChatRole.Assistant, $"Message {i}")); } var summarizationCalled = false; @@ -266,4 +384,20 @@ need frequent exercise. The user then asked about whether they're good around ki m => Assert.StartsWith("Golden retrievers get along", m.Text, StringComparison.Ordinal), m => Assert.StartsWith("Do they make good lap dogs", m.Text, StringComparison.Ordinal)); } + + private sealed class TestUserInputRequestContent : UserInputRequestContent + { + public TestUserInputRequestContent(string id) + : base(id) + { + } + } + + private sealed class TestUserInputResponseContent : UserInputResponseContent + { + public TestUserInputResponseContent(string id) + : base(id) + { + } + } } From 46d0b49fc3cc60cd0802b9bb87182cdd00d36466 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 20 Oct 2025 19:10:14 -0400 Subject: [PATCH 374/472] Tool reduction (#6781) --- eng/packages/General.props | 1 + eng/packages/TestOnly.props | 1 - .../ToolReduction/IToolReductionStrategy.cs | 41 ++ .../Microsoft.Extensions.AI.csproj | 1 + ...hatClientBuilderToolReductionExtensions.cs | 32 + .../EmbeddingToolReductionStrategy.cs | 330 +++++++++ .../ToolReduction/ToolReducingChatClient.cs | 89 +++ .../ChatClientIntegrationTests.cs | 357 ++++++++++ .../ToolReductionTests.cs | 663 ++++++++++++++++++ .../OpenAIChatClientIntegrationTests.cs | 4 + 10 files changed, 1518 insertions(+), 1 deletion(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/ToolReduction/IToolReductionStrategy.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ToolReduction/ChatClientBuilderToolReductionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ToolReduction/EmbeddingToolReductionStrategy.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI/ToolReduction/ToolReducingChatClient.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ToolReductionTests.cs diff --git a/eng/packages/General.props b/eng/packages/General.props index b66f1a4ffa8..441a30afa73 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -29,6 +29,7 @@ + diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index 5875491f919..603e31c0e5b 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -21,7 +21,6 @@ - diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ToolReduction/IToolReductionStrategy.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ToolReduction/IToolReductionStrategy.cs new file mode 100644 index 00000000000..029eeae47a1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ToolReduction/IToolReductionStrategy.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a strategy capable of selecting a reduced set of tools for a chat request. +/// +/// +/// A tool reduction strategy is invoked prior to sending a request to an underlying , +/// enabling scenarios where a large tool catalog must be trimmed to fit provider limits or to improve model +/// tool selection quality. +/// +/// The implementation should return a non- enumerable. Returning the original +/// instance indicates no change. Returning a different enumerable indicates +/// the caller may replace the existing tool list. +/// +/// +[Experimental("MEAI001")] +public interface IToolReductionStrategy +{ + /// + /// Selects the tools that should be included for a specific request. + /// + /// The chat messages for the request. This is an to avoid premature materialization. + /// The chat options for the request (may be ). + /// A token to observe cancellation. + /// + /// A (possibly reduced) enumerable of instances. Must never be . + /// Returning the same instance referenced by . signals no change. + /// + Task> SelectToolsForRequestAsync( + IEnumerable messages, + ChatOptions? options, + CancellationToken cancellationToken = default); +} diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 36e6bb00562..54cbcc99754 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ChatClientBuilderToolReductionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ChatClientBuilderToolReductionExtensions.cs new file mode 100644 index 00000000000..5a644267328 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ChatClientBuilderToolReductionExtensions.cs @@ -0,0 +1,32 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Extension methods for adding tool reduction middleware to a chat client pipeline. +[Experimental("MEAI001")] +public static class ChatClientBuilderToolReductionExtensions +{ + /// + /// Adds tool reduction to the chat client pipeline using the specified . + /// + /// The chat client builder. + /// The reduction strategy. + /// The original builder for chaining. + /// If or is . + /// + /// This should typically appear in the pipeline before function invocation middleware so that only the reduced tools + /// are exposed to the underlying provider. + /// + public static ChatClientBuilder UseToolReduction(this ChatClientBuilder builder, IToolReductionStrategy strategy) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(strategy); + + return builder.Use(inner => new ToolReducingChatClient(inner, strategy)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ToolReduction/EmbeddingToolReductionStrategy.cs b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/EmbeddingToolReductionStrategy.cs new file mode 100644 index 00000000000..f9e4c60995a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/EmbeddingToolReductionStrategy.cs @@ -0,0 +1,330 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +#pragma warning disable IDE0032 // Use auto property, suppressed until repo updates to C# 14 + +/// +/// A tool reduction strategy that ranks tools by embedding similarity to the current conversation context. +/// +/// +/// The strategy embeds each tool (name + description by default) once (cached) and embeds the current +/// conversation content each request. It then selects the top toolLimit tools by similarity. +/// +[Experimental("MEAI001")] +public sealed class EmbeddingToolReductionStrategy : IToolReductionStrategy +{ + private readonly ConditionalWeakTable> _toolEmbeddingsCache = new(); + private readonly IEmbeddingGenerator> _embeddingGenerator; + private readonly int _toolLimit; + + private Func _toolEmbeddingTextSelector = static t => + { + if (string.IsNullOrWhiteSpace(t.Name)) + { + return t.Description; + } + + if (string.IsNullOrWhiteSpace(t.Description)) + { + return t.Name; + } + + return t.Name + Environment.NewLine + t.Description; + }; + + private Func, ValueTask> _messagesEmbeddingTextSelector = static messages => + { + var sb = new StringBuilder(); + foreach (var message in messages) + { + var contents = message.Contents; + for (var i = 0; i < contents.Count; i++) + { + string text; + switch (contents[i]) + { + case TextContent content: + text = content.Text; + break; + case TextReasoningContent content: + text = content.Text; + break; + default: + continue; + } + + _ = sb.AppendLine(text); + } + } + + return new ValueTask(sb.ToString()); + }; + + private Func, ReadOnlyMemory, float> _similarity = static (a, b) => TensorPrimitives.CosineSimilarity(a.Span, b.Span); + + private Func _isRequiredTool = static _ => false; + + /// + /// Initializes a new instance of the class. + /// + /// Embedding generator used to produce embeddings. + /// Maximum number of tools to return, excluding required tools. Must be greater than zero. + public EmbeddingToolReductionStrategy( + IEmbeddingGenerator> embeddingGenerator, + int toolLimit) + { + _embeddingGenerator = Throw.IfNull(embeddingGenerator); + _toolLimit = Throw.IfLessThanOrEqual(toolLimit, min: 0); + } + + /// + /// Gets or sets the selector used to generate a single text string from a tool. + /// + /// + /// Defaults to: Name + "\n" + Description (omitting empty parts). + /// + public Func ToolEmbeddingTextSelector + { + get => _toolEmbeddingTextSelector; + set => _toolEmbeddingTextSelector = Throw.IfNull(value); + } + + /// + /// Gets or sets the selector used to generate a single text string from a collection of chat messages for + /// embedding purposes. + /// + public Func, ValueTask> MessagesEmbeddingTextSelector + { + get => _messagesEmbeddingTextSelector; + set => _messagesEmbeddingTextSelector = Throw.IfNull(value); + } + + /// + /// Gets or sets a similarity function applied to (query, tool) embedding vectors. + /// + /// + /// Defaults to cosine similarity. + /// + public Func, ReadOnlyMemory, float> Similarity + { + get => _similarity; + set => _similarity = Throw.IfNull(value); + } + + /// + /// Gets or sets a function that determines whether a tool is required (always included). + /// + /// + /// If this returns , the tool is included regardless of ranking and does not count against + /// the configured non-required tool limit. A tool explicitly named by (when + /// is non-null) is also treated as required, independent + /// of this delegate's result. + /// + public Func IsRequiredTool + { + get => _isRequiredTool; + set => _isRequiredTool = Throw.IfNull(value); + } + + /// + /// Gets or sets a value indicating whether to preserve original ordering of selected tools. + /// If (default), tools are ordered by descending similarity. + /// If , the top-N tools by similarity are re-emitted in their original order. + /// + public bool PreserveOriginalOrdering { get; set; } + + /// + public async Task> SelectToolsForRequestAsync( + IEnumerable messages, + ChatOptions? options, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(messages); + + if (options?.Tools is not { Count: > 0 } tools) + { + // Prefer the original tools list reference if possible. + // This allows ToolReducingChatClient to avoid unnecessarily copying ChatOptions. + // When no reduction is performed. + return options?.Tools ?? []; + } + + Debug.Assert(_toolLimit > 0, "Expected the tool count limit to be greater than zero."); + + if (tools.Count <= _toolLimit) + { + // Since the total number of tools doesn't exceed the configured tool limit, + // there's no need to determine which tools are optional, i.e., subject to reduction. + // We can return the original tools list early. + return tools; + } + + var toolRankingInfoArray = ArrayPool.Shared.Rent(tools.Count); + try + { + var toolRankingInfoMemory = toolRankingInfoArray.AsMemory(start: 0, length: tools.Count); + + // We allocate tool rankings in a contiguous chunk of memory, but partition them such that + // required tools come first and are immediately followed by optional tools. + // This allows us to separately rank optional tools by similarity score, but then later re-order + // the top N tools (including required tools) to preserve their original relative order. + var (requiredTools, optionalTools) = PartitionToolRankings(toolRankingInfoMemory, tools, options.ToolMode); + + if (optionalTools.Length <= _toolLimit) + { + // There aren't enough optional tools to require reduction, so we'll return the original + // tools list. + return tools; + } + + // Build query text from recent messages. + var queryText = await MessagesEmbeddingTextSelector(messages).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(queryText)) + { + // We couldn't build a meaningful query, likely because the message list was empty. + // We'll just return the original tools list. + return tools; + } + + var queryEmbedding = await _embeddingGenerator.GenerateAsync(queryText, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Compute and populate similarity scores in the tool ranking info. + await ComputeSimilarityScoresAsync(optionalTools, queryEmbedding, cancellationToken); + + var topTools = toolRankingInfoMemory.Slice(start: 0, length: requiredTools.Length + _toolLimit); +#if NET + optionalTools.Span.Sort(AIToolRankingInfo.CompareByDescendingSimilarityScore); + if (PreserveOriginalOrdering) + { + topTools.Span.Sort(AIToolRankingInfo.CompareByOriginalIndex); + } +#else + Array.Sort(toolRankingInfoArray, index: requiredTools.Length, length: optionalTools.Length, AIToolRankingInfo.CompareByDescendingSimilarityScore); + if (PreserveOriginalOrdering) + { + Array.Sort(toolRankingInfoArray, index: 0, length: topTools.Length, AIToolRankingInfo.CompareByOriginalIndex); + } +#endif + return ToToolList(topTools.Span); + + static List ToToolList(ReadOnlySpan toolInfo) + { + var result = new List(capacity: toolInfo.Length); + foreach (var info in toolInfo) + { + result.Add(info.Tool); + } + + return result; + } + } + finally + { + ArrayPool.Shared.Return(toolRankingInfoArray); + } + } + + private (Memory RequiredTools, Memory OptionalTools) PartitionToolRankings( + Memory toolRankingInfo, IList tools, ChatToolMode? toolMode) + { + // Always include a tool if its name matches the required function name. + var requiredFunctionName = (toolMode as RequiredChatToolMode)?.RequiredFunctionName; + var nextRequiredToolIndex = 0; + var nextOptionalToolIndex = tools.Count - 1; + for (var i = 0; i < toolRankingInfo.Length; i++) + { + var tool = tools[i]; + var isRequiredByToolMode = requiredFunctionName is not null && string.Equals(requiredFunctionName, tool.Name, StringComparison.Ordinal); + var toolIndex = isRequiredByToolMode || IsRequiredTool(tool) + ? nextRequiredToolIndex++ + : nextOptionalToolIndex--; + toolRankingInfo.Span[toolIndex] = new AIToolRankingInfo(tool, originalIndex: i); + } + + return ( + RequiredTools: toolRankingInfo.Slice(0, nextRequiredToolIndex), + OptionalTools: toolRankingInfo.Slice(nextRequiredToolIndex)); + } + + private async Task ComputeSimilarityScoresAsync(Memory toolInfo, Embedding queryEmbedding, CancellationToken cancellationToken) + { + var anyCacheMisses = false; + List cacheMissToolEmbeddingTexts = null!; + List cacheMissToolInfoIndexes = null!; + for (var i = 0; i < toolInfo.Length; i++) + { + ref var info = ref toolInfo.Span[i]; + if (_toolEmbeddingsCache.TryGetValue(info.Tool, out var toolEmbedding)) + { + info.SimilarityScore = Similarity(queryEmbedding.Vector, toolEmbedding.Vector); + } + else + { + if (!anyCacheMisses) + { + anyCacheMisses = true; + cacheMissToolEmbeddingTexts = []; + cacheMissToolInfoIndexes = []; + } + + var text = ToolEmbeddingTextSelector(info.Tool); + cacheMissToolEmbeddingTexts.Add(text); + cacheMissToolInfoIndexes.Add(i); + } + } + + if (!anyCacheMisses) + { + // There were no cache misses; no more work to do. + return; + } + + var uncachedEmbeddings = await _embeddingGenerator.GenerateAsync(cacheMissToolEmbeddingTexts, cancellationToken: cancellationToken).ConfigureAwait(false); + if (uncachedEmbeddings.Count != cacheMissToolEmbeddingTexts.Count) + { + throw new InvalidOperationException($"Expected {cacheMissToolEmbeddingTexts.Count} embeddings, got {uncachedEmbeddings.Count}."); + } + + for (var i = 0; i < uncachedEmbeddings.Count; i++) + { + var toolInfoIndex = cacheMissToolInfoIndexes[i]; + var toolEmbedding = uncachedEmbeddings[i]; + ref var info = ref toolInfo.Span[toolInfoIndex]; + info.SimilarityScore = Similarity(queryEmbedding.Vector, toolEmbedding.Vector); + _toolEmbeddingsCache.Add(info.Tool, toolEmbedding); + } + } + + private struct AIToolRankingInfo(AITool tool, int originalIndex) + { + public static readonly Comparer CompareByDescendingSimilarityScore + = Comparer.Create(static (a, b) => + { + var result = b.SimilarityScore.CompareTo(a.SimilarityScore); + return result != 0 + ? result + : a.OriginalIndex.CompareTo(b.OriginalIndex); // Stabilize ties. + }); + + public static readonly Comparer CompareByOriginalIndex + = Comparer.Create(static (a, b) => a.OriginalIndex.CompareTo(b.OriginalIndex)); + + public AITool Tool { get; } = tool; + public int OriginalIndex { get; } = originalIndex; + public float SimilarityScore { get; set; } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ToolReducingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ToolReducingChatClient.cs new file mode 100644 index 00000000000..6a5d6d925fc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI/ToolReduction/ToolReducingChatClient.cs @@ -0,0 +1,89 @@ +// 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.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// A delegating chat client that applies a tool reduction strategy before invoking the inner client. +/// +/// +/// Insert this into a pipeline (typically before function invocation middleware) to automatically +/// reduce the tool list carried on for each request. +/// +[Experimental("MEAI001")] +public sealed class ToolReducingChatClient : DelegatingChatClient +{ + private readonly IToolReductionStrategy _strategy; + + /// + /// Initializes a new instance of the class. + /// + /// The inner client. + /// The tool reduction strategy to apply. + /// Thrown if any argument is . + public ToolReducingChatClient(IChatClient innerClient, IToolReductionStrategy strategy) + : base(innerClient) + { + _strategy = Throw.IfNull(strategy); + } + + /// + public override async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + options = await ApplyReductionAsync(messages, options, cancellationToken).ConfigureAwait(false); + return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + options = await ApplyReductionAsync(messages, options, cancellationToken).ConfigureAwait(false); + + await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } + + private async Task ApplyReductionAsync( + IEnumerable messages, + ChatOptions? options, + CancellationToken cancellationToken) + { + // If there are no options or no tools, skip. + if (options?.Tools is not { Count: > 0 }) + { + return options; + } + + var reduced = await _strategy.SelectToolsForRequestAsync(messages, options, cancellationToken).ConfigureAwait(false); + + // If strategy returned the same list instance (or reference equality), assume no change. + if (ReferenceEquals(reduced, options.Tools)) + { + return options; + } + + // Materialize and compare counts; if unchanged and tools have identical ordering and references, keep original. + if (reduced is not IList reducedList) + { + reducedList = reduced.ToList(); + } + + // Clone options to avoid mutating a possibly shared instance. + var cloned = options.Clone(); + cloned.Tools = reducedList; + return cloned; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 448de8d11df..992e86a1184 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -41,6 +41,8 @@ protected ChatClientIntegrationTests() protected IChatClient? ChatClient { get; } + protected IEmbeddingGenerator>? EmbeddingGenerator { get; private set; } + public void Dispose() { ChatClient?.Dispose(); @@ -49,6 +51,13 @@ public void Dispose() protected abstract IChatClient? CreateChatClient(); + /// + /// Optionally supplies an embedding generator for integration tests that exercise + /// embedding-based components (e.g., tool reduction). Default returns null and + /// tests depending on embeddings will skip if not overridden. + /// + protected virtual IEmbeddingGenerator>? CreateEmbeddingGenerator() => null; + [ConditionalFact] public virtual async Task GetResponseAsync_SingleRequestMessage() { @@ -1395,6 +1404,343 @@ public void Dispose() } } + [ConditionalFact] + public virtual async Task ToolReduction_DynamicSelection_RespectsConversationHistory() + { + SkipIfNotEnabled(); + EnsureEmbeddingGenerator(); + + // Limit to 2 so that, once the conversation references both weather and translation, + // both tools can be included even if the latest user turn only mentions one of them. + var strategy = new EmbeddingToolReductionStrategy(EmbeddingGenerator, toolLimit: 2); + + var weatherTool = AIFunctionFactory.Create( + () => "Weather data", + new AIFunctionFactoryOptions + { + Name = "GetWeatherForecast", + Description = "Returns weather forecast and temperature for a given city." + }); + + var translateTool = AIFunctionFactory.Create( + () => "Translated text", + new AIFunctionFactoryOptions + { + Name = "TranslateText", + Description = "Translates text between human languages." + }); + + var mathTool = AIFunctionFactory.Create( + () => 42, + new AIFunctionFactoryOptions + { + Name = "SolveMath", + Description = "Solves basic math problems." + }); + + var allTools = new List { weatherTool, translateTool, mathTool }; + + IList? firstTurnTools = null; + IList? secondTurnTools = null; + + using var client = ChatClient! + .AsBuilder() + .UseToolReduction(strategy) + .Use(async (messages, options, next, ct) => + { + // Capture the (possibly reduced) tool list for each turn. + if (firstTurnTools is null) + { + firstTurnTools = options?.Tools; + } + else + { + secondTurnTools ??= options?.Tools; + } + + await next(messages, options, ct); + }) + .UseFunctionInvocation() + .Build(); + + // Maintain chat history across turns. + List history = []; + + // Turn 1: Ask a weather question. + history.Add(new ChatMessage(ChatRole.User, "What will the weather be in Seattle tomorrow?")); + var firstResponse = await client.GetResponseAsync(history, new ChatOptions { Tools = allTools }); + history.AddMessages(firstResponse); // Append assistant reply. + + Assert.NotNull(firstTurnTools); + Assert.Contains(firstTurnTools, t => t.Name == "GetWeatherForecast"); + + // Turn 2: Ask a translation question. Even though only translation is mentioned now, + // conversation history still contains a weather request. Expect BOTH weather + translation tools. + history.Add(new ChatMessage(ChatRole.User, "Please translate 'good evening' into French.")); + var secondResponse = await client.GetResponseAsync(history, new ChatOptions { Tools = allTools }); + history.AddMessages(secondResponse); + + Assert.NotNull(secondTurnTools); + Assert.Equal(2, secondTurnTools.Count); // Should have filled both slots with the two relevant domains. + Assert.Contains(secondTurnTools, t => t.Name == "GetWeatherForecast"); + Assert.Contains(secondTurnTools, t => t.Name == "TranslateText"); + + // Ensure unrelated tool was excluded. + Assert.DoesNotContain(secondTurnTools, t => t.Name == "SolveMath"); + } + + [ConditionalFact] + public virtual async Task ToolReduction_RequireSpecificToolPreservedAndOrdered() + { + SkipIfNotEnabled(); + EnsureEmbeddingGenerator(); + + // Limit would normally reduce to 1, but required tool plus another should remain. + var strategy = new EmbeddingToolReductionStrategy(EmbeddingGenerator, toolLimit: 1); + + var translateTool = AIFunctionFactory.Create( + () => "Translated text", + new AIFunctionFactoryOptions + { + Name = "TranslateText", + Description = "Translates phrases between languages." + }); + + var weatherTool = AIFunctionFactory.Create( + () => "Weather data", + new AIFunctionFactoryOptions + { + Name = "GetWeatherForecast", + Description = "Returns forecast data for a city." + }); + + var tools = new List { translateTool, weatherTool }; + + IList? captured = null; + + using var client = ChatClient! + .AsBuilder() + .UseToolReduction(strategy) + .UseFunctionInvocation() + .Use((messages, options, next, ct) => + { + captured = options?.Tools; + return next(messages, options, ct); + }) + .Build(); + + var history = new List + { + new(ChatRole.User, "What will the weather be like in Redmond next week?") + }; + + var response = await client.GetResponseAsync(history, new ChatOptions + { + Tools = tools, + ToolMode = ChatToolMode.RequireSpecific(translateTool.Name) + }); + history.AddMessages(response); + + Assert.NotNull(captured); + Assert.Equal(2, captured!.Count); + Assert.Equal("TranslateText", captured[0].Name); // Required should appear first. + Assert.Equal("GetWeatherForecast", captured[1].Name); + } + + [ConditionalFact] + public virtual async Task ToolReduction_ToolRemovedAfterFirstUse_NotInvokedAgain() + { + SkipIfNotEnabled(); + EnsureEmbeddingGenerator(); + + int weatherInvocationCount = 0; + + var weatherTool = AIFunctionFactory.Create( + () => + { + weatherInvocationCount++; + return "Sunny and dry."; + }, + new AIFunctionFactoryOptions + { + Name = "GetWeather", + Description = "Gets the weather forecast for a given location." + }); + + // Strategy exposes tools only on the first request, then removes them. + var removalStrategy = new RemoveToolAfterFirstUseStrategy(); + + IList? firstTurnTools = null; + IList? secondTurnTools = null; + + using var client = ChatClient! + .AsBuilder() + // Place capture immediately after reduction so it's invoked exactly once per user request. + .UseToolReduction(removalStrategy) + .Use((messages, options, next, ct) => + { + if (firstTurnTools is null) + { + firstTurnTools = options?.Tools; + } + else + { + secondTurnTools ??= options?.Tools; + } + + return next(messages, options, ct); + }) + .UseFunctionInvocation() + .Build(); + + List history = []; + + // Turn 1 + history.Add(new ChatMessage(ChatRole.User, "What's the weather like tomorrow in Seattle?")); + var firstResponse = await client.GetResponseAsync(history, new ChatOptions + { + Tools = [weatherTool], + ToolMode = ChatToolMode.RequireAny + }); + history.AddMessages(firstResponse); + + Assert.Equal(1, weatherInvocationCount); + Assert.NotNull(firstTurnTools); + Assert.Contains(firstTurnTools!, t => t.Name == "GetWeather"); + + // Turn 2 (tool removed by strategy even though caller supplies it again) + history.Add(new ChatMessage(ChatRole.User, "And what about next week?")); + var secondResponse = await client.GetResponseAsync(history, new ChatOptions + { + Tools = [weatherTool] + }); + history.AddMessages(secondResponse); + + Assert.Equal(1, weatherInvocationCount); // Not invoked again. + Assert.NotNull(secondTurnTools); + Assert.Empty(secondTurnTools!); // Strategy removed the tool set. + + // Response text shouldn't just echo the tool's stub output. + Assert.DoesNotContain("Sunny and dry.", secondResponse.Text, StringComparison.OrdinalIgnoreCase); + } + + [ConditionalFact] + public virtual async Task ToolReduction_MessagesEmbeddingTextSelector_UsesChatClientToAnalyzeConversation() + { + SkipIfNotEnabled(); + EnsureEmbeddingGenerator(); + + // Create tools for different domains. + var weatherTool = AIFunctionFactory.Create( + () => "Weather data", + new AIFunctionFactoryOptions + { + Name = "GetWeatherForecast", + Description = "Returns weather forecast and temperature for a given city." + }); + + var translateTool = AIFunctionFactory.Create( + () => "Translated text", + new AIFunctionFactoryOptions + { + Name = "TranslateText", + Description = "Translates text between human languages." + }); + + var mathTool = AIFunctionFactory.Create( + () => 42, + new AIFunctionFactoryOptions + { + Name = "SolveMath", + Description = "Solves basic math problems." + }); + + var allTools = new List { weatherTool, translateTool, mathTool }; + + // Track the analysis result from the chat client used in the selector. + string? capturedAnalysis = null; + + var strategy = new EmbeddingToolReductionStrategy(EmbeddingGenerator, toolLimit: 2) + { + // Use a chat client to analyze the conversation and extract relevant tool categories. + MessagesEmbeddingTextSelector = async messages => + { + var conversationText = string.Join("\n", messages.Select(m => $"{m.Role}: {m.Text}")); + + var analysisPrompt = $""" + Analyze the following conversation and identify what kinds of tools would be most helpful. + Focus on the key topics and tasks being discussed. + Respond with a brief summary of the relevant tool categories (e.g., "weather", "translation", "math"). + + Conversation: + {conversationText} + + Relevant tool categories: + """; + + var response = await ChatClient!.GetResponseAsync(analysisPrompt); + capturedAnalysis = response.Text; + + // Return the analysis as the query text for embedding-based tool selection. + return capturedAnalysis; + } + }; + + IList? selectedTools = null; + + using var client = ChatClient! + .AsBuilder() + .UseToolReduction(strategy) + .Use(async (messages, options, next, ct) => + { + selectedTools = options?.Tools; + await next(messages, options, ct); + }) + .UseFunctionInvocation() + .Build(); + + // Conversation that clearly indicates weather-related needs. + List history = []; + history.Add(new ChatMessage(ChatRole.User, "What will the weather be like in London tomorrow?")); + + var response = await client.GetResponseAsync(history, new ChatOptions { Tools = allTools }); + history.AddMessages(response); + + // Verify that the chat client was used to analyze the conversation. + Assert.NotNull(capturedAnalysis); + Assert.True( + capturedAnalysis.IndexOf("weather", StringComparison.OrdinalIgnoreCase) >= 0 || + capturedAnalysis.IndexOf("forecast", StringComparison.OrdinalIgnoreCase) >= 0, + $"Expected analysis to mention weather or forecast: {capturedAnalysis}"); + + // Verify that the tool selection was influenced by the analysis. + Assert.NotNull(selectedTools); + Assert.True(selectedTools.Count <= 2, $"Expected at most 2 tools, got {selectedTools.Count}"); + Assert.Contains(selectedTools, t => t.Name == "GetWeatherForecast"); + } + + // Test-only custom strategy: include tools on first request, then remove them afterward. + private sealed class RemoveToolAfterFirstUseStrategy : IToolReductionStrategy + { + private bool _used; + + public Task> SelectToolsForRequestAsync( + IEnumerable messages, + ChatOptions? options, + CancellationToken cancellationToken = default) + { + if (!_used && options?.Tools is { Count: > 0 }) + { + _used = true; + // Returning the same instance signals no change. + return Task.FromResult>(options.Tools); + } + + // After first use, remove all tools. + return Task.FromResult>(Array.Empty()); + } + } + [MemberNotNull(nameof(ChatClient))] protected void SkipIfNotEnabled() { @@ -1405,4 +1751,15 @@ protected void SkipIfNotEnabled() throw new SkipTestException("Client is not enabled."); } } + + [MemberNotNull(nameof(EmbeddingGenerator))] + protected void EnsureEmbeddingGenerator() + { + EmbeddingGenerator ??= CreateEmbeddingGenerator(); + + if (EmbeddingGenerator is null) + { + throw new SkipTestException("Embedding generator is not enabled."); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ToolReductionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ToolReductionTests.cs new file mode 100644 index 00000000000..96c9adc6311 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ToolReductionTests.cs @@ -0,0 +1,663 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ToolReductionTests +{ + [Fact] + public void EmbeddingToolReductionStrategy_Constructor_ThrowsWhenToolLimitIsLessThanOrEqualToZero() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + Assert.Throws(() => new EmbeddingToolReductionStrategy(gen, toolLimit: 0)); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_NoReduction_WhenToolsBelowLimit() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 5); + + var tools = CreateTools("Weather", "Math"); + var options = new ChatOptions { Tools = tools }; + + var result = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "Tell me about weather") }, + options); + + Assert.Same(tools, result); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_NoReduction_WhenOptionalToolsBelowLimit() + { + // 1 required + 2 optional, limit = 2 (optional count == limit) => original list returned + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2) + { + IsRequiredTool = t => t.Name == "Req" + }; + + var tools = CreateTools("Req", "Opt1", "Opt2"); + var result = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "anything") }, + new ChatOptions { Tools = tools }); + + Assert.Same(tools, result); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_Reduces_ToLimit_BySimilarity() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2); + + var tools = CreateTools("Weather", "Translate", "Math", "Jokes"); + var options = new ChatOptions { Tools = tools }; + + var messages = new[] + { + new ChatMessage(ChatRole.User, "Can you do some weather math for forecasting?") + }; + + var reduced = (await strategy.SelectToolsForRequestAsync(messages, options)).ToList(); + + Assert.Equal(2, reduced.Count); + Assert.Contains(reduced, t => t.Name == "Weather"); + Assert.Contains(reduced, t => t.Name == "Math"); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_PreserveOriginalOrdering_ReordersAfterSelection() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2) + { + PreserveOriginalOrdering = true + }; + + var tools = CreateTools("Math", "Translate", "Weather"); + var reduced = (await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "Explain weather math please") }, + new ChatOptions { Tools = tools })).ToList(); + + Assert.Equal(2, reduced.Count); + Assert.Equal("Math", reduced[0].Name); + Assert.Equal("Weather", reduced[1].Name); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_Caching_AvoidsReEmbeddingTools() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1); + + var tools = CreateTools("Weather", "Math", "Jokes"); + var messages = new[] { new ChatMessage(ChatRole.User, "weather") }; + + _ = await strategy.SelectToolsForRequestAsync(messages, new ChatOptions { Tools = tools }); + int afterFirst = gen.TotalValueInputs; + + _ = await strategy.SelectToolsForRequestAsync(messages, new ChatOptions { Tools = tools }); + int afterSecond = gen.TotalValueInputs; + + // +1 for second query embedding only + Assert.Equal(afterFirst + 1, afterSecond); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_OptionsNullOrNoTools_ReturnsEmptyOrOriginal() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2); + + var empty = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "anything") }, null); + Assert.Empty(empty); + + var options = new ChatOptions { Tools = [] }; + var result = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "weather") }, options); + Assert.Same(options.Tools, result); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_CustomSimilarity_InvertsOrdering() + { + using var gen = new VectorBasedTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1) + { + Similarity = (q, t) => -t.Span[0] + }; + + var highTool = new SimpleTool("HighScore", "alpha"); + var lowTool = new SimpleTool("LowScore", "beta"); + gen.VectorSelector = text => text.Contains("alpha") ? 10f : 1f; + + var reduced = (await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "Pick something") }, + new ChatOptions { Tools = [highTool, lowTool] })).ToList(); + + Assert.Single(reduced); + Assert.Equal("LowScore", reduced[0].Name); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_TieDeterminism_PrefersLowerOriginalIndex() + { + // Generator returns identical vectors so similarity ties; we expect original order preserved + using var gen = new ConstantEmbeddingGenerator(3); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2); + + var tools = CreateTools("T1", "T2", "T3", "T4"); + var reduced = (await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "any") }, + new ChatOptions { Tools = tools })).ToList(); + + Assert.Equal(2, reduced.Count); + Assert.Equal("T1", reduced[0].Name); + Assert.Equal("T2", reduced[1].Name); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_DefaultEmbeddingTextSelector_EmptyDescription_UsesNameOnly() + { + using var recorder = new RecordingEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(recorder, toolLimit: 1); + + var target = new SimpleTool("ComputeSum", description: ""); + var filler = new SimpleTool("Other", "Unrelated"); + _ = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "math") }, + new ChatOptions { Tools = [target, filler] }); + + Assert.Contains("ComputeSum", recorder.Inputs); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_DefaultEmbeddingTextSelector_EmptyName_UsesDescriptionOnly() + { + using var recorder = new RecordingEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(recorder, toolLimit: 1); + + var target = new SimpleTool("", description: "Translates between languages."); + var filler = new SimpleTool("Other", "Unrelated"); + _ = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "translate") }, + new ChatOptions { Tools = [target, filler] }); + + Assert.Contains("Translates between languages.", recorder.Inputs); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_CustomEmbeddingTextSelector_Applied() + { + using var recorder = new RecordingEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(recorder, toolLimit: 1) + { + ToolEmbeddingTextSelector = t => $"NAME:{t.Name}|DESC:{t.Description}" + }; + + var target = new SimpleTool("WeatherTool", "Gets forecast."); + var filler = new SimpleTool("Other", "Irrelevant"); + _ = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "weather") }, + new ChatOptions { Tools = [target, filler] }); + + Assert.Contains("NAME:WeatherTool|DESC:Gets forecast.", recorder.Inputs); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_MessagesEmbeddingTextSelector_CustomFiltersMessages() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1); + + var tools = CreateTools("Weather", "Math", "Translate"); + + var messages = new[] + { + new ChatMessage(ChatRole.User, "Please tell me the weather tomorrow."), + new ChatMessage(ChatRole.Assistant, "Sure, I can help."), + new ChatMessage(ChatRole.User, "Now instead solve a math problem.") + }; + + strategy.MessagesEmbeddingTextSelector = msgs => new ValueTask(msgs.LastOrDefault()?.Text ?? string.Empty); + + var reduced = (await strategy.SelectToolsForRequestAsync( + messages, + new ChatOptions { Tools = tools })).ToList(); + + Assert.Single(reduced); + Assert.Equal("Math", reduced[0].Name); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_MessagesEmbeddingTextSelector_InvokedOnce() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1); + + var tools = CreateTools("Weather", "Math"); + int invocationCount = 0; + + strategy.MessagesEmbeddingTextSelector = msgs => + { + invocationCount++; + return new ValueTask(string.Join("\n", msgs.Select(m => m.Text))); + }; + + _ = await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "weather and math") }, + new ChatOptions { Tools = tools }); + + Assert.Equal(1, invocationCount); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_DefaultMessagesEmbeddingTextSelector_IncludesReasoningContent() + { + using var recorder = new RecordingEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(recorder, toolLimit: 1); + var tools = CreateTools("Weather", "Math"); + + var reasoningLine = "Thinking about the best way to get tomorrow's forecast..."; + var answerLine = "Tomorrow will be sunny."; + var userLine = "What's the weather tomorrow?"; + + var messages = new[] + { + new ChatMessage(ChatRole.User, userLine), + new ChatMessage(ChatRole.Assistant, + [ + new TextReasoningContent(reasoningLine), + new TextContent(answerLine) + ]) + }; + + _ = await strategy.SelectToolsForRequestAsync(messages, new ChatOptions { Tools = tools }); + + string queryInput = recorder.Inputs[0]; + + Assert.Contains(userLine, queryInput); + Assert.Contains(reasoningLine, queryInput); + Assert.Contains(answerLine, queryInput); + + var userIndex = queryInput.IndexOf(userLine, StringComparison.Ordinal); + var reasoningIndex = queryInput.IndexOf(reasoningLine, StringComparison.Ordinal); + var answerIndex = queryInput.IndexOf(answerLine, StringComparison.Ordinal); + Assert.True(userIndex >= 0 && reasoningIndex > userIndex && answerIndex > reasoningIndex); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_DefaultMessagesEmbeddingTextSelector_SkipsNonTextContent() + { + using var recorder = new RecordingEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(recorder, toolLimit: 1); + var tools = CreateTools("Alpha", "Beta"); + + var textOnly = "Provide translation."; + var messages = new[] + { + new ChatMessage(ChatRole.User, + [ + new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream"), + new TextContent(textOnly) + ]) + }; + + _ = await strategy.SelectToolsForRequestAsync(messages, new ChatOptions { Tools = tools }); + + var queryInput = recorder.Inputs[0]; + Assert.Contains(textOnly, queryInput); + Assert.DoesNotContain("application/octet-stream", queryInput, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_RequiredToolAlwaysIncluded() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1) + { + IsRequiredTool = t => t.Name == "Core" + }; + + var tools = CreateTools("Core", "Weather", "Math"); + var reduced = (await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "math") }, + new ChatOptions { Tools = tools })).ToList(); + + Assert.Equal(2, reduced.Count); // required + one optional (limit=1) + Assert.Contains(reduced, t => t.Name == "Core"); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_MultipleRequiredTools_ExceedLimit_AllRequiredIncluded() + { + // 3 required, limit=1 => expect 3 required + 1 ranked optional = 4 total + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1) + { + IsRequiredTool = t => t.Name.StartsWith("R", StringComparison.Ordinal) + }; + + var tools = CreateTools("R1", "R2", "R3", "Weather", "Math"); + var reduced = (await strategy.SelectToolsForRequestAsync( + new[] { new ChatMessage(ChatRole.User, "weather math") }, + new ChatOptions { Tools = tools })).ToList(); + + Assert.Equal(4, reduced.Count); + Assert.Equal(3, reduced.Count(t => t.Name.StartsWith("R"))); + } + + [Fact] + public async Task ToolReducingChatClient_ReducesTools_ForGetResponseAsync() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 2); + var tools = CreateTools("Weather", "Math", "Translate", "Jokes"); + + IList? observedTools = null; + + using var inner = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, ct) => + { + observedTools = options?.Tools; + return Task.FromResult(new ChatResponse()); + } + }; + + using var client = inner.AsBuilder().UseToolReduction(strategy).Build(); + + await client.GetResponseAsync( + new[] { new ChatMessage(ChatRole.User, "weather math please") }, + new ChatOptions { Tools = tools }); + + Assert.NotNull(observedTools); + Assert.Equal(2, observedTools!.Count); + Assert.Contains(observedTools, t => t.Name == "Weather"); + Assert.Contains(observedTools, t => t.Name == "Math"); + } + + [Fact] + public async Task ToolReducingChatClient_ReducesTools_ForStreaming() + { + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1); + var tools = CreateTools("Weather", "Math"); + + IList? observedTools = null; + + using var inner = new TestChatClient + { + GetStreamingResponseAsyncCallback = (messages, options, ct) => + { + observedTools = options?.Tools; + return EmptyAsyncEnumerable(); + } + }; + + using var client = inner.AsBuilder().UseToolReduction(strategy).Build(); + + await foreach (var _ in client.GetStreamingResponseAsync( + new[] { new ChatMessage(ChatRole.User, "math") }, + new ChatOptions { Tools = tools })) + { + // Consume + } + + Assert.NotNull(observedTools); + Assert.Single(observedTools!); + Assert.Equal("Math", observedTools![0].Name); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_EmptyQuery_NoReduction() + { + // Arrange: more tools than limit so we'd normally reduce, but query is empty -> return full list unchanged. + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1); + + var tools = CreateTools("ToolA", "ToolB", "ToolC"); + var options = new ChatOptions { Tools = tools }; + + // Empty / whitespace message text produces empty query. + var messages = new[] { new ChatMessage(ChatRole.User, " ") }; + + // Act + var result = await strategy.SelectToolsForRequestAsync(messages, options); + + // Assert: same reference (no reduction), and generator not invoked at all. + Assert.Same(tools, result); + Assert.Equal(0, gen.TotalValueInputs); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_EmptyQuery_NoReduction_WithRequiredTool() + { + // Arrange: required tool + optional tools; still should return original set when query is empty. + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1) + { + IsRequiredTool = t => t.Name == "Req" + }; + + var tools = CreateTools("Req", "Optional1", "Optional2"); + var options = new ChatOptions { Tools = tools }; + + var messages = new[] { new ChatMessage(ChatRole.User, " ") }; + + // Act + var result = await strategy.SelectToolsForRequestAsync(messages, options); + + // Assert + Assert.Same(tools, result); + Assert.Equal(0, gen.TotalValueInputs); + } + + [Fact] + public async Task EmbeddingToolReductionStrategy_EmptyQuery_ViaCustomMessagesSelector_NoReduction() + { + // Arrange: force empty query through custom selector returning whitespace. + using var gen = new DeterministicTestEmbeddingGenerator(); + var strategy = new EmbeddingToolReductionStrategy(gen, toolLimit: 1) + { + MessagesEmbeddingTextSelector = _ => new ValueTask(" ") + }; + + var tools = CreateTools("One", "Two"); + var messages = new[] + { + new ChatMessage(ChatRole.User, "This content will be ignored by custom selector.") + }; + + // Act + var result = await strategy.SelectToolsForRequestAsync(messages, new ChatOptions { Tools = tools }); + + // Assert: no reduction and no embeddings generated. + Assert.Same(tools, result); + Assert.Equal(0, gen.TotalValueInputs); + } + + private static List CreateTools(params string[] names) => + names.Select(n => (AITool)new SimpleTool(n, $"Description about {n}")).ToList(); + +#pragma warning disable CS1998 + private static async IAsyncEnumerable EmptyAsyncEnumerable() + { + yield break; + } +#pragma warning restore CS1998 + + private sealed class SimpleTool : AITool + { + private readonly string _name; + private readonly string _description; + + public SimpleTool(string name, string description) + { + _name = name; + _description = description; + } + + public override string Name => _name; + public override string Description => _description; + } + + /// + /// Deterministic embedding generator producing sparse keyword indicator vectors. + /// Each dimension corresponds to a known keyword. Cosine similarity then reflects + /// pure keyword overlap (non-overlapping keywords contribute nothing), avoiding + /// false ties for tools unrelated to the query. + /// + private sealed class DeterministicTestEmbeddingGenerator : IEmbeddingGenerator> + { + private static readonly string[] _keywords = + [ + "weather","forecast","temperature","math","calculate","sum","translate","language","joke" + ]; + + // +1 bias dimension (last) to avoid zero magnitude vectors when no keywords present. + private static int VectorLength => _keywords.Length + 1; + + public int TotalValueInputs { get; private set; } + + public Task>> GenerateAsync( + IEnumerable values, + EmbeddingGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + var list = new List>(); + + foreach (var v in values) + { + TotalValueInputs++; + var vec = new float[VectorLength]; + if (!string.IsNullOrWhiteSpace(v)) + { + var lower = v.ToLowerInvariant(); + for (int i = 0; i < _keywords.Length; i++) + { + if (lower.Contains(_keywords[i])) + { + vec[i] = 1f; + } + } + } + + vec[^1] = 1f; // bias + list.Add(new Embedding(vec)); + } + + return Task.FromResult(new GeneratedEmbeddings>(list)); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() + { + // No-op + } + } + + private sealed class RecordingEmbeddingGenerator : IEmbeddingGenerator> + { + public List Inputs { get; } = new(); + + public Task>> GenerateAsync( + IEnumerable values, + EmbeddingGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + var list = new List>(); + foreach (var v in values) + { + Inputs.Add(v); + + // Basic 2-dim vector (length encodes a bit of variability) + list.Add(new Embedding(new float[] { v.Length, 1f })); + } + + return Task.FromResult(new GeneratedEmbeddings>(list)); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + public void Dispose() + { + // No-op + } + } + + private sealed class VectorBasedTestEmbeddingGenerator : IEmbeddingGenerator> + { + public Func VectorSelector { get; set; } = _ => 1f; + public Task>> GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + var list = new List>(); + foreach (var v in values) + { + list.Add(new Embedding(new float[] { VectorSelector(v), 1f })); + } + + return Task.FromResult(new GeneratedEmbeddings>(list)); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + public void Dispose() + { + // No-op + } + } + + private sealed class ConstantEmbeddingGenerator : IEmbeddingGenerator> + { + private readonly float[] _vector; + public ConstantEmbeddingGenerator(int dims) + { + _vector = Enumerable.Repeat(1f, dims).ToArray(); + } + + public Task>> GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + var list = new List>(); + foreach (var _ in values) + { + list.Add(new Embedding(_vector)); + } + + return Task.FromResult(new GeneratedEmbeddings>(list)); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + public void Dispose() + { + // No-op + } + } + + private sealed class TestChatClient : IChatClient + { + public Func, ChatOptions?, CancellationToken, Task>? GetResponseAsyncCallback { get; set; } + public Func, ChatOptions?, CancellationToken, IAsyncEnumerable>? GetStreamingResponseAsyncCallback { get; set; } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + (GetResponseAsyncCallback ?? throw new InvalidOperationException())(messages, options, cancellationToken); + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + (GetStreamingResponseAsyncCallback ?? throw new InvalidOperationException())(messages, options, cancellationToken); + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + public void Dispose() + { + // No-op + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientIntegrationTests.cs index 6322e3d6b64..a9e08a58e52 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientIntegrationTests.cs @@ -8,4 +8,8 @@ public class OpenAIChatClientIntegrationTests : ChatClientIntegrationTests protected override IChatClient? CreateChatClient() => IntegrationTestHelpers.GetOpenAIClient() ?.GetChatClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini").AsIChatClient(); + + protected override IEmbeddingGenerator>? CreateEmbeddingGenerator() => + IntegrationTestHelpers.GetOpenAIClient() + ?.GetEmbeddingClient(TestRunnerConfiguration.Instance["OpenAI:EmbeddingModel"] ?? "text-embedding-3-small").AsIEmbeddingGenerator(); } From 9ad9b28072bebd6320d597e463c46df4e8f2cc3c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 20 Oct 2025 19:12:28 -0400 Subject: [PATCH 375/472] Fix coalescing of TextReasoningContent with ProtectedData (#6936) --- .../ChatCompletion/ChatResponseExtensions.cs | 24 +++++++++++- .../ChatResponseUpdateExtensionsTests.cs | 37 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index cbc00dd98ec..551607cc775 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -193,10 +193,32 @@ internal static void CoalesceTextContent(IList contents) contents, mergeSingle: false, canMerge: static (r1, r2) => string.IsNullOrEmpty(r1.ProtectedData), // we allow merging if the first item has no ProtectedData, even if the second does - static (contents, start, end) => new(MergeText(contents, start, end)) { AdditionalProperties = contents[start].AdditionalProperties?.Clone() }); + static (contents, start, end) => + { + TextReasoningContent content = new(MergeText(contents, start, end)) + { + AdditionalProperties = contents[start].AdditionalProperties?.Clone() + }; + +#if DEBUG + for (int i = start; i < end - 1; i++) + { + Debug.Assert(contents[i] is TextReasoningContent { ProtectedData: null }, "Expected all but the last to have a null ProtectedData"); + } +#endif + + if (((TextReasoningContent)contents[end - 1]).ProtectedData is { } protectedData) + { + content.ProtectedData = protectedData; + } + + return content; + }); static string MergeText(IList contents, int start, int end) { + Debug.Assert(end - start > 1, "Expected multiple contents to merge"); + StringBuilder sb = new(); for (int i = start; i < end; i++) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index ee9ae354677..368e11b44d4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -631,6 +631,43 @@ public async Task ToChatResponse_CoalescesTextContentAndTextReasoningContentSepa Assert.Equal("OP", Assert.IsType(message.Contents[7]).Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToChatResponse_CoalescesTextReasoningContentUpToProtectedData(bool useAsync) + { + ChatResponseUpdate[] updates = + { + new() { Contents = [new TextReasoningContent("A") { ProtectedData = "1" }] }, + new() { Contents = [new TextReasoningContent("B") { ProtectedData = "2" }] }, + new() { Contents = [new TextReasoningContent("C")] }, + new() { Contents = [new TextReasoningContent("D")] }, + new() { Contents = [new TextReasoningContent("E") { ProtectedData = "3" }] }, + new() { Contents = [new TextReasoningContent("F") { ProtectedData = "4" }] }, + new() { Contents = [new TextReasoningContent("G")] }, + new() { Contents = [new TextReasoningContent("H")] }, + }; + + ChatResponse response = useAsync ? await YieldAsync(updates).ToChatResponseAsync() : updates.ToChatResponse(); + ChatMessage message = Assert.Single(response.Messages); + Assert.Equal(5, message.Contents.Count); + + Assert.Equal("A", Assert.IsType(message.Contents[0]).Text); + Assert.Equal("1", ((TextReasoningContent)message.Contents[0]).ProtectedData); + + Assert.Equal("B", Assert.IsType(message.Contents[1]).Text); + Assert.Equal("2", ((TextReasoningContent)message.Contents[1]).ProtectedData); + + Assert.Equal("CDE", Assert.IsType(message.Contents[2]).Text); + Assert.Equal("3", ((TextReasoningContent)message.Contents[2]).ProtectedData); + + Assert.Equal("F", Assert.IsType(message.Contents[3]).Text); + Assert.Equal("4", ((TextReasoningContent)message.Contents[3]).ProtectedData); + + Assert.Equal("GH", Assert.IsType(message.Contents[4]).Text); + Assert.Null(((TextReasoningContent)message.Contents[4]).ProtectedData); + } + [Theory] [InlineData(false)] [InlineData(true)] From 09472076b1c0d53822d9ce085d9df408286bb867 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:42:29 -0700 Subject: [PATCH 376/472] Doc updates (#6930) * doc updates --------- Co-authored-by: Stephen Toub --- .../ChatCompletion/ChatOptions.cs | 32 +++++++++---------- .../ChatCompletion/ChatResponse.cs | 13 ++++---- .../ChatCompletion/ChatResponseUpdate.cs | 18 +++++------ .../ChatCompletion/IChatClient.cs | 2 +- .../Embeddings/EmbeddingGenerationOptions.cs | 4 +-- .../Functions/AIFunctionFactoryOptions.cs | 2 +- .../SpeechToText/SpeechToTextOptions.cs | 2 +- .../Tools/AITool.cs | 2 +- 8 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 1415ae68055..738f724dcd2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -133,24 +133,24 @@ protected ChatOptions(ChatOptions? other) public IList? StopSequences { get; set; } /// - /// Gets or sets a flag to indicate whether a single response is allowed to include multiple tool calls. - /// If , the is asked to return a maximum of one tool call per request. - /// If , there is no limit. - /// If , the provider may select its own default. + /// Gets or sets a value that indicates whether a single response is allowed to include multiple tool calls. /// + /// + /// for no limit. if the is asked to return a maximum of one tool call per request. If , the provider can select its own default. + /// /// /// /// When used with function calling middleware, this does not affect the ability to perform multiple function calls in sequence. /// It only affects the number of function calls within a single iteration of the function calling loop. /// /// - /// The underlying provider is not guaranteed to support or honor this flag. For example it may choose to ignore it and return multiple tool calls regardless. + /// The underlying provider is not guaranteed to support or honor this flag. For example it might choose to ignore it and return multiple tool calls regardless. /// /// public bool? AllowMultipleToolCalls { get; set; } /// Gets or sets the tool mode for the chat request. - /// The default value is , which is treated the same as . + /// The default is , which is treated the same as . public ChatToolMode? ToolMode { get; set; } /// Gets or sets the list of tools to include with a chat request. @@ -165,12 +165,12 @@ protected ChatOptions(ChatOptions? other) /// and polled for completion by non-streaming APIs. /// /// - /// When this property is set to true, non-streaming APIs may start a background operation and return an initial + /// When this property is set to , non-streaming APIs have permission to start a background operation and return an initial /// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with /// the continuation token to get the final result of the operation. /// /// - /// When this property is set to true, streaming APIs may also start a background operation and begin streaming + /// When this property is set to , streaming APIs are also permitted to start a background operation and begin streaming /// response updates until the operation is completed. If the streaming connection is interrupted, the /// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API /// to resume the stream from the point of interruption and continue receiving updates until the operation is completed. @@ -189,10 +189,10 @@ protected ChatOptions(ChatOptions? other) /// This property is used for background responses that can be activated via the /// property if the implementation supports them. /// Streamed background responses, such as those returned by default by , - /// can be resumed if interrupted. This means that a continuation token obtained from the + /// can be resumed if interrupted. This means that a continuation token obtained from the /// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption. /// Non-streamed background responses, such as those returned by , - /// can be polled for completion by obtaining the token from the property + /// can be polled for completion by obtaining the token from the property /// and passing it to this property on subsequent calls to . /// [Experimental("MEAI001")] @@ -203,17 +203,17 @@ protected ChatOptions(ChatOptions? other) /// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation. /// /// - /// The underlying implementation may have its own representation of options. + /// The underlying implementation might have its own representation of options. /// When or - /// is invoked with a , that implementation may convert the provided options into + /// is invoked with a , that implementation might convert the provided options into /// its own representation in order to use it while performing the operation. For situations where a consumer knows /// which concrete is being used and how it represents options, a new instance of that - /// implementation-specific options type may be returned by this callback, for the - /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options + /// implementation-specific options type can be returned by this callback for the + /// implementation to use, instead of creating a new instance. Such implementations might mutate the supplied options /// instance further based on other settings supplied on this instance or from other inputs, - /// like the enumerable of s, therefore, it is strongly recommended to not return shared instances + /// like the enumerable of s. Therefore, it is strongly recommended to not return shared instances /// and instead make the callback return a new instance on each call. - /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly typed /// properties on . /// [JsonIgnore] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs index ea513d2f073..6f7ca4eeda2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs @@ -12,10 +12,10 @@ namespace Microsoft.Extensions.AI; /// Represents the response to a chat request. /// /// provides one or more response messages and metadata about the response. -/// A typical response will contain a single message, however a response may contain multiple messages +/// A typical response will contain a single message, however a response might contain multiple messages /// in a variety of scenarios. For example, if automatic function calling is employed, such that a single -/// request to a may actually generate multiple roundtrips to an inner -/// it uses, all of the involved messages may be surfaced as part of the final . +/// request to a might actually generate multiple round-trips to an inner +/// it uses, all of the involved messages might be surfaced as part of the final . /// public class ChatResponse { @@ -69,8 +69,7 @@ public IList Messages /// the input messages supplied to need only be the additional messages beyond /// what's already stored. If this property is non-, it represents an identifier for that state, /// and it should be used in a subsequent instead of supplying the same messages - /// (and this 's message) as part of the messages parameter. Note that the value may - /// or may not differ on every response, depending on whether the underlying provider uses a fixed ID for each conversation + /// (and this 's message) as part of the messages parameter. Note that the value might differ on every response, depending on whether the underlying provider uses a fixed ID for each conversation /// or updates it for each message. /// /// Stateless vs. stateful clients. @@ -95,7 +94,7 @@ public IList Messages /// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained, /// the token will be . /// - /// This property should be used in conjunction with to + /// This property should be used in conjunction with to /// continue to poll for the completion of the response. Pass this token to /// on subsequent calls to /// to poll for completion. @@ -121,7 +120,7 @@ public IList Messages public override string ToString() => Text; /// Creates an array of instances that represent this . - /// An array of instances that may be used to represent this . + /// An array of instances that can be used to represent this . public ChatResponseUpdate[] ToChatResponseUpdates() { ChatResponseUpdate? extra = null; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index 8b24b5c6b19..c4a9f8ba97c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -22,7 +22,7 @@ namespace Microsoft.Extensions.AI; /// The relationship between and is /// codified in the and /// , which enable bidirectional conversions -/// between the two. Note, however, that the provided conversions may be lossy, for example if multiple +/// between the two. Note, however, that the provided conversions might be lossy, for example, if multiple /// updates all have different objects whereas there's only one slot for /// such an object available in . Similarly, if different /// updates provide different values for properties like , @@ -100,12 +100,12 @@ public IList Contents /// Gets or sets the ID of the message of which this update is a part. /// - /// A single streaming response may be composed of multiple messages, each of which may be represented + /// A single streaming response might be composed of multiple messages, each of which might be represented /// by multiple updates. This property is used to group those updates together into messages. /// - /// Some providers may consider streaming responses to be a single message, and in that case - /// the value of this property may be the same as the response ID. - /// + /// Some providers might consider streaming responses to be a single message, and in that case + /// the value of this property might be the same as the response ID. + /// /// This value is used when /// groups instances into instances. /// The value must be unique to each call to the underlying provider, and must be shared by @@ -119,7 +119,7 @@ public IList Contents /// the input messages supplied to need only be the additional messages beyond /// what's already stored. If this property is non-, it represents an identifier for that state, /// and it should be used in a subsequent instead of supplying the same messages - /// (and this streaming message) as part of the messages parameter. Note that the value may or may not differ on every + /// (and this streaming message) as part of the messages parameter. Note that the value might differ on every /// response, depending on whether the underlying provider uses a fixed ID for each conversation or updates it for each message. /// public string? ConversationId { get; set; } @@ -138,9 +138,9 @@ public IList Contents /// Gets or sets the continuation token for resuming the streamed chat response of which this update is a part. /// - /// implementations that support background responses will return - /// a continuation token on each update if background responses are allowed in - /// except of the last update, for which the token will be . + /// implementations that support background responses return + /// a continuation token on each update if background responses are allowed in . + /// However, for the last update, the token will be . /// /// This property should be used for stream resumption, where the continuation token of the latest received update should be /// passed to on subsequent calls to diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs index 570eb7ef497..f4e0141ac94 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/IChatClient.cs @@ -62,7 +62,7 @@ IAsyncEnumerable GetStreamingResponseAsync( /// The found object, otherwise . /// is . /// - /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , + /// The purpose of this method is to allow for the retrieval of strongly typed services that might be provided by the , /// including itself or any services it might be wrapping. For example, to access the for the instance, /// may be used to request it. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs index db547a7b8fa..e5d8459351e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs @@ -55,7 +55,7 @@ public int? Dimensions /// /// /// The underlying implementation may have its own representation of options. - /// When + /// When /// is invoked with an , that implementation may convert the provided options into /// its own representation in order to use it while performing the operation. For situations where a consumer knows /// which concrete is being used and how it represents options, a new instance of that @@ -63,7 +63,7 @@ public int? Dimensions /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options /// instance further based on other settings supplied on this instance or from other inputs, /// therefore, it is strongly recommended to not return shared instances and instead make the callback return a new instance on each call. - /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly typed /// properties on . /// [JsonIgnore] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index 2bb9841a65e..ab49eeb7f24 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -90,7 +90,7 @@ public AIFunctionFactoryOptions() /// -returning methods). /// /// - /// Methods strongly-typed to return types of , , , + /// Methods strongly typed to return types of , , , /// and are special-cased. For methods typed to return or , /// will be invoked with the value after the returned task has successfully completed. /// For methods typed to return or , the delegate will be invoked with the diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index f57dbe2dd3f..0e93a9bb1af 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -60,7 +60,7 @@ protected SpeechToTextOptions(SpeechToTextOptions? other) /// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options /// instance further based on other settings supplied on this instance or from other inputs, /// therefore, it is strongly recommended to not return shared instances and instead make the callback return a new instance on each call. - /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed + /// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly typed /// properties on . /// [JsonIgnore] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs index d8551fe6586..366dc66f77c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs @@ -37,7 +37,7 @@ protected AITool() /// The found object, otherwise . /// is . /// - /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , + /// The purpose of this method is to allow for the retrieval of strongly typed services that might be provided by the , /// including itself or any services it might be wrapping. /// public virtual object? GetService(Type serviceType, object? serviceKey = null) From 51e981bfa8413a073043e2ade5c6079418dfaeeb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:44:22 +0000 Subject: [PATCH 377/472] Support DisplayNameAttribute for name resolution in AI libraries (#6942) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub --- .../ChatCompletion/ChatResponseFormat.cs | 2 +- .../Functions/AIFunctionFactory.cs | 6 +-- .../Functions/AIFunctionFactoryOptions.cs | 2 +- .../AIJsonUtilities.Schema.Create.cs | 2 +- .../ChatCompletion/ChatResponseFormatTests.cs | 35 ++++++++++++++++++ .../TestJsonSerializerContext.cs | 1 + .../Utilities/AIJsonUtilitiesTests.cs | 37 +++++++++++++++++++ .../Functions/AIFunctionFactoryTest.cs | 33 +++++++++++++++++ 8 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs index aaca11c4979..76f8a486ad1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs @@ -88,7 +88,7 @@ public static ChatResponseFormatJson ForJsonSchema( return ForJsonSchema( schema, - schemaName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"), + schemaName ?? schemaType.GetCustomAttribute()?.DisplayName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"), schemaDescription ?? schemaType.GetCustomAttribute()?.Description); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index bd382079c80..d9318f98585 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -120,7 +120,7 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio /// The method to be represented via the created . /// /// The name to use for the . If , the name will be derived from - /// the name of . + /// any on , if available, or else from the name of . /// /// /// The description to use for the . If , a description will be derived from @@ -297,7 +297,7 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// /// /// The name to use for the . If , the name will be derived from - /// the name of . + /// any on , if available, or else from the name of . /// /// /// The description to use for the . If , a description will be derived from @@ -729,7 +729,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType); Method = key.Method; - Name = key.Name ?? GetFunctionName(key.Method); + Name = key.Name ?? key.Method.GetCustomAttribute(inherit: true)?.DisplayName ?? GetFunctionName(key.Method); Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; JsonSerializerOptions = serializerOptions; ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema( diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index ab49eeb7f24..5caef21900c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -39,7 +39,7 @@ public AIFunctionFactoryOptions() /// Gets or sets the name to use for the function. /// - /// The name to use for the function. The default value is a name derived from the method represented by the passed or . + /// The name to use for the function. The default value is a name derived from the passed or (for example, via a on the method). /// public string? Name { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index e905ce93859..ad15c62aef8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -80,7 +80,7 @@ public static JsonElement CreateFunctionJsonSchema( serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; - title ??= method.Name; + title ??= method.GetCustomAttribute()?.DisplayName ?? method.Name; description ??= method.GetCustomAttribute()?.Description; JsonObject parameterSchemas = new(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs index 9ac67ff20dc..420871ca9e6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs @@ -169,6 +169,34 @@ public void ForJsonSchema_ComplexType_Succeeds(bool generic, string? name, strin Assert.Equal(description ?? "abcd", format.SchemaDescription); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_DisplayNameAttribute_UsedForSchemaName(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema(TestJsonSerializerContext.Default.Options) : + ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options); + + Assert.NotNull(format); + Assert.NotNull(format.Schema); + Assert.Equal("custom_type_name", format.SchemaName); + Assert.Equal("Type description", format.SchemaDescription); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_DisplayNameAttribute_CanBeOverridden(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema(TestJsonSerializerContext.Default.Options, schemaName: "override_name") : + ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options, schemaName: "override_name"); + + Assert.NotNull(format); + Assert.Equal("override_name", format.SchemaName); + } + [Description("abcd")] public class SomeType { @@ -178,4 +206,11 @@ public class SomeType [Description("hijk")] public string? SomeString { get; set; } } + + [DisplayName("custom_type_name")] + [Description("Type description")] + public class TypeWithDisplayName + { + public int Value { get; set; } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index d011f8a9030..6f087bbc56c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -37,5 +37,6 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(decimal))] // Used in Content tests [JsonSerializable(typeof(HostedMcpServerToolApprovalMode))] [JsonSerializable(typeof(ChatResponseFormatTests.SomeType))] +[JsonSerializable(typeof(ChatResponseFormatTests.TypeWithDisplayName))] [JsonSerializable(typeof(ResponseContinuationToken))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; 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 d83e4ad716b..cb61fcf7086 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -423,6 +423,43 @@ public static void CreateFunctionJsonSchema_ReadsParameterDataAnnotationAttribut AssertDeepEquals(expectedSchema.RootElement, func.JsonSchema); } + [Fact] + public static void CreateFunctionJsonSchema_DisplayNameAttribute_UsedForTitle() + { + [DisplayName("custom_method_name")] + [Description("Method description")] + static void TestMethod(int x, int y) + { + // Test method for schema generation + } + + var method = ((Action)TestMethod).Method; + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method); + + using JsonDocument doc = JsonDocument.Parse(schema.GetRawText()); + Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement)); + Assert.Equal("custom_method_name", titleElement.GetString()); + Assert.True(doc.RootElement.TryGetProperty("description", out JsonElement descElement)); + Assert.Equal("Method description", descElement.GetString()); + } + + [Fact] + public static void CreateFunctionJsonSchema_DisplayNameAttribute_CanBeOverridden() + { + [DisplayName("custom_method_name")] + static void TestMethod() + { + // Test method for schema generation + } + + var method = ((Action)TestMethod).Method; + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method, title: "override_title"); + + using JsonDocument doc = JsonDocument.Parse(schema.GetRawText()); + Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement)); + Assert.Equal("override_title", titleElement.GetString()); + } + [Fact] public static void CreateJsonSchema_CanBeBoolean() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 8094d4230ef..459f03028ab 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -237,6 +237,39 @@ public void Metadata_DerivedFromLambda() p => Assert.Equal("This is B", p.GetCustomAttribute()?.Description)); } + [Fact] + public void Metadata_DisplayNameAttribute() + { + // Test DisplayNameAttribute on a delegate method + Func funcWithDisplayName = [DisplayName("get_user_id")] () => "test"; + AIFunction func = AIFunctionFactory.Create(funcWithDisplayName); + Assert.Equal("get_user_id", func.Name); + Assert.Empty(func.Description); + + // Test DisplayNameAttribute with DescriptionAttribute + Func funcWithBoth = [DisplayName("my_function")][Description("A test function")] () => "test"; + func = AIFunctionFactory.Create(funcWithBoth); + Assert.Equal("my_function", func.Name); + Assert.Equal("A test function", func.Description); + + // Test that explicit name parameter takes precedence over DisplayNameAttribute + func = AIFunctionFactory.Create(funcWithDisplayName, name: "explicit_name"); + Assert.Equal("explicit_name", func.Name); + + // Test DisplayNameAttribute with options + func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions()); + Assert.Equal("get_user_id", func.Name); + + // Test that options.Name takes precedence over DisplayNameAttribute + func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions { Name = "options_name" }); + Assert.Equal("options_name", func.Name); + + // Test function without DisplayNameAttribute falls back to method name + Func funcWithoutDisplayName = () => "test"; + func = AIFunctionFactory.Create(funcWithoutDisplayName); + Assert.Contains("Metadata_DisplayNameAttribute", func.Name); // Will contain the lambda method name + } + [Fact] public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction() { From 74d57c7a2d973dc02b81f1b9b32ad67f41036b2f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:39:13 -0400 Subject: [PATCH 378/472] Fix EquivalenceEvaluator MaxOutputTokens to meet Azure OpenAI minimum requirement (#6948) * Initial plan * Fix EquivalenceEvaluator MaxOutputTokens to meet Azure OpenAI minimum requirement Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Complete fix for EquivalenceEvaluator MaxOutputTokens Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Remove accidentally committed nuget.exe Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub --- .../EquivalenceEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs index e3f34d75982..8ecd33c6188 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Quality/EquivalenceEvaluator.cs @@ -52,7 +52,7 @@ public sealed class EquivalenceEvaluator : IEvaluator new ChatOptions { Temperature = 0.0f, - MaxOutputTokens = 5, // See https://github.com/dotnet/extensions/issues/6814. + MaxOutputTokens = 16, // See https://github.com/dotnet/extensions/issues/6814 and https://github.com/dotnet/extensions/issues/6945. TopP = 1.0f, PresencePenalty = 0.0f, FrequencyPenalty = 0.0f, From 96f58bed5f5816c5c70389705b25d8fb93572c90 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:43:35 -0400 Subject: [PATCH 379/472] Support DefaultValueAttribute in AIFunctionFactory parameter handling (#6947) * Initial plan * Add support for DefaultValue attribute in AIFunctionFactory - Check for DefaultValueAttribute when determining parameter optionality - Use DefaultValueAttribute value as default when parameter not provided - Add helper methods HasEffectiveDefaultValue and GetEffectiveDefaultValue - Add comprehensive tests for DefaultValue attribute support Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Add test for DefaultValue attribute precedence over C# default Verify that when both DefaultValue attribute and C# default are present, the DefaultValue attribute takes precedence. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Add test for DefaultValue precedence and update gitignore - Add test verifying DefaultValue attribute takes precedence over C# default - Add .nuget directory to gitignore to exclude build artifacts Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Address PR feedback: consolidate helpers and fix formatting - Revert changes to .gitignore - Fix formatting issues in test file (remove extra blank lines) - Make HasEffectiveDefaultValue and GetEffectiveDefaultValue internal in AIJsonUtilities - Remove duplicate helper methods from AIFunctionFactory and use the ones from AIJsonUtilities Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Rename parameter 's' to 'text' in test to avoid false positives Use a longer, more descriptive parameter name to avoid false positives when searching for the parameter name in string assertions. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Unify helper methods into TryGetEffectiveDefaultValue Replace HasEffectiveDefaultValue and GetEffectiveDefaultValue with a single TryGetEffectiveDefaultValue method to avoid resolving the DefaultValueAttribute multiple times, improving performance. Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .nuget/nuget.exe | Bin 0 -> 8974384 bytes .../Functions/AIFunctionFactory.cs | 8 ++-- .../AIJsonUtilities.Schema.Create.cs | 33 +++++++++++-- .../Functions/AIFunctionFactoryTest.cs | 45 ++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 .nuget/nuget.exe diff --git a/.nuget/nuget.exe b/.nuget/nuget.exe new file mode 100644 index 0000000000000000000000000000000000000000..ed048fe88924912b4b5caf05f0d3cd0ea6513a27 GIT binary patch literal 8974384 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%0AYb+RgDWw_A@V2bWD-`D9nUHeXJ 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 z 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

    6!wXQi9=zZT9`N#_Ad()hr&Ly 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 s)8ANn3tK_nf$I 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

    E>We}uzNHL&$^(8FZ&>b_aq`4(-ZK_I{E`*1 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%0DoEn8&QzO_RVBB38HkFWvzv#o*=5K| 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 S}#~ z6Vam(NMlLmvqU>j@=+)Iwx6rWN7M|^>e;h3!{V!DY=w}=W6?{7#~E0N7f=dxYfDT2 zroQF9R2vzMu{ii}u7v}d7a&*aU`DS^W8_|2wlYmCwj1PehrVfOIr*}t^%CWF2K$S=bBM~zKBz<-6R2(qC&v5*L44$4eoPZYX(z=rK|o97c%E|P zMUJhoJ%4ve?jZbn!3*%Bez+!DE#Gpm7T2g~3>&!5YOA&lFE8^P@L2`Dgz@l<8-@^8XIN~y|XvV2y?p0vgZ)@7GGVP5e zqH8x1=-EvLVnc^OZ0Hb(w6{KrK5+6!;Mkgh%~powb8@1MlQ z6p1U}c~2r!sD1xN*WHxvxQMQFa0CX7%H9;fxyhFY7@PD#ry<$kMkL?Kt-iQ#_Hcx4 zs*w+r#+r(lWf_`k^Rw_`i#9Wdl3yL3@u+RA3onLjfrV&@iLC^+6Ykb~W^1I$2T|tq zYHs`#-@P1WZj?kFa;OS@zF#ktplZ{$aeKv!O%)`~CA=7u1&vG_> z7qfo&b1@TjYx4lXS-v4?pK1BN!SdZ!3dgnE3H0pt0#kODz_i^#Aj)?~MbdV*BEH>8 zk&NA0keuB{U{P6`e``V0W!{p4RvYtWzx?Nrq4fJ9PO8?;-{zqpS=w@VvBItGWy4g~ zov0lcfp4+ZkcTf3-wWOb-`WmNEBnDavYe42`H>;conZ4Fc!x7Eh{RzfyBqX}C)X%Kk+^vuN$71U9Q(Ne+a%mC+|5Df z2lV?MiZ?&tC-@M~I=MVQ6MO``UPEVtf5Cxg!o?il>Hm$S!J_EQm`3*tB0vcVMsYP` zRd2kroa9@3rZe`y_*?sUXZdRskM;M%f7G97T}Aw__1EIu3u%9({@B*nR%K*Zk`X+< zcN8)qtLk{YoNa%Uq1XE>!;kd9sT0*hgR-qrHR6_@DDs>ABUcH)!J%^SOix*fANP-j zwOlNUy3P=1+?uftw{%vT7vrZn%ib94vqARw{nA&T9O+lsqnL?wU6SjTI$2GP?DGYY zplZ&fwH@UQkUwFCzc3n#uVQ`Q&Qy1rhlal0QaqhM(ZaUI=p3ZeWg{ z0tEL#6p%Ql&y<6kAw%S5l9g_zRT-X%RJzGM_!Q|dKHpR2Q3PikdKx!44o4=?awH@B z=#kgO921OP%y1qQ3_u<8Z`3Hq$12hQ@|gs;TgB(M=zlt;nQNILn|1UZ(mw2BUSr29 zubKI4(Me$YJxj!#0Nu1ZGxMK;woF7jQD{5nJI}?**$*%v0s@8 znNyHC{~f&f6@kCcXPBFP)IuoY3QVrxe3l5pME5hdwus``CvdLqo~>mV&N-%(xmO*6 z-={dndKV!V?dKruSzusJ#SicCV5J(HFRy7>3Fcf7g-e;!el_5ZdPJ~|Q>fbAY%PP% zndNyl2(*Fpt?O6N%=-G>P(|Bt>9pOW2kd5?7oT}@WfG4zfHPBu^K4j$xr%v2USoJQ z-Pl{nF+08n#0gt#a0-Z*hQ6Z%(KnwLABc|9S!-IdMi6`k8!h!N18e;pSm!#3>Q)fH z05O_@)t+=9zDMh@MtlK`_}6+;Z67vuM(?lf1LVGz^76Yy5F`ZJbump4(8jo}=2Daj z^UgU8no9xa@9Mh17;ytI(QsLSh^mzY`&rfW$Quk7mU+d z&F)No4t{D=0`i-qgyj%?9ac;>LAhn*w?3QSA@*rdjy2ivYGx`OJPH2hAU?Ig2u-Pe zl(fdc3pFW?qhqPGvpHlO1Q}7sRzb1ZJ!A~#+C2q&c8#P&!mH@YwN$b!TC`Opu)Mq!$tkax{*6IOA*L;N9#?*+K9h>IR%j_Z!$s0+VA z__j>&Ey1whI{-Wqxe62D99q`=8v@J_>#H36N*3y-=)e%*^2@ zg^jA%;sNDGn=%OpFE|UGr?!>8*!M`$DFbj<@FKnf!kyCoAq%hyAfJi;WklP#GC%|) z4VLRsQN^w3EWDH!n|WAD|JTDj5`}p%5_h@tpeE zL#4fQ?GXY!dziqKJzQYg76sxvcb*_Qdx*fI#xuWY14)+|1b)$6O@s4Wq%)f3(aBjp z%>2NU1D?Cm$Sa3xZa`AI+sV}!*EGU`9fRg-`eTd_|ClrcUffc2Hj3N4hJE)9HRej- z;S&hk&Xe7%xXuXln%ed^l=onSO^9kAhAwdK{855`|0oRfJr4lfA8 zz&S|+FJy9gjIZ?)+$YSYau@Em?PFW4f`v#kPCcBsI#9VQSqJzS8S z9U-u&@r+X|vw(D&k@%6a#kymv8O!x%0DO`8xGpWP>5f7;J6hPdb~%Bb9V0Mhmlv3} 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 Sn!Ohp;N1dYW~nx_F)() zKHr4B15}v059oOjIETN4!F&}}h3-XD^@GiWzDxsr(w9kIkamtc(vPshQDOyloNGOC z{(x~zf{z~MgElQbyVoA2-(p`bGUi0DiZ_+Sgs?lS!|p7D{jU)APKphtd2P3#`;Ej! zy5$9QBkWB!OkOl>Uxv+sYZ9uXa|!AL>GozAV7>v*9-IVp-cb^6&jnA*oD$igHNiC} zXB)CqKp| z@%-qXtd{&Jr3sI{^g)?&SL}n|wUhr0S11|RhujeosmvLn@QC|-@I#sPQdir|`6=)n ziayt{BaTWk_g_SUg3Ccie~GWcZw)Bz!KyUGN_c5cgupU;GHhtDQxiMBS~ruxHhoOO zqXf%d4m%*;!{TYqy^wS1A}z3nNW}?^(jPa)EW%K+wNN2VLC7lYO)3m539(?5GPPqr zZFs@Tc9j_F=tAg)Q!!C&r!5;Om5^(QiO>P66fT zag~MBNy;J4+`!a_EhHmqaQD^=Yejhrqkgd2c_V=E&y?w6K)`4cey_&2@%#J`UQxQQ z>Em8JASlT|mK?x!oNaWD6UdY;w$FZ=5wyqen)@cacX#FEywxaSFaUFqz&L1g$o`q` zX*Dw#oCW7Q$g)J*?$v-mrM)RLWl?h_d$Y>kY&m;dEPH9YY{LczvbR0u;_L=xFt)FW zDRXPM@3F(7w`iAGcUseV@z!=n*aPYIxyInD$Zz3@M(hX2AqcILkbMPe6I4MI)z@2~ zRR?|WH-LzLgdmKFN8uwHZp8cxZZHpG(Q z-jVU;jkm>&!yfE#)SG8S;v%3A&cRR4pFB5*NR+}4K8XPAx6$%Q8@j_y1@lR>j{PFP zwDmC2Ai^c}u6$wN-t4>!aYq0T&bZObjncsgfHq}h%D!HA#(FsYE=DyDyc`8Yx9s{@ zrCB=+IXlOkbYt4fSw^+_$o}R4CUzt^dMUV{MaJEtwc{}??`S>2hu zBf+@xR@U8-<#l&1Xh_uEgCvyI@&I4hN7Spk-EH0NmaMzC*t+99DXhCysMTcMF=Y51 zFbIVA{XUKinbH&ZotX=s#DjZLE9>q8@=ojSDI^Mhi65oe)E(iV?w-ZN)E&l`BZDt6 z-_&q?LD;BNym%|vK6Vj5bxySp~F5_Mg_0?$U#7}!wEIR+KDmEu_~bLKnKDQtCkiShbORl0`XSSp+Q7MB$|87H zQbUao=ozhyOnGHAyICD1%#cufkT3~aT=?zEVBaW15i3pWtjVEg+A*McQAr@o2fXgX<o_HoenV~}5Pn)nE5MgpzMv$rmXG;4*=@}(6;qx(m6&Kl=*e|`& zW{=f~KQ9f0!5ymfEAKLLFT!!4TZV!peY86og8Vo^<3>Up`Kj$2$NNRC^waPa;3xHYebm7$92^uLzLKi~`MrB2!(+O&_DwC#Pf9Umn-1!cxr z<1BITB+On^xmYz*miUXNty>Q%nyZ=e)aV5{<*605DcmEn!5%1Vog_7*2I>M%W&Pkc z^&y92;LgtG;LFk~d?AxIK368<{9Ns2WYCdy8WUjDV>3Oe%~%Vdd{F9s>0Hx(VHf9Y z@a(PNnHgu}BGu%FGPNkPX!;@<_FmTMo;Z!WFMV3qO_^*|nXEI*+;m}`sUdk>CvQ*P zSl~}JW>j?R0NHe*>0|eCu0=hZTBjZ;!>_(tKpQZ{AEOP_Ws-$aWdgsEYDx7q&M|1Y zIE{UbS_?!ccoKc{y6FTzk=`6*{>jV=o#3ZcKu{+*-o!svE#B$`r-PpQI>8!b7oFe< zzQlEcQhDAAo#1hsm&Huz1j~^370?M-hB0-5Gc0WzS0^Bk)<-A!g=O;3_)=ddc*>^z zxk=lOvm$uc#SqFIcG=yVKo5WebNNik*O`)EK2w7EoHJje2N?Yr(G|QwUgA$jox;|Kl@U7+-rX4jAI}6I?c6++xkX^9)e~L@ zrmC$#m4uf#trrd9Wzkxp7}OFA=o=<#y{o|3BKC9cMjb3q)`6%9((HL^+N(-i`*+t9 ztOjE0EuCmOY14G->M-onb=>ux*?2z-byw!+$eXMcaCo@89PJ*Qt!7%2Uv-eTX+Opl z3(-OTz#dVJn&Y%fJR_q=Ob7W*6%cV9-?TCaUJBG^1Kx~ z$g?&ti!*W^Dvw^*L2y-CjEn26gRBG}U)Mo!?LgU@1$F{iMWZ6BiVm`_Rglm@ z&P&$8>pDmsE#ReDlqz(Pc)hKc4zdb$S4{^Iz~j(C&ewh{)Irv=M^qhT#`xAoUXalv zri1*g3W&H4^7m@-RtLEN^wifu{y}`yLH@;;xDN96^1Kx~$Ukjf7BiuPT!_4{fDXbk zjH!dX!_u~KbrAAseRPlps=BP87s=%MI*4o2;s6#&+uk?J`3>@1gfd4u2z-HGRd$e+ zHH64yqYW4Qtew0JE|&0(`X?58a8HdIV2t4dOj0@Sb6PMbe5aOObc^mqQ(klS4cvAV zTU5d2M`^>Iv~tI)WLBA=3@Lwha(H#}#o$jBouM11Ae}M)=h`jSnYGH-LFXLs^^zoC zRS#g(d)*dM;oUfv9smnxT*lLFzL9ZXWjte=K&u(`BN_MQE{AuaF1ow9hq61`L)qr? z`XzQGjS2-9*w{PdVrB-d)2y>O+ReMQFOl&*6`Y73nH~@==xvjuJoD7{fAFPuewVIw z7%=GIvlU<*q~Ur;l%}yVjUDS(gSPr(eU5lJlr-~2%cVXPmzL+P80(vCUKTbn)?bFa zufSN(GK@LaUv6pJxMMwev_50~B+KLpe5pUyx7xH5Ot!FLz^IjXLmT{1wh)V3%inY!^7V z7PQKUZuMUs)34FK!;I-CnK6AD`kZwc(bL2$Mi0~#K-jamglV))2G_JENo&-zYcj{fiazB7;{X&-qN;l$8_>&ea7@| z%jD^NsXwOo*t9cDT05rSfHJ?f#`Gsq!un(S`?SpeQDeGrWsEWX{ou>%W4d_URM_%+ z7FLYuwOE=M({D`j^z||Qr8$C@PuByfggRq7jz@BCLLJl|%?Y9UXwEV3Ry&tIL1xR# zIEZ6RoQX`yL7e@sol_^%&Z?R=6~t1Z2d0*g3VN%hwPW@NP>%Xz_7=!4WA>JOiI3Sg zm*=e*vjdx#G$h9CTafn^7_(W1F~{s%Eo~cj%qEZ4XUsm#GWlS>)E~1K*|dk4wALr~ zgDA6+81YBh0r5r{$LWtUTOh{Pg0`&!BEA;XS1sPkRj(n*vmqnX)CzF=`C_S(V z+=ViSx@7m}uN|M*I?F^Jj)N*+CbEl4Ror%s{LXXs0Ke{5eo;cet^X+h*m7kmwzMQ1 z4M+5K@s(W(p56nVR_d@|@9Vy@b~Ud;x81Ztw|z;~Vf5eqoKK-XK9sBv(SN0Dc-^42 z&IT>JR8?(n4(qVyN|rRK@|7%`ygD?F0Xk0WI@i6Z$L`I!&b4{m78b2@369$9Tx-q1 zpl1d1UZBd;YIwf~HpGtm)P!N<9~nfWO^>yiIn=KpjhIw7J0+kLv1nz|PEp!3g| z$>f}X?f9+dz%#ql~_1XylldUNMt$lXU&?sQu; zRGG?cKCimXP%NS;;||WYkgJa-$rKshSy8MeA#_p}*5SFL>P<(vh+ zelf{cu}fI4zV1j{<(Sa1<_f5Yxcy=5cIrDjp`CsS_0R>gLRWRG!t?aPuWnVa^{IPw zzpU+Dd~N7CdlCku@&!7!tJ=P^M{tk3hB5V< zuUpzSu6{!vt&e`Q)H3;GzChk@t$kM6TW#7?Oj^6g<{Kz;sNZyVyvkIYI*TVwRTfXe zIk)by`KFegwT+U$_SBm?3n9Xt@`VsPH*W9KJvQG0e=79@uXwr3(S~+pO=H9f>FBPpoD~l58-(uRwNt4ys)Ep(l}diIv4%-eCH&Tao<2lT3{QdL9XWjQoE4I`I5i}}p)kf3d#*|{75z5^T`xzyyjrwTLV z%*&KLW_%UG=6B*Jhz_LA`Enq2bCEaO_V5VM(0`goShqAbds%zC2@gicnFKthy3NgF z!)16%^`r*yi^Nt_21wrxm1C$mmiH_&3AvZ^i8Fj>IG+I}-$gmX@$=QSLDlGG+c=ZK z%-tHpbOFDSHpta{|ZNqIqsE z+tT;T+7btpur0BrXj_U7$wgb@A-QF3i7BPutJaoA!?tu9E-aIchSHW;xN6F4{kAk} z+R}(=OM&w!%JHzaC6fQzQ^rhx&9lGXfwKM}Dl4va-S8_~upHGi`sr%7G5UcP?U#$r z-VlICKv%3E5Z&Z;o5gzAEKGeLYIOn_fMh}CJP~5 z<6BJ`Zm0wir}15J8hipz9-#mDyPA&pnXX{=Ir^=Vv9l)~qwhQ{lw(uh?|m6Xfk zH2w%Q*3rpA8n21dcuOS$(iqjH5toFd$l9c(;XQF0ek?T99Q#5Vu8q@BgHI6+`w~Sc zPZ#qGeUd#B{wKf*dy%eFy!12SQ`PUG!QmYF*0kJ4tb6E5b+uv5^p$#+{-c(*4dWwR zNt@iWVrUChSZN*A)Yk-UKULcFF!_*gtH9ahS^5mwfOo)9;>M)yF-sdqmykB9A4%Fm zg(SS6CQ@P6a(K+f$BXll^uGvsKRV z{~2I*zWc~PGIypm^ZDG|@uFe05kD;HlFoxY4` zVH2vsGg0e>XOUVT<5|=|g=d0Sc*ceZp7F*F@Qjm*a-JnHSL2zW7M?|@g=f{tlQv3{2vGWVv9A!koj^6V9An;S&V zYVgd;Sz|Q3bLOgAX3e%)E@yStR_?Uj-v_n(tG5nwmrZ-y*lF*!X>T7p?L9W_9VRXB zmsyKG?3bux)A^aMchQt3ubBho-PD*T)raHJZq6XO&8L+g6gZ<){%38D8Lv-+b&RR9 zp~LjMJ3BML&tFCI75PWKX5>a%@a3p|g~PhE*Ik^$Az!}+zhd?%j!#iP_WH)FdIto%sWC4`uzvOnoR5Roze)0% zedHqBN#ZWD*DoU|zutH(za{InRDB!Nc)gnHLkY*XeVh^0;ct_5$o4X2Nmz@oZ&a)^ zu5E~+(b!_9shQG^K3X2=|tgM~1f8{M*o0XYTl*W%6hF0@l|_`#GET0h8AH@ckKOHgmiw zOI}Mpe7UiG`2M10XKnwd`S1yM#`58N4*V(8pP>)D?!$+>HR|~ARcEdCpX$T+e3GxC zE5Gi;Cl2)+(uYqfqK*&W>A@GYGqgT@|A%_1>%%7yYy0pWipBFU0c6?ov-7A~8J3Ou zKF7%5*Gv|3>ob00Um-z&@?{~cy>F`XWh8$g>Zf}C_2P?1&8Mf3Q3aOTw{Bxw?^8xVEKrXIs1Pw&Fm7;0h#e_3a&(dQ3vs+#Oh7BRox^U!kUh zuOgw%EEhER!Er>S;LK)Wo1hwWUX$6HZq&Ph>ri3t2A*@?^Rt{(=kPY$Vc`Pg*!{jw zO?H+A*P;52(?;-h5gV+3d(J^f_c!p^6aNWs<$fqY@Fwse_;|Ck)#SNp#0THhyx*WO zN)dB@e+ChL*B~E-CfPEZZ^}rhTNvF7P;}mZK^5u!mXY3XDjA?-Q|Af5`Mb)m&L`K+ z&#|UGXxrMRqTYp`&1SC8kj>m&#hNlmM}vtCje(bPa{C7VMFu2J*|s?=tZ}`y5pElv zIk1JZ4d&7Rh{<-STVZ#^`s~N2;0?%9@d%mUAAo-MrCxjq=9yAg%$&$P6Q|5eDf7~; z=_!K~g?VUK72LC@aofCA;MR;H`%K^9IY$A-KSgfoQ{x3+g)9YcZApl%7o35(0rsR` z@GX_$|K30{{69v9za7f(*3N$e(Z5uNIaY5B34X~|)-lA@@OwLFCggQ(QeH(Lq2J#6 zTpwChUN^vP?hx5Jw`%n+1jA}>^Naf|+EmrdYT#KN93rNMZ;j4AkO}^cJ+#YlJYMiU z2$c2e{Vs(yc~Ph8htr*p0OpIRkM7N12FK2tof{56jjvUBZR>iyvf&F(jDS2jJ4f>d zT>lsj@0COKOTA**4$|ARCUlVI!1aV{-CHBq#RLJ(a&!@fY_fCp|Ai ze^RbzjeSj-ssDoWLp0M=y#Biv+54(rw%fP5=B{TyQLYljjvN$GZKsXSo53&s=^YT`9(8^alae1-N{Q9GTAh`E?Ssyq^YO9~L`5&XsJP-L zl#lO$j7&^E#OLl+BqFtLR$;sB!rs0T%9lm?COSi_`%=q-{{h#7zhF|5^GkchX#3z4w~=+f3NPW#xbI_UT+VOuGPsfaQHV~(S>!SK^kee* zXB0ww6y0D6PkEALUeN1@!H36!w~T_12!k=7{Sh#t85wzq%fDPAqNbBN1{I@qJrR@t z$H$WY+{TpuCqWIA(8T03>^BMy<-h6GEdNrz%8MQ1^53lTe|0GTe|+WSzooAHvoPb8 z|BGRzsUrXT!S-T;|Fq}u&y(Z%y+989A8#x8C4kwv$5VJOTf9_V zLHCwTV4diM>EC2;*)-<|DEGuzTQKzf8;=I#UG}y^426a1WtT2{sT492VsfO!%Gu0*ZGift>;GP zICc*BGw1bgA+K@<@Z2}yy?c7iGl2B~;sG9nX{ONh((j&8GlQv*Hs=PUYeRi>zHP_IWx_Zjyvr#j#$7wo0_xibA!_~toB}KO$5h*mgft*yS;d)Y~cAo5; z$G@)g2Vmy9&hPnzU0nOxcJS`sv9IljwXxS)U;D%AQzuVs>_ea3xnbGx9H`&tnkAJ~ z@SIecv$A?V`o~C$-&c<}?Car49D~%xKHl<6xj{5HK4y)JK1TXS?n2{%i7iud>D_}FTg=L zxD{`5G>CsnOJm_>Tu;N0XjZypT_M;T)0Cw48#XNVX zB**||W9Ut^wC9jR($3QPoZ)!E+flGfF?(u|LqDYN0T*=*D&7ZSf)iYbw9VnYiA`=s zHgan1tYL5H_p<{7yB44p1eVz)^O)#p!00+}aW=oeVPdrJPUjIk3sL*7sJSuf={`_Y zqn-+jS}F~xjq}9r*;ujSnM$m9rp$_G+HFnS70Tk$vb<)4$y{b;C_Nz|tv3v5%pug0yU)4AQEVNJ1el>;d)tq|*|BY9r z{i@bsUaD89{c0-tzwT!n#q0xFXFB-RrR~A^Q3h$)w@j^DtySmyOT7V+=HR!KFT1$? zFA=uWbwL|}Ya^jC*=fsQdz=P5Rec*95y{hsTw2xu;za=|bvpjByuoC4_>{KJermdw zP5T#|KvLO6U)gi!qq*b^=eMAD2I#f28}^H@PIcP##=Tde*|`K|??&0fQ6oI;W))(R z8Z1M`x4*s0ZemxD1qinji?CI7K^6IfTyw!2LYb;)7FMo%%H`Vn-CDWM8qs9SLWQ#p zk6*4cCbKV@>AUeolz$VIYpw}*zB=#iuygFd$wSB5G%5G8*4??0_NP>?KWu=7@l7|# z(l=J3R9<*?2HVJHwvCWOVH>G4%Nz5Yg|arnt+l3&gnPJUNT@oE6IrHzU9~|2y2$B4 zlgj*h=^`!2Yju%kyw}l1HV0oewk{%Wht`y87FnbIT-VZO#^n0ds*6t!4pp6(?hFJo^WD8S&Mb6qd*doP%7M9?Le{oZk#gXz zH{3wiIR?0?pwoo6_oW@4GO-1D-{!2H+|}uH`I^czsn`eSI;kh9y(G}I(Cs$`6n|iQ zf^zw$BDGL2^TAFKJyZP0W)(Sgc7rzn7WODMWxUHynX|_Ag^?_|>cJ^zH@t5J zSQDM#Y2Gp7ch03Np+9|)eCTMBl0Z&aJ54&}{559jLwlUjuq@aQZ^KW~tdOxV7ebCi zoV?+?FsB@Svx*($B<=8&%B@Ma)^czfrpmZbHDI0D{45_Y3Y(2|?g* z|Bz3V17zm?G+QGd|5D=Ci8&ujZ~4h|!M0IAs*SLE9UI8*(+9Y!;yq!M!8 z&<$A0@mE8n8-f$q%JxE$-C#!mn<@1(biB zW}$6_wPxB#f2+Qmc7axghSBSl?%-%55>Z+Va$Aa} zDqRIikmhitnf-$`tWI1Tmp>AXxCBYhJ?51@2e6=3=Jy9UJ74555o{B!&d2fWBkyB$ z-i$Cy=UMu0+wU`w8EK(8H0CLwxdn(n@E$G!7Pn>6RB#i&;AUwm{fyIS5N-9v>$nw# zVir&`7c z=tVQ6g~rcF*Ce7QEzk5qzw%2*8C;2WezXRL@@(4u>D7cdRXT>LPq#9CEWU(~o}gnV zLOr1eWXvJ0rYoW7LteFCISwDbBp2BqFyX1v@dWMct=}STt0^*Ij^)+$5FYX>hrVjS z6@IxLrJY3Xzw53z5S$H80F;4gNTlsU-#?@yy~w@Z3pw%A!agJM`vd4a<6mcSjK*mL1XoNZ4?x9li{H>8G&e6&5!a+mJWo zXG=f9cWDzOpOr;$Hoxs}G9{29Jk>I#rS5hP@t{mH2kP}pL<=_wK4$+(_Z}V^{E=O; z6e8-1Wek~vWL{}He!(#CS-w-H84>_l+g{>t4ddlIRk}?AX1Fqz_Tsra1K20C-zkzV zVUG-(35Gwx%5h6KGQE~X^Ylk~Dk{|E$=f{py90C!@jBKvz>1-xEze9m7J&mHoSZM% zF*wUWC34c79ROdQUO0HFwt|CgFu1g*GH$R3z+(iKqE*Ca-C$3RSJoKWN;^9O+}yAp z`bk&tD9Y&NWc|c<2PNpIEVike*}s8qrLDorXxcdq8Rrqs>SNHDWVGn$)sN*IuCX(L z8UZvPNwEC+oa6Sh5yZZ4XCQ{~fPH6g@azeW{}7=C7?Edza$y|NO}C>$@u>* zk3S+A|HJb5Ba`tzvhnEOPe(5){X2*3w=kXPtL!Y`VF9g5lMJ5=wO9IeLNe zwl%ad$?)13o^#`QmL~GdHhGw2IBoIFckP&28ev|@--J*MpgNR}&&!xT6>L&1z6bcuh~Qnh6%=LJI++T3D$}Q(1CV|Q>D!#(WT*1)bMpcfn|A5rtE%mWy#^$Y++4wCeBb;n4>JPa};?& znz&uBt8ap{FT&0uZ*a6Z810o#MX__mE(H4}5ARL%l1SkNk2G0w2a@?EI9umtuMt9g zDU`2;is+c&%$}=swD+m~=xx9g+K&p=?MFATwqy3Ar#SP9CM^m35ebILN$f`pSx^vd z?Lud>Od|&&6w?p23w;X7L%UE{U$^roq(6u0#D?){maCRMlBjCfBT2EbN7ASv6I;q; zzDV1sS~D1N8ki?SaSbwHC$?HM33z5}+Ap1s=;0E6MoDx#SiFoGGQ~bXY0Cs>@GINu zw`H|9vQisWESsz!q#r{1+22&FPG)U>DMZ_doz9KLrgNjS$CGLUp*?_xm~+XG;C+DM zmEZ}K%b^1}6Hl4Ru6!$pcZ4iQ2>j55618Pv4I))K3&E&_F8FuRJ~RSY)$Ll!>v7n% z?u6hgIl_*X;#_hAoh2}}r?@YTqFFivb}hV9KI4`yF==UVWbP8id@eM1MU3%Ep9eP3 zZ|zAIF6?##oHs=e#y@F*jHepUwtJ#EUWw18kOOPjS6DDV26XpXpVODyh{2H5Z zR%<;~6uWpz%b9M(3C&W@x3SpRdoLx$UVX6?b6bACJ+r0*Z8X;nE* zwK=7;!I&mru5(Bl#4^F{D2&SJfU(_KTNLbLBIv2oImn*K*DH5Vkmqm?2)jLz?VPg& z_*W6X?O!<3xGUHNsgV(8m~)Y}G(xJ1l)_AD1>Q097IHG$eg3AhANNb6NEg=8ET?C0 z?L%RU(MGva<6&##x5y5Xqkb~ZHh7*7y2`J)Fj>K-^GJ|6&A?YuO#O!NS1Ekhf(&oY zmjKxN1Rp!4)s!jZlRLY}qu@irV@uNo3WD}P$@6ye9l^X%LTDeX2Dc0TWAxFufIN=% z%|AhycAHcLVjV3P0-^|ZwCr%DpM~vv_#Ft~@Wn-@{uM}zcN(VK(`ZRkA);tfesCA< z&za&N<1@jX{K}%Uor!pbEgD*TV{464c^2YofNOEW>jF3rraOLLs#I?3Cm9sFb|`ain>H+(Tj zpiF4H^_?y8ej(Cf?0p+LaHA;W>uh`l_3|o?cLsa6F#C zad{OSs};wG5;)ps0!O{PisMqj@k9d06;*Itra10P;CK(qQ7^CJxLj~NnZR*n6&zP6 zj*ld8{0Uz{y}XL!J%ZzB2^?2d!EvSH_(TH7)htK7yo%#0!SVA1j%%vmxLR?1Hi6?> z;;5Hbaa z+(I1n@+yv71;=v<9Jf}%@j=D$#RQHI5=XtfDbc%b)4cQnm_EoAiH=?jBu`-ccY~)B zm~RuzItFEAUG#R%%V9VgA2<+mk`af9PeSw+VuLQ#WQG-@j%)FGEZ1Gn1Sxm0{Hw2k za$+lAK6I^Q2PDu)`Ifbkx_CXsr^_e=TDFYB`S|edETkQ0xd-V^&|vhhn`T=Z%j);U zY2t^{w1vG3saB5 z!eDiU-CZvo+zIe*YV;AJD2y-_6zA=b0A4gLpl~h`r%FATNu?gT z+H$Q>5X*2rU-}tWp{_wVOkVDayOF=6bdSMBKEBZ%VW2CZgH<%GId{M~@hIfMTm4?L zj>}>FIiMMm1HgWa5?~SbUIk)h;U1QKgn>Ch(7qp{1H0ZRl zT$~5cRENmKckutry?2}(#nC=Kn%SKV7o-c?JxRbxz#YwjK*)r>lMqJ|CKwP=L;)t4 zUZgwv;vlW#Np*!0{o$tfPkE)ZB_{dBnJmO>}W@&V~vdXtDty6+79pT%N zksAxz(y~DUD6;3KO-EJG;+(ODm z!cp8lTq!p$k&NaxLfJ?-irWV(<;F#t(cC^HHxiEG_VG%&aVcgrH=j8r;V5qZSt&Oz zT*bIO1}}%Y@NddS!cp9?m?~QR|2hXakIW7B_^MeNfmd-^# z!e9sE=`m8+d!;GdfhIJTg4N|)EJPBjyyi``?MRqS&eH#&F|2vGShH{ETDrVxnEJo& z^Z4TDtuHt# z-Ez*Dq{^I8$38aavD|x&bAx-goU`T=l{u?Uer(QTx%wLC23K!6H<=HVbCWv!u{n?B z_G_FQ+`i@9Y{scJn$`J_&3UXQpvJkO30MRDThx>m^DmXTMH_;#IX5&1yn(^`wvN`I zt#x^oDj%=8W3>!5c^X;|k{DBr;P{3S9Na^~^JL6@9Y?&0^7T4|#h5$e?SlC0W!xkq-Po9$SlNWE zX%F9k@B}}Mes?(5*|-%RgX+?a045?IBk<@Za;(8_=BuX5{j&Rk;09@{NP;oh_aK?h zOmO@Zd)Kj}eArd_`z4BD*RX#^wirwCkd92*Sa^KCX1(H@82K-uz}QC?dV3&&8zp}_ ziR5PsYcb8}2{h*Y6D9a0g51t3!wsTg`98WBbD7z&(_kc|W00_d&X@JH1Wv!BGnU7r zL3UGJIyMQ862pi|aPv(y2_HMzo?^PW<^tE#Qt2yzIS!jTM6f(S8KL5UGe(!`OXue| zr*ZGx%m-@e>%BBLQ^!g_1@wN@kMUFV0%RR}2iqGaM_VB%Q&t#intx*mnGJ_7eGMz6 zvmwdARM3P&Gpq01sv8a+^tz3gh1%-5Ag|02!)A_zk>7t%DPy4T%nYi?s0 z#6OxcT<_p{klWiH|7+sEFqg>Py@tK7=5+VGEm&yj?#*Wr6YsO&uDrrjV5PfvomCXz zA+uQS1>U?DcCY40x1Du?#C~iG`*#N%Ca@&HN|pR@0uL47l`lKY-3Xi~!1Jm+c^s%P zTYyWdJXyEZnul(G#gS7xvco?d8gV>pMQ8huzPBCFe$KKRZ3jJ)OwA6YlGN-#Ha--% zRcQ#Tr^oaSJ28Y*9E{fVJ`#Tl~5J zYvsK&F~2OpT6u3y;En>UmG?{n`vq7n?}0T4M3d+ZJqZKu8|s72^bPF*d*~Y)fKl}g zttRmOVK!29)-IB=AbhLTX?0oAbodbK=lL!;TiuJI6XDI(uK7S_L&e)@jAm7u%wb5Puue=C{Z@oBi(22o5~kk~!z_{2*s1 zGr24F1YL8cBa!P!7T`ayN2Dz&Kpx>TpTMQbVFmc@Z6KE%lSp*V!+w-7E2p7wu4@DE zJV&H#3!8X73lD>}CE!qR^t;!TBs)^Mj&y;u&26c+wDQM#yz-akZxsJbwhfLMQ*xM; z1FPQz{i}f7j~<8Dl0!!jX#%cYbRy;Z5;&}v1x|0u9@DHznsZZkKY{5b|cB zT(oG|?8D}%voAG#CZL0Pyo8muaZZ=gRw+gpJ;Ri#wh$NZ0J|?Ve-;-ilhf&1eA>oW z_q?$z4gldDlKw?3U~Yve-peM@*NrAPR(2m=Q_@a0rL>)#{wk6Mw9g-Y9Q!kS1}I?f z@M8#%gwxK$(Y#xbi=HKn>@+8F(v$f;-r5Iu>8O1SAH%H+Y%F0K-u;33kR`jC#8V#0 z0*>VDfN)M4&S(V6#Dh z-5MAw-oG1v=U$ghKz=9nfU|B$jE7sN0u-h$#q7XFs0~s<0%<|5o7GcxFoGxT{5jW2 zb^_5|+ov6fy>Wgzq3hDTiEk?I-{^YR+fO^l+JK42`HW5KdN*=eo(wX z-mC#G*wosik8bh|`kO#8bnS7$F~t;$^LQkTbk1=+C7|h?>tGU4*tZ-^0t)-KgGoSP z=Q)@J6n4IYNkCy2IG6+!cAtGU4*!LVv0t)-SgGoSPKX5P!DC}|vlYqjmBPuX_Hgv_jseRL1?(Ns0GHV(8wooR zVmt*qmgbLKni4>o1Ng!Y!&a=gd{+~$?yRD7bU(lzOZh#EfUp*NB8Q=Bz(Z5kPIDg6oU~F|G zLUnz451f8ybfB?;#6lMxGbVaBgT*h&GdcvxU}HXe??Lo3(4)H{zyLdLLG&R@{!9sS zfLmM#QRYCn-ho8~mM&Nmp9?xd;zx+OK!P4n!-&6w?4|`#S^)eV+*AOe0Qft&t^l$E z;O`*EY(X?Z0Q?=y3gBb#=LyuDw3Vm>=wM|7nc5E`13c8TFH>dSa6)<-C@rc3M~ZN& z;eA=Pjt(ONef7@~169brKK4+41ziM-Hsew^O2iA6Gl=8E=15X#nsfXT$pZO>Bs2Bh zY=)&w@;b+zn->2a6z_quq;pvO1w!Rt;z#zw!!{WXqF)h$v=d9Of{jS?r)%sNQ`$uI zYsCA}z5G>*M0B5$V6Ue@qWkUp-9e1UNlS1EgzyMP^N4;!lI`Mrqu&abs$9QQrUTVf ze=h)+<{vZ~{REqj2NY13kD3f)=Ho%jW5>80LG(w08(@DTY_82O(_Z>Rb_P7`WmO%- zVW$omxMbf{h=cn`CX+-WC^qhLU#8;_3C zbbb8`GTT2*!w(_MJ`@*PaIr~M=IGrNG>Y3dTQX^M&o1-L3$V9NyRW9ny8!t2iF~KU z<@*M}arwTC_m#*ui0GJnGKxqCc;(4YayIP;OUggKGIe>4U;4MYm?~q=?tP2-b9a6GE zNSMV(qPLIHy4ROj{C#-Z()IX(O+F9!0H;Fy=qHdcx(qfmz|O0Em!SbmUEgu<>*{dX z1yvC#0GS4ea#eoOt6);T4he|fXsa{*h1WpX=wa9q^RTzS_)nDLdHh5(A;ZF-NJZ?J zVZ8aU_unHQtuaZX3^pibTN9<_AQ-5CgNwb1fO`N46*PQAlOa>iOq>@pR>fk zE~BeaaJfE5{4?NA9;5-aA`|8m3=B^oM&4nl)N%CH7WD5POYP_2ke|iEKx$y|49Z4s zlbHcfTNq%zfp$klrlJQKnQl$CrIs@lNvbu`mNpxr3bdtNq9tZw^6VpH$w)3UOocf* z9XVp4&QeOuOxJ?`wS=s6CEj3xSII+*hc+H7m~;rnv1O4Sv znvNVm(rcFT*#aF3=*3691R(|vtO4?6LQoRhK?VI&5#dKSqfjI}mIz`5<}Mgj`E}sx zB~LcWv3(H{;?~q3(Gy~iD5Q>_hWKcPX4R4`y^PoaI!;TSFB77-JUNC;NzQZXX}D_N z9etE%z_E|*Mr-#3jt8FQyY?Zp-FV>thwa9Cl^anm(&gz}uX0m0jJAY#p+tXWCZLjz z88f`;=$Sv**k&|on^9};MBPoU6*(NuA#^9Lp|m3N0a$_Kq;H7+h6Mf6bNJQ!?m}}a z+*_Fdzw|;CoHC~oOn`#5c{y22kIBWFm!koVw-{ZswnX!1#ftd{O_v_Zya>k4N@MX- zwI)qd#Axl;u*hWeci^`8>Gptj^%7pzIj=soN6upXIfI}c66lN8|(MLnxX>M<(0Qrj%cOrQ;IIKu6?>5wzBf}i5UO8_6Zr$_6d{J z{n9?A18fI%?NdhEC-|ba%yOo30)NmxWwm|EHndM)shNm2<>);jQ+$@p!}?Jcf` zIXy|Uyd#p7hVT{C#D<2jYKCYHbZp6W=Nl3JqqK#L*BO4fa!~3;tr^@y^G<*^gF(b| z-Y5rY3@0MmHiqmTN-KCL%BvURk;XnsD{lK{+X6){;$E0{8ht(wUfC_*ufrUEB#e0Wl+ z&C%;%kK+b_F+CZBaU6!ZR;2v~)5iSaEOZ6^uJYqDBQ&|VH&W31QwyNupPkmh=@0Ob z%>)&W@}-fJS2G=}2}x19$x*u7KL)<;+ba3A#qc znOh}P7The>OZbmD&^t-X`6>y3#=`G%a0vho+`Ao20)REnNpdcc1CI*y>@T_*HiJUr zG8v91RBpkm&Q!cX#})kw0@dAiU`|H^X(4{`O)3whHjnqXv?TyIVK^4kiJ>jtfeU!$xv*q~N1-VO|Ou@iA*Ba@lgI zhuWXYdZ*$2&(KS!HN6r?G)<|kVK%ZI<3Kzyhve~D*cU%oVS{69PA5t~1ZH~>?Te0! zTvD3In3)g*<8*c~<rcEi(lWDON%`?(ZpS*4R^eqUZ=8BVImIf0Q3n*e+QdrwOVo8h>I@L^~wM>?ufuOC4O!-+nL%FxX z@|aX+9%MSgLd*d++hR}jZKr_aL&$ehrkJznWzcfNu0i&_T9c(GL<@;%D^y-t-L<2D zzEmRGl4HB1`mUte2CQ=zS*e&Ak;N7RmP7K;j}UpNLvqY#+8*i~)ga1YWl^rMAE|_Y&tg*eGZ{G(Tpd;y>WyShrZ`IaBy3 z)5fJ7F9HFzp7{(HZ3|YiXF=|tQU7MrG*X#m5PxSfcvoN6=uiHy@072R$CMe9;X6F< zF_yiVj zK64+6Di*z*;OsNk3ACOG*OnQZ%0BZgMCWl-PX_VbV$F?U*() zZ^3DDDpw{aZ2yae8<-zL@Byy-D`k2qla|@I#>}O$T#Yn3lGv?bI#D*U#9s!3MRTwX zqB@VrMP|;!L~kSMp>5W-lgGJ{V5F@#iD?-#v7W`1WC0)1z#@eMQY&DQYGiRBS$u$< z`&B9Eo4F+<+7{WA>2{lZ+T@V@$YkE~5Y5B%`4G+l9}@*=PNnjVOoogZ(_JqkxQXB1 zd@)1%w+eK^f6k+fAZ-NYkFW(6z7d{r_%aLsdIS7fywjuI;gAs~pV#m|6Je~KjpIF6 zmmvJO)+_Yr%s-*x>@%kVSg*iNKrmf7mik%~DU%ls)GLfLwb-Umr>@pS+H?snHb;)B zD>z#-6&N|)(ZHq+{UptETff&t`(l>b4)L0{!jpe($9QWoKOHkVv5(F3o?w0|v_9z! zl;1(7d~qu=sNrrNb3a^(AN>mmvSfx$4EGR{H@~D4^39!i^iKm%zghvstaZIbk#mX+ z0npH5t+bl3TPx*V_UQi<|BYmrANmab_adFUkO13w`eo+rNywfsXP}#cyNfZ7e$Ud7 zH~k>Hqfgc2!1Mn{uoOr6JYA0yy*~z@XXaQBz?#A zIJG3X$oFm6nv}PXz~cmJ2~NLb#x(CK6zJc;L#IHxn+9bL*4z!%46y$SyA&siF0)r( z4AOwb=G-|HBIDLDw+oKNnxBG2*WmM@(=Ka&Y<9Ue7a{{_-^hmO9;OE|(WuuZOb)|* zd@}mjiCs9$A|YqhycCWDP;<}}n9JcLFjYcd-crYkQ~UpQdTi+$b?2wjnS5YAfl!)T zt0jCNS$j<~B$9c()?_pSG)(S`TC6-JE<|*=A#CY7zHwS%2HGo}D4mKlag#cI5nS(4 zT&-b^w#51JUBK6A!x($V`ffz-!VZ+#fZT9to!VwfeA{ygZ{kN5lKR&J18d!DFc>JeIz(Ey)NQ$W%Ash* z#hnGlN6H+Ln{6RG`sMdgvQMg#Nq+nPfp^Qb#NKg!)>8s8Ujk8!nx%9QGSW#`A1WQs zJbM&!U0kQ!XGT(ou{Z zaH5CFjh0VMq5p6o~S%d3N z|4Xjcrt{u^AkQnbGZ}76_49-Ae$~JFi>`xJ^vE%>UVqW;^1^<%{c4E`P-=pMemyI1 z9renr(H1m}NivukXs@Y8>B@hHQmnG_cj9yT<|Q8v@Fcx zb&ZKPH-?vxQ!zlN_fF2@l9**-O_)B>m*z<;V|UZ6u!f}lfE_VTVQsfD|D;shgSsXV zUuBu%K0fFH1;bvNHetT`JJ6`5h@Qu;7o|P6Nw3j=h(zcY0Db9X)xz?{*!dC ze`Z=a+v=;_oX6S^bZ>wzA=OG&$ZA`9S`%{aKG4X8Xp_guRf_na2=lAbvA2OOTLj2O zs6ligf>YwkKpN`1LvII!|AF*6(M#jR(!izw1kt}Rd|k9Dhj-KQ?w3N<*Hpg9+!)$} zZ8>yqB_y@*yR`ponP_8=^*E;Fx4O0h{+mcIsaad^;Xrp9(FZpny=0>P?vbW;FO@Uv zBL`GH4(xFpltMDSX(=P_*`=d-YXv;VlV{})dhjox_cxk(XD(-U5ca6uI0cp_?`N`L z*^9Sb5Hm6v&Pn(7ZtAt~Yh8ywlx9Zt5eO3lbuv< zRvh$E?{dPpKI-jpm`t+Jq+wt%D!)WRzeJ9SVv8`5G<%@^x-|cQ>8XY^AK;7?=CA_U zq~3RtKE>B7cbq;Wj$a~i!o14w5Y*=VRtt*jFcsvLbDVuw>7K+uKmK8kfq2DMNL2n4 z)A5TL^ywU-$t4FB@*NKT{Iq?q)sRDyA*)s z`F`m^#O3Fg`S2I>*GvEw)FDJ+)P}c2ZWl0BdrnBd<)6Y=k_X>$O<1~9g|3P8tGIe* z?;Yai%8xMx`A}|P_5fBEWzR3knG130l6-!*Tp^0@=X#pTe?~CX(_H=wp6T*KjL4TC z<|9{r1P@40dEh_g?Krl)ZaY=_l-$egAGBTbM()RIaxq<*fsz?#IUq_iAkF&vtxS&DMD9n84N{zVc+;7RsmbpYJ&D#+xoFN*ad%SIlZT%G5$55BpOh!= zWbdabaBnzQM0D)~d(n%1052hO@keN9mMFTno;e%YSo%x&#HycZoYQmf@IzR*W`88% z&4YgU*PS_=vyeTx;?75VA+}Q%C(E;|_!6AivYtuBMe>|n+(VvIiZ@_BZCTGM#U+qp zS5%9p8@(zSG)28iA z#`8m@8|2J6Fg{FY(`B%OVRnGLeV87yHjwt%iTwz;<`Bg6Vqx$#SzeS8kd6~uc9I|jmRfSIn>NvZqTeDg7Ol1ldzUm1c;`{q4%Ke4C?B({;-p0#*y zf#=6a-;L1NkR_rB8*uGuw*Wi5H-S=kc1%u1bL5WmTm69H^GWmZ7NUFqG1wQj$4hD$ zv%J}8Jocit)RrMrx;d4LI~hG-Cr1}D5e^Jy?wfwd>T)+ao+KFDR4%Z zcUgLio3NCjf_R}ky_P;^Ku;dQ?X&< z;7jO`KzhZwY|y=NROF6z)O0rUsOh^PS^a4$z)#Cc<=omR}!Dnt4dX`IOC$ zM$dv09s`<=WUeDDu$uFx}C{obT#KjN_z9djdc8R~8u zi~*~sOsp-$aaa7dC9#$MO50b+dR%w)3S!$@gq(TU1^DnQXqIsPQdEs~%4jcYoBo1# z-rt9~aiB}&%(tMU9$CP`NM>Z>gJ@zy@tI~2)>51XW5p=8ALh}|8kNfCo6)ST4sd#8 z971a#v_RZ(XCFq?^f?LhE_{H8k(k*Ivb10pyK%LG&b4=JUb<@3*^zi=Dv(fXg=h3A zOonbDk--!whW_)Zm1JL=wiTFv9fwhI>m^?1NKmOG_3G-Gzq0Sznli>o2M9*Y}Iv@bj< z9p&vy52TGI*_U3vOwkwfJ~9}siWf`{X4;ZhQV#J7sm&Il;`r%keb`pj4|K>DgB0@- z^kkzgK!`?b5vx1eDNb^w7O_^fWMt24p#TVYP-)Q@fsDKOsnTc%hPx+3+uSVN;Fv*z< zQ2cN=(T;d`<{IH8#b!G4JT9QsPv_uUG&Iz- zIHXj~SfjHQ`8z;v!5Tf6J@Kv7iL}tRgkWQ9h%?h@E`?DWX_b8?8I|Q?%A#K-52GD{ z-c-QU2YmBF5XiYWd#1kn9;gC$gGDsSh6V-Tm^_;z^tut@r=o}n0>Ct}A1P-pK}vEq zUA@2c?DV!db+OUTB%N=yI4 z`Dybd6kZrzic}*sOlafUTQv2`(c+9|*82MHZ>(yN*RFdJSpG z#JG$5UKLw)o0WcQ6J+nikvlfD_R<2HthU(FLDD$n3{}pgt5cSZ=7Vk^8jn6UTyD> zI74sDz%3>`zn2g`L)BR}j3}7|4irNU^qLL?vn)CWtKv1xMN{#>=0{-k&jue2M$@4= z-57_ae?7c3r^9G{Jn=dMkLj=IXA{PaP4th@1Y6bJHT80MEyPjeK(cX0(t!) zZ?<))j>)^i_0buZh;V-km^-d+2|NxXQE9(n2F}#w|FD~(Z)bc>$hKiBR43grEhMp{#9Lx zjc?;^d8Xc>%sW3gIg9Cojwo}}mX;GF5}hTbpqheYiE)RU4Z$gaV)_XfoIqBwb)KAeCGwmXefub6S+(@d z4iF6Mp433}C5EGA&@x&dZj+2;U*#a^6hm@gwC6>cKzuV_06+12Q)aB?9g2a!v|i5`FhIZK+W@6xYwi*A&7NSD3eD4?fBL^ys9#fPx=x`pMWMLu zPZS#X$-G?_5=BZKlCS%*XRVot)`D2_r9bo~1N5QKfXGL#e(0fxZnpQ~Wo{}~yTZW6 z<{JG+2#>*DQgVs==p>sUJqx65;kvi_MyyEIi-Eh3WG&Y;|67n zez+AaJraHy2*qpNY(+-+6}bEz1`CNPfN&%{n0SZar^ydXOtgFkW^(*gS@OuTM7|FN z9CtPZWd?kFi9ImmXz|`fHI?~VAjQLgf{Q`E3C;B6qpzxhjzfPzrcnZ!MGBH}G}sLH z$L#{)R;6uduQRtHc8jtXTKq0tC@#Fh91k}hV2)=Gc&!#RwU;Bp&9=;ZIgGx7r1P>; z9kQbJp-*}^C3k(p(3=YetJTu@;U@N`3i}O@K9{DyXaUkdZ=Bln^JhN{!{B`j#Ve2` zuGoyO~B>M@mm(KOGR`)y2 z5h^lw?Rb;Tr_ccV>KVDaYASh`js!+@6n?U@xQ5o{t9ZfPnThByu*MDz>N?#b^&YWD zBX;@|+Rer%EnZ^7BDKD=FN5|_`XJ5T2AjHmb^=pJo;M9?1!7~tqJ5B?fnETEMf+>U z#L0*xVHAOXpNbVk$MA)AGX-^*gR0dVW#$xfO=fcuL>A-7HP;FJ^cVu?`><|r58`Zr^F}hP{Xr zB|TLgi`0~4^?fYzm77D~5%xq6Vy(JsuV~s-3?sYui>8uC7{Ldze|qx7Mei}=HSuv=5~Lm+O;m@lS=l{3D>e$SnT6!h(!!-Fenhkl8CLN!Iu zyLOpdFM5O*O^7G)LH1Vd{aI_?;`HvdDq1nBIWx4V(x38E`pf<8fX$h)Ibe1nIP(*2Aa}`D~E1` z0NvdSn$!Bt-gK8Vn5(oDGDvOJ7CjfzlCiQUWUhH_$X5u+VFl_!~5wC?`Jf;pV{z!R>M1Hji{e${?3lQ_p%@VQ!#Cv zd8mb5KG%uq>0Ht!YJ9#GsBdOuI!ujBhc!!4)=ZmsruY^CT~LI~TVgN9pU zaW_dEAsRP67I(A65u$MuVsZCK93dLl7K^)A;t0{W_E_BSB#sb`>xjkuUg8MRxXxJI zpCyhEjhh&YdrRU7(YRbJ?rn)9MC0O05| zqN-E(U4HQbMgWK7lr`{Nk8OaCwPsbTv8)lA>rp4OVeulN(EerMO$W{THJU1Gzp~dP z+we!PVl?ikzhl#L^h|pO>FwF?0geOA&+xsSu(G$~5c|lE8(*=_;vT{cNE;L{23xG% z#CAwve8pW3HT+e)<-nf_d~~NID}M1(kTqc-hKH=4rZgOVfB`DP-v!)w2p76Q3c&XO zko^{^lw5cwdoF_L`=sZf5GV{s?~sN~V=5mdVWWWmfbh%lgYf_0QQI|=60uVeH+F=$ zvyEcM<^!jB-vOg9P;M=k16{db3J#2<8BNIgV4`>h0607C7q3KsTeg?u88wr05M9M^ zR>MC;xF!N3$twC0esG|L*~giO=p#OZ=xRpEE|Zvo6IkN03U*@+ZsOmN%pN36V$bBE2)-TN1{*!O{Ohz#bFq{a!M78wXg9w-b(uY=MOzTTa&H z&#ZhcAqEa2E+}IpM77yPWK1=HH;h4fr@V6z*H8UveX05+TYujFn_bBaqF+KX-gU{% zNV3^z=r|1Uis%eykH=N0h{a+bN3%mS@gY|S|25zPJ%9$$y$H(3Jk7^&Swr7M)f~Ez z^|SUhP^{J5$li>~w`>JMUF|A0275OtI()M4(Z+FTaz%ML;h*-b15&wJoSIDsedpgo zHtA1G0DJ)Zor6h0VZV1U2`KCj4kiJGJ>XyxP}qYGCINwI9~)_Hz~+bjc?l@~Ob3&I z!oJ{O5>VKN4kiJGZRB7Q5SXSl(>Hyv0J;8=`a;?pJ6s7UY!ku|SFq21`@}4Uz-RI4 zcnzybk=JY+zo~uB;j;t#qPQ-`KRcC^C^H7zL{0!L*MmraS8jSun35BXy1_UkI;GaCn_E=$X8(y&p-3YIp;k9pGBKN*L)o? z!0Zxnko;u4iJb@0DhTS<5$xkS5-qJ>u%Hpk2`G~xCUF0P$5H~!G|<%SYYT`d!%T|11t#&siAC%>lvTj`mkNSln;ByJboS5QA10>6rw;@#W_(g_>s_~m@{I-B% zIra$QB6!;o595kxkQhN(-ogJ%gxBPeSIV;k;j~c+XdbpF>`6^){5ajkNgt2!Lr_KR zrgeUMSZ~7z2O%TR9UV^zD9@c7Oacnq*}){Buz7_2Skn$<@2GEN|JG(`)vVK7l>=t1 zs1cRv?ZQAUZ}RG$SDjzn57DrIUHQQIUFrqPZum5iZtr(^^3OM{9sQiMN6DQ;X>#TiJPJH< z<(Kvc@xW9DeZz{Q*{?rhRVI?6>KCzXc)sB8FTV@qcVGG4M}CjOuh_nC_Crv57rxnB z;&x)(>P*#~jG$F?f{Hd@Vw^TNNdod_4?tM=w7r=i^Wt~~aW@XS_~vNDM?E+qWR67$ z8DZ@<)7D@d^CUNzn_)0BMZDU%ubg)^nc&Y~OtY0XS8v+2WFzQD%Vmy!^exc!FD9;E zVl9!H>>K6-o<}xH<;+*`hSX=_fjL*LCYF_lRlrGdtGVUH$)1(qb`5glSR(>Js)KIt z6@>W*ltYl7maK0MWthe58+MlDj+c1$FwagBQ95VQeX}Ldu+FD@z07HN>+=mS_KkS> zzZ#+*HUJImF2PL@?B9+-)x~BETB~ADO_AkpAS|DfjvIM>b0q1w_~@7DNuUPdz&FoIn=AIZAKWVDaTISR4bIcZaKHBCC2kNazRxQlW0`&k|J1Cp=Td!hu z&{h!D+iL<~J5u}RkrhS}UKa%Rc_K<{-Q61kl?o#(o?2m);vb01;|i=n8kx-~b=uFd zo_R|sS@v>OmIB`-5c{YBb>o0m50}PxgJPFb8RX6mjt+U-N40uLbJ5)u^8*X?n6>CW zD@!vrlDCy&O@UuivTYMyD-dd&fh({dHGdWfgq3&efo!3|%1Ax%W&yrm5Bz}u!7k48 zOaXpW4_qX`|I`By5a1{EK#nxS$_MqpjRp8&J&-OltbAM#Y$mXF+@cSw#6UoQabxXs zRoC3`&c0Ul9qsw_9e)XEn)3-e-u8WT=*A9~opCQqG->y8v=UI-y&X&f3X?7l z@b_h0t$!r1$WP9D1bAC8KS6Y`WN6U?fj0#k0(VD*-a>%OYq+eL(%WN7y!x>-x;TqD z4IY-k{Q($rHK&uJdF$*RMa^u96J(U&a1exxjz&M4QvlkIo1cT8QxD0?ySN}qOw_V@ z3~aEiClN~-_qIF^s2pEYbGNtUi(E-1#OGyD1*+xGebQe;%3DggNexe-n$}E38o-2H zxq!^F_9i>=u`^{>lS(G>7M_D;n{1eTy{f|23>J5OFha)X#BViob|Sb5L=17(ixZ~wgYT7`GHvn_9VC4L}%knK{k+k zeL}gH%!aWOrOX>^(CfjM#Izp34%iXK_$ct$mD!s7rA@=M;Tq70^-1ivvU^FoJrtq( zeI7Fnc!=O!)h39aD?Hd&W0SuIvM+r|gjYv6KR6LM)(>YTNLPxQ=k|O+rUgujX-WV{ zfF0mq5>VKI4kiJG9pqpVP}r9pOacl!n6R4t%PZ44#NkRnaSwGc2>`~ur_=@ie8Y*b zK&HcV^-nrahVx>9%#P_7-ms-be9!(v_h42-`@?n^BVRjN&+Gz7o~ z$uBhYNQ2pHtuW%?lkU5I9FPt5{_ZECZOTaBf_=}GV;wrrULhsMA-_)%4Z60YPJun` zU>tE0k9&?T#Pf@gCy>(;*ilVNJY?`%dwgD<|~+I$)xAj@q4$={~oM%T6rFE(87I8+f7YLzMj=cnM4V z?Pslnju|csfrb~|J0YHdHxXStZS|_6mFHW4G6Tddzl|4W;GaZ&2S3U#U1Ed^;q*LC z{KfW=l_%b4Tj5gvx;~6A`yO`~70-&UJXR0<;=jPx+Tp($ly%cvbfS-Ui?aRy5_qV^ zqa}c@w9d#G-|CJL>0WXBpNHxCKt8M z;sT9cx1`9mn^B!AeRDXaW5+u*A0k1_fobROBfQvL=L%T>v9J7Nz|d!w%;fu&=fG)* zv%>Ozt^R}YZf_xa1&u)0Y!3Dm&{ZY`4doCLl>vDa9mX5HE@B@=nIxfh8MJ(Y#8MhH zi+wJQHoW<#z#3i0W^1s)XG8V!qtBVFO^5G24w{`tm&@nYj0`QB z^a3s^O_#Ss^eK{(OQE2_oft`_->-LO{!x)hrU8`ETyzgAR+q?TtMv!~OSrbxX zN-|Gwht2TSjj=k`664@K*ya@(BGlgH6FY`;pK|*v&lQljZ@dj*vFT_W9{CpS4Pll5 z1y<;R%vL=4?#@Dr7rW+92Br%Dw4Wyx?lP9Bm)wp&DGLgf!z610KGNjE=s!_%DAA?vGGLSxtwSsuM`tgGRiR0emLEzU~G`ZzcCMw#( z0rtLC;d*6~l`|=F#x=$LoM;<)zWoW%%tJZ59-ovm`@8XSX7vvAjz(7R`poDYj(P_v zNtRr)9%mj7M;%*ZDy1`vOdCswwH-RA>Y?Wb)r(n?6|u%Z3giPw)KhaZqsTbr@9IyR z_Zq^xAg`FXmCo5Sct@C>vur50Xv2U!GQR--^uYKyZUXFj1~`7{?Hmb{c$4;_X9*)r zh#w%O>X6bkBt}>yougY9jQrtD9e%ck&j`WCw4zL7Yah#(K9y-@m-xS-c6m?8ov^zi z@6OR$Z_O^LX+ok^xF0p+6;~Xxw$<7!xtAUtIi6G72t>P$=Ec>(dR~F>f`{%4EW3FB zyM^hnuPXr%qi@jaU=mQ+cn6b!!X`MF1QgchU=mPRyMswUVI2-80flusm;@9y(ZM93 zu$+TQKw)_YlYqjy983ZVo8({;P}pP#lYqdg>t$uv_m+U-4?CCy07Dc0XE-Ps*+75a zbny~EJkKm$j_2+uFWr03d$6A8%rTFUVJ*lWk$4PTnhG2mR=$GuZ|z=JKX+$8A2`VZ zDric^2?}n*sT|?(m-Td2GS!z!XQMjd1B7l@zi)Ve0R%qqG}UFshwoo_7-;NV0LQ1l z0o@)VZwP|GJPpyZJgjj7a);sguOVcjbSs+fW_-W9C7m(LFulW3bbc=ko)ekbhX5mm zmHeZ@xFc-G-4^CaFp)hOxLKieI~dKw^ERr>uEHmCGw)5|3+11WCds7MIlnEL+iXKg z?7t!`?8(FH=3&Sbn4?gu@x`$zFophk;AIP>9Zgz48_ob1IvfK`>7bac(AN|9j!%;b zcwT`%B?(8iKPG4A$!7mr9Glr;j;uOTRTO+MJtb3c&>pY6?jD>?kDn;r29=6CZ^I`8 z_!6xGq2x>Hz}bK}iwWjc*gMP6y)9E*6)5xCdE-cOXLAq=Oi3|0u^M99uyaM*nhD;c zDAzqvuCWqbq9yuUN&a@QF3rc!XBF#FwPJlLUaUX0#d^&c#rpYZ#wgbRg3uN#D`d4; zyH{4MYmQy41kWq)2WqG6eiLBn~7o56Ly_=)G5u@!B+UZ>ZTT6GQ%siD5yimEaV6^(x$ zwyLM5ehRoaizB)V2OZE{Scv!3lufyUl$x~ZWIpnnvYVVcqA&xG(A)+dM;F$V@a+p83D(6#$G&d&W#>Z z2(eWv1`0aMtjFjaY29q{mVo)*B1g4oMHp7lsxW*ht_99jVQ57V#^8|>lfqY?5uV>Y zRfJg^VF=TQ2ZSNH6D9@<#40<*%G`YL$v~C9fdn8M_c82)C;zY$;7Y{faWx;(V4@pw z8DEm+OZiBYzsmQcO?QV_g_CxLdnAtL|E%gqv(g3+X zMvRa9yVxP$hd!-zF`_W$4gm;_osm|(usj&cGIBKg3e6=erD3csS5^|H%R0j+YYU&> ziwl&9PGu#^6U^hIIbN3#Sg8kMH1+7Jt6s$BCmok*06{#gy4bVygx1Qz2@g_SiAMHB{Uc zapqGYsa+%In~jA;R(ENgSUDB|*OBbSd|aqh3m%vnPlhPIO~(s9v9g4^XSGR1vie4; zbn#l*Jae$3uLtz$zpb8b9mZ%}kK?Mcs#Bq9pc^uQ6{wa(3uDI1L1WmWh>QGVaKIg< zTO6flP*G3Vpo-l|0IadcPA-&?O9Qh2;Qndkz^+=+{{*rgv_Eyyu)nsl1VYOw5oyXr zJfUA)ANhhVwcnBOb_UA@q#g5aA-hAjx?|VZtK>L6EEf>@IFj^lP8{N*C`o`f{qUMb z+rP?s?BN{%{(;;mj=lnS_L3e)UvGzvXXU!R%3f~N>W=RD%Vfygl&YS!`=UePkheC< z?XsiX0N{ij&vHO)?ET+s@&-)R4z(LfpH1}8*h-RP=%pka8H&VW3D)5%zGD@-6 zj%48>ytm`SRzQ;PGRclNX2Lf)&~&8QQ|g=9VMcg0_{#hsm8zoR+X$=BqnVFs>r$^I z3zhYp3e9B31Md@H6j)<0j@XIV%@R)c=L3-GXMej5-&=?l;dR7^nYY-NjzkI} z?ykdQ14t`3yg3;p!uUuCvGjY$5-ZgJhXh)$k|ViVy~+%5u&ozMrlLL2q783|DLIUvWc7 zhqVjNEkqk3T-+Ex_@svbn5}3}{InFr&je;ZUa*-6H*he(Qx9C!wO1G6jq26ABV~hf zaeHV0mr^E+J2Q|@x25n=^t&L3%#q?X+63uw90|4W0vzlJ+YhX#pRYO(Msk0{#dPY+ z!j5dNqp46qCfl-YO;;QY1LY(y@=i#%>(m(4l`|u zTdLM*S45~;D3Zz22o&BizHK}hAIMxyNVSbuya`c17#Fq>W@5EmSv2=!iMg#M+5!Bu z1O+?#1y5UZNNa&qmJyTcv5j-1JZU!1CUv_E=H&DhB;k*-xMr#>tb{v z7HzThhxa4V3J(_hky}KLM`s(IOBqtyu6O9V-qtsFdJ|!D{L?;telU&q$}CrhRqJ}E zugEjJ(WvUOA2e4y5u0FpmN3%1g4|coM|3+|C`+8f#-ljwU*o_?^BOszL0=JvO-6B8 zP~*Tz^9DH-R?ML=io*dl4vaK!f&EdH7zpYkF}Rt3bipL_e{+N-61Ro*?H$a zk(jFV<;hELf(l z6>ui@Q0BL`nLO48>pfM>7jH4(A~6~{IMsd^j~?=R-?8GYZo;Rc(rX@NNIiay)VoPss| zpMd5oQU^sAq(a`6#gpP|!iMzeJ|5-Brjw5p0ZGC5^OoEIahr%|@-s(N4XRz1b* z+7asYy^X!YzE``4j+4Utv&Fv}C#}^^vHKl~$_qaqx(X!_8Wu(+U$)n=T9g} zqRiP_%;Rw4k9XQk^WK8rIb6zMHtec=E#r04ClM9l$jUbgVRS6RH!JlGd<5m|1$Q^3 zZT7>DU6YpgE#Rti_?|4U<%>22^4KOWjE zNk*5!!`chkb_cj?miB@y=9X*?u-;HS5Vq5Si!>-Y0I3Lz23h@NljFJ-aYMaRv~(Tt zdSstauU{MlZcj(LOki6Yw>YQFJ#24dNrh%>kYhsZCg9{T6^AwF?7#->=nzZGG-cb| zxDvJ@nW4YvUkXI`*;x_F%t2%#S_BoaIhXqu#Un^V)0PSJ+`<=dXPwyW@L`xz?8vrf z%?yA#&B?Iass91j1v<)|%e2}u<)1-D-(qz%wKvJSMfvB1wqc_Ya$CLyFK&EtfcHfI zIOc-mcor+?B3R;lGQWJX)cGWDKFN(wVbn2Wg3J{kK^u}X;2PK#EP@i)`i4b^?oeJ{3r2W7{m`YCoVuVYCa1On@+X_U13Yc2t>`o^6Ra~;!YEE-^JX%MLtB^$jzglQavxziEc|1Tdb2!xK)6e2^%j680Q^Sq;w7pEQ>m(g)__VEhbi;+%K zf#VC+Xv!5BM-wTHRz>Pp1*Si)Zz}d_K*Divcre~sUBp}L68d;~?8XGN24cGn#odAs zt*897G>xQxJ;3lqfwl0^0&7cV<(4!hg-slUfW= z%@I&}q!}dR*tw5!W~birCPNIyL@IWCL&>s!y#Uz~#jwF^!*H2}-v-;PcHLx)Z)YFk z;>VfKthRe8xkun=DeFA98Qg&pO_<9?&%r-5$#tK-olV?};c=~ytvmiEP4d{?9LILy zR#-dQh1>W@mbr-3=dCw$BqH5&cOxvq25S)2>B;cf8aMsr_8 zcqH5wzs2ncUc$)e7jUb#yk1g9i@+#IDE$_(j^XAAkAz2}yo);sJtL!ED*b4AZlXNr zJ9z@-_azeA(dCW^kAyqnx45(LV`TKJnmnUw}aR-gNk*z(*T;gN6wev1bXyo8a_{h!J6hYdUrB(ddr5W=`J z5x>QQ30}g;=r=3l`47r@Z3E9kNNjl?ittEy7=DXiA$SQRS6=Sgpu)bN1tmRTua~m#Z&QEatA8X8mv3s;JJh554iqP-@L>1t@u^YFJW~< z-Qa!_c9SC3XYKx|A4^Do1b3Cg^~kWMvv2rl5K7}P4A+;f9ODLE(NP$#=8Ip&FLv7> zgGWGD9_i=|{HBJ_#BUItCE=4|@b6UN1xlP6J|2KXR1z@dkoGn; zT*FHZ`)F9AT9n9Rhj%*|9wWY*A2Pr#3maggb`v&CG)t~R zH&0l>0pimbtH0_SlgCTVv7kspAc;bhCI+Q{2}5+B!s_)Fg)%;J`%&9 z#9~GW3PlTTNlY-nnWtyo2qb3OmrUcOnmugslXmAXLFGngE0vq7p_{9rpSh4(NFDsX zs)O;~lMwgVmFQq{b$bw}K6|>7_!L%a;&bSMgF<$x+mf>Dj(shT@Yr4AzDZen7qxVx z3dn~yjP@&wFn3Z89|NO8n3EmD$7+}@q>cdDJ62s*b#&2Z>S&M3&#Ou}j!O80DuMk* z_KJt7gQ3k)@1lJmh}~)J552uA?gS(RM$EBgZI;GO8e^wZUMj%YO2KjfjiWhTvw^ha zRkDyCJ`a&$bUuF5wUX5{Q&&}PaWYqKt%go=gM9OK=HmgEj~3ZGTx321bFvJxMPgi9 z%ANuCX#E!gjzbk1Ln^hmKUQw5N^^TPbVoIG%0ydT%<+`)K}yK|?wnp;DDoxDDPstj z{Qp2e*7d!E%AHOavzQ|MQIy2U*tq?I2>J|LhEOFMqCT5j`nS2a(= z%+sH2o+f#>g8Vqi6NW@9%G1d!%2QiHb0rHqBD9Y5>T<`*sRp}3>FKz7zq_mXTM{p`PVY?Q?`x7j>!+82CCZHTQ40%K6|$1GWNJl)D@~=D%7VBdSL$Ii z!#@CtA6<@LTdph;d?;H6OC0rNq)4=gGc}31;nM*Bh~?#TyZ z*JLm_)3@Uhjsg72FP#3OVR0V;ZZyeKXx3id`jf=H9*Pd>v&e~jq@SI@%aYA;B!14#S(rjAHAb< zG5IV8G+X(7m4T?bIZTaevr@G-t6WyL!({tSvbFgPycx)68GI{$a2zWSxR7l3XVUqO`KfP{%E5WnJK-HOjCT;8{PWFuK*1oG zoG_=t+ab(v@q;iA$D7}4m~-dmS`G8#59a3@<~bd6kA`_1%KWkxE=%fa-;jkVa4#62 z`~>*}M6YNU0_P|#eCZP?pk(wHM5?SQ+4po3^x{zP897YXJP3@Y)}*pHfmFQ2PJE+7 zR9?7P?jhk;O(PloY0R{fQDscHmFW@Tf;N5>|3@47|9NG69CxGZM5cwXY7z#XH`q9j|BBNGor?+24}(5`%i?qYI4|5DH9$RXy1 z-@tjw;MkX;uILht_U3AQ@lw3Y86nWHZg8&XWawQ_(bUMJ%YbMef<)%K2)QHCOPRW( zXF1Fb#{1@bz~FcPzK^Hy9q-)*zNdh1Qjf2_ifP?!()<7j=&;J!o%4aAv5VhCtX}ys z44_PlrTp>ZLUTFrB_E0Ce-Okc2(uwJaxFu6<}(HM_iA{swkrUaTWgeWSZ1;~HZC%l zy}4GBG=D(J$N_bWl%J1pH7Q~p|K-*5?kJ}#!5MwbQ0_E7mHN{AvbmTP8?SN&>PZY5 zaXrMI?(Ds;w#w>LW$!wmTleA`8}~1qCVWZXNR1wYS6SqSzH)T2QgW{t*Y1EOCx@on z8`o4FRi9_em@ATc6ukiL|aQ&UhV2n|~^< ztx(ZVm?i74o4q3ue>%#g)tlG{)V#=Ldzvk=-$2og~(;1LS7qEh8 z20k~4!&g1#5L|~dz=>FVM3jAga~=5crkV?ZA^mSNfaN+}9puJ@W+<9${dDnqpq76n z6Ev*X1Ad7wkn#qRiQhih2`tZKx)K%Lh=^=q8i3fDb`w+b%`cz}q?dpocqH+N<{)h# z+zf>2k5>1hy-DYUhFR7J(O2`mW^cnmw};I9jEU~AKGQe+m|gTp)jO`(H-3_<^NqKV z%b33L9VUOPMpu2~ZFqOSjlPj97iY0f$_c}mZ@e9d;v1`OkqJ9eZJ+2D?;zft_@Q&W z3s32L-pv4g`>^xvvL+pv94516b77~s0J~jq?>cy#hfs76jsgqqJ)eX27sT0-v|oh& zO$Sa-CMs}1 zLaC&R1H#Z2Q3sv?11Z0Akns1Brg)0w!DJK&fAIaT5Rd3d z;&sfJD(B+Q9jgoyf*(BvMBe`LG$osYgb;oP;rcRIo&wXxGI*9G>}%k}jFiFS)H9)e z>2I+(DT5~%N2qJhjwhkuX&3DXyjx|)Di9YuA<=Unr0$=`6S`*_jkFC;8lqKV#1uMvQ(WSSZg9QJEQjI`>LkJb*5Zj82uXrEomI~XK}wvYK^h< z;?3*xHT`0YNXMD?fGeU{%)H2#bA9XoAXuL2Kpv)xRjBq7L`3E`)w{zotRINP+TRJ zt>yV5cz&iztWMhG9>EJ$&Oae(Y5Bw-v#k^vp9VkIR?=(Ld+3FoSfg`>T(YesZa%1b zXz{{N(hCz>HPX*)39(-2EZwz)J{UBo7p;pD#RVU;fPk+s#gdZ7;3+0WsKzzA>jYx# zei@M`UGF!Ohz_F=zjUpFp)F_%pwHQYcEM3r`#vkTyXJ9Y*j*Y)IlIdsJQ8N{YweDa z4R+TAP}T0Xf>thSw7ZGU?vCw5Xe7t?s2OBq*&ZWD+unDq?X`d^ZhMxs<=KkZ&ue=t ztB(oDFseR|2Xm*F2?&pbZTPi%VPu0|+5zf2C?@Q}Rk7?B>>SO^AOd<~dvcXu>HwZ@ z64Yi&{f5#NCj!?wD4wmo6m%D_sJ=|5UM~S-S6^u5_4Nf+#dIRUrj^x%FhVj|-M8KH z&$2yE`e^TM`qeZ%`ix8c3Z)SxR+#UaI?K$kZzfVb^jk7lGPkk~2n9(uq=66bpS}sa zEaL8sSd^e@9@=Z5egOA*vilvBb;gbN-m>RX5 z&6^k}_q5opbX>-+oL{BM#AbN1gfQ;eiG>{5{QZ`BicsN&Q{ZYD=u|i+NZ6Iji;Hfo=8|Rn{Tg-&24D41M^R2K{U3o ziX5L4%ND*(yb21$YOF3fp**KbS+DCog%W2RR!Di?3Mf|<<&xY%t|OPSOH}ulvnyIPVJ~uF-E!$<68P zv2mTT^0iu2FQaM>BgW-^;c|KvH7%7I*Qa0`PF}8rO}j@VQ~^e zl4H(Mbysi@1w-35%bH|6UEWePhf`txl=C83}1^TQ`;IaN4x4k8InzX^3=f9ommIL9gvc zefPLbCcar+III3bZ+-B*4EnRp?e*wOMjxVkyzq6-qJ*(lczF-_q7`04=(T^FmHFQ9 zlHRO~)~tFem!_RCi@`{5^pCMeS@47{?g?E%ep1n^XrpX~G#zcfzt>pKoPS_BQ?k#| zey<71KcjH}&+RwLwTB8){U*IR5b^sp{ay$CF7Ji79A{xP!C*yZ`4IP$=K=-u>WvIptg`OkCyQ)MXRu#ac)Sn>J)|>;jCR@ZO3^!re5aCdp!`w%iA)yY}ZHZXLjPXyjRwTuXh?&FEhZ~>16|i zN5Yx-wR&M>gI>M>P~Y(=!RjD-A;$)oZk$dHY{+QwZSlFGjR18D;Cd$8mn)F>|Hs^$ zfXP)RXy zI(4eb?r{ij`a5~kQ{+t=>L)iqScTaMrL3Jj5tkU3JG0VB`0aMgH7F1%zfHn4Gy$fC zfa&T+n845ks^K{!fmm;A@;vOx6lqG8fxiwca$B{?H+KC`+MSR>6Vcb=yoxJWIQMJ zn%B%j(QEnf&KSnm?8Q`EXBC_kpa=fF@%c}cSUl%rQ6h9Lrn32c!vC*;|C1IjnUv-d&zv{{0$<)%yFjCnSrBc8o4JW09df5Ee*mWXF#OuQ9k zH}?T zdUKol5%5>l4-~Xlf3yT1cn7iaImx_94yKljli`_CctqF z0xcZBBph#te=h#xtD$Jf-h8A&_qo-&WqHTloQHWc62usXB#Z5gK!BmVegVq49y9^jd=?g7vcVLZY%IznlIC~^ z0R&NFQVI)&F_~_t-8Z~3tA0JA+!`VAVvwO+@4#M`IR(LT!A(0hAqkCd-i7bEwqRQY zhNXq8&EW>@4KgP&jocsQnHS}@c(=y(rOc`LhLe?*@nr+T#6Ue81J&Vh8|*aXiR^-T zUiFJ{9UKH&N2Q?(+bUdlZy55HVPgd*Jq)2C|~Z=xG5?-?@yN-6Z{#4dE5dX#Ril&55>Sf+`@0N&%V~ zY-WGy=07K{M}GdT?+d3FtEu%7C=IBr?~S5jO{=t~vc5c76q{BFPT^PTTfDv} zN+zo}bU-LK&^B!*s=5Vjt==f-LsIoqaR9`_QHn2xr#s}mcFQ*l&Z$U$E7D69>9}y( zY74u6FIv~?ok_TH1ndUjX2$gSyoxyoLkikTR@&3DqF?hhFlf%V2lUG-U;vkrSMR}e z(%+dru$bo2d*rwm0i+qy>(q3kW9GIb-Mr42SxpQ@W9I7(W2T7uzJaphF(X|})BLax z&u1TQ2p$sRHzRoPN*H!aaBqWWhoR6$Vhl%AV#nqCSU&ZCu#>$2Zrpsy!wU4p?C-H}1! z78KTsiw%CuYYAXqNKUKg1_(D`9R*r3as6CI`Y?n7x`LfCiUd}TF9+X{wtO;eN|Zau zcN8)5{F#z3$L{+HDc5JdQ$w0N0ISWibCe~YmiKkai)aagja8cGz7OCHk>=XdnF9ya z;fGJF3FU^Rw@%2NMeYk4a-&@+CL6xVhBi{>MjIcOHmZ&m@7cCE&bLT2HHOQjVP5bm z$Q>fM*u0kV9zpvXg5(637yJrSc*dUQ;)u`4VWw8xQ?U_^r)U(Sr^m(QIeW1j%UXX; zIGiW!rL+xRjtp6Bw_r&1?y?FVIpWlr?vwMY98V<=2ilyL5{?Gbx(wISrR1d_o(L{4 z!@YFzscT6nd`u&YX$fIEZm{&uy?6Z=LP&7rdOPT9+r+kF{pv7yB7nJx8#NGzGv*0k zv#57{6PJq5+0Ji)_d9{lq{Inr5Cz!PX2NkpPR4J@fZQ2(Il!h3A1d#ju&OIV%(^Qqhp!f~38R zxL?96+z{BRGGTO+Zej0H9Is5{w!V-qHP@oKTnP= zb%NpN;&!)#>Vh3Gnv1DQ*cigzJ3oby5r#renUUVG;4Ao%x!4*}nuK5heT zPo+CD<={g4v$o!1rv60~B{$E9uyIuvC8)8c>GEhuF>5XWHsmT$FSzNAK^iP19?~$6 zYEOB%4f8?_oHyFQzWEQ0GoNk*5YFMP&|+{8lx5ycShz)I6HG*vO?EbuQGT8xnGIGr zk3xOE`Td6a9M_4VMGa#=gpkh$$o;t?eVZ|q6vk5Q2`P!9Yj7V#9Bwy>Y|)8xD7V9X z7kD2Q2NNK0O@it?p{;aq3RsgqL6(PzN{)jbmhLXNz)-3K!UH?I4VJFyTsHU+P`}Oh zyr$+&C|sInaUQh@EJ%7`k`__G8K_8W&=|@CopaveEQBG;fW$ySh|L%s^o?&fbRw0LPG|8^XmzS$p~U#2aj;MmzI}W@yuB^ zXV#* z8D80A0?dm;i%}{rpKGci(@;YuQA0**IGzP?G)6V3)He7VWuJn)7f}1~zNq1Ui6>i4 zxy}L;NbAhW6TQWpO;F=Dzy$o|i+LlhJ0^j@d_(Kp#IjpP*1N4hL35n7Io`(HHt;mC>FK zKFngmXguy^#6eCfVFh171+7^X@;-u^+tb-=j--&s4-D>u zPh;L|^MXq`&!)(l(!ocO4lZSom%t-A)-p#Y7EMWJP8kS&NN%2~9frC}3$6mk%+wBn zyVIwH{&yyUVE*9@?hUAY48=iz9Qi*^Kdz4s<3B+^Edi>`iY`hf!DZ-AiH$52wHb|{K1c78a^0r^>g)!Ee;b_K(_8^b=wuu`!!Od-A4Jv@^?UFIY* zJZf?~%H!Q^Rk19LVu%Vh#P5uF43cgGm3WAiVpp&`X3aqp)IswT$dx|}) zcw2Sn^x||8o{H0prDFFrd{f^191jr4b(qDTGkMQ7W6Y&_45dyi={t*^cEpu1ooWj@ zdokkJbSe!ta#eFCj7t={9Ri-XtBrJv_^5;I1%_kCNM&4CR@{3wB z$uH5$u#2Y&kiQj#NpPS6G0V^HJE)%P8IM z=yK|!GDcYaFqPQ2R+K!gY!k@lnh3Hx;;>Kf32Y9Us}V2UZ;;9XWagY<%eeStd0Sff zq85Ac$vsC%9>}B`Qp}Xhhc`nvs#m9y1V7CxKMhm+{Za$(G%hczOJ%uYIu(2o6G1BY zEWQ?4&IHy;zFxfsfL_fROHtQp_pxSZ@#sJC|hI)jGHbvrN)Ywu6f)T?CVD*s9oBwZrkTMp-p3 zid!>c6-`12u0?x?=V8p?YK&AixQ^a!#T&M>)p<9{<6i>&>9S@|E2Z@!PSIrrO-&e( zupnv(HdGo*n;D=ck-o4N66s4ZLrNB+oU8=JcZahSM1McWgpNjC(eJ>@g-C}9hh5Oy zQYE4Eo!~S00-KkN6TwhV^|`!kc{N84rQ1>kSBoRyJq&2bA)=ddm|;$H8DTGF3)E!9 ze?4jyMl{H2O!<@+0Mh5Ul@h`bP(CxXsz0k_ub#F{HfCr!wT`hl31*$l8sE^~eJ2yoyVzIbWAh*IWpL(A&c3y{{egH=ppCu|FTX5wd^Xb_Y*=Kd(r z_H3QthU+ry)!mBhz^%TE@u0L*M#VJx_jY{E z9r&z$2hyMd^pH_$Oe|9@3BJ78NFLmar z1jsjcB0X*tCu#XYT5d;6himRaa9cXF_FYId%t4k?;$oSZL4JMnZKOGu339@jBlFEXa9ccXxU9nGWwcKBzJ5Q?S=9d7%ZbA5z6Dw=6JKoL&#Q@HB( z%+mmlxd&y{iNwLBh-0u}@VyKs9tX*&8GJf}?_)4gImpS1!Dle|eg+ep(ptr1g3cE5 zN#z8oWt7xN2aR{p{04oYw|Ig6zJu~An|eVv#GA_IU`j8^Z7tOAr5*0IKTI42CxDgZ zvBlMahs^qqhRoB3C|nxA*KM=&i2YLVC^lsDZdR7F+?InnzRNm}h>U4CVQ{mQH^x+) z345CQ)NOoc%=efV`*PpMFI1hp>4{Gg_Y>pSE%JeOQdQX4|}6Pz2=ie78&=wiu@Dz`Jh0 zf*xTXnJ45MT%e*En6dBh%NEA77+!+!Fy#Ig^z}0pkULLY|LD-xy?0Lu`zxeFn*JLGb>E{lQ{qEFwC=IQ}m6b^8zWtIgWiJRzui{ zW3Sm)*_PS7KuW|LM+rE7!E$yP1+!qZL= zAgVuQz|R<`yPemL9?0pmgO&c=qcQFe5A5Omp4~+4M(=F|>&Yc5aO*C?7Mt5lZR`{!DUj z8SiG18Q{JhUC^3M%*gAB_JY{L3qB9v;cVMwmZzA41^%9ol*ZL8^Eiey|&3u4jxNyqJi| z1bZ;%!;HxUFC}8K!2-r`$G#&Q{5}zr3kDhUHpb+FmlHAhU?;|K3dSoje@Mg>0^CsI z%R?M^Hs+6snATtsV}vcY2Cqah6Y*;0f^O$X^vRD&kG2bMeo*2DvcvaF5uZfQu!{(dqp& zyh}}Sv4A@u%LynmB1jo4S=-8;_vH+c7nz4zeh^Sxs4-x0f5y$Z0U|Fqoe z{u7~p5;aVzgrqTP#ElaktTNklXD{^4qe!3Zj67?5`KhBn|-0NewBj3XmK6F0)!Sjf0 zEv=BpY33kAvt{u_4Vkpfja3wplk;Ofer#L#x(v*K=6*lsyt&{9yd9b_=QBfOXj-Is zK?fdLf`tuRs|af(JEb|-xeCdChBiv~LCjH}NmHyn@P8owkH$Z{kA^kd;Gb0uo`{co z4Q7Cr6@@{LNB5e4!gE?-kUj5S^BO#b!NubKm$-+;{Wr}qOFSr*IcAFI?;6u7o_}bJ z5047GnO3ae2=l#e$$SA5?TlR)GT+E=&@$1vy1{SZG{58LMSOTnqFkYwve=rv2fsf@ zJ3UccxD_|!$a(ch_E}%HhVz^9)(}J4)4Xv641+JrNbm~TTbXe6KArytJ#%V zyx^L}dl_>dl^QZZ5k!DTaZxJaKLSq=Fj;>|qSuwoO8~awToU(e@Oy-eau$hZ-&{HJkQHFDv~D=9B0M)nQ@aPYBSFW4ar8w$hR;AMgZnd}evP0E6OOgrGERv2Nbl7Hhc|Le%< zlFkdH2xmnsr>_X*^srFW`^O3OZZb)|o4&z%C7l;Y5wBP8D--IS5!PXwOMJ}?!-RnW zhf~PDk`A_#Ox_^-hRc#fnLh?uRt{^h2gN?~U@#<`V$c)Fd<_Ug>BfqS@l^=UvGK-8;zrr=zO z{EuV&VMA-`b1CNmwAk2A*5-C=AlxAkB?-@i)ag>#y|_u-cWg1Q0z5VUN@uEngXdfp zY>UvGQ7!7F;T_ZHooLh(P_}pi@Io5+J4$Fcu!53kx?z7NKchkvQPB7YesNwFfSG^7 zX=MW+uSq)2FUXIQS7r0~1i|206Dc6kGV?EFFKBxryylz$K#x z$$_7I-TI;H`*%aNwm;ugz2jlMpv!+!;-Z~0r-L!ppS&dqb}MRyEQ=p~tAg{GK3gGc zflLLDkM{6-BBiYnH<%6|%uvHKYTmlU#UYgy>u;Z$&skmyKJp*HnB-s6!?BytGxUaG zufxA2ofk+ENj*cvzf@Qh+iK83L%mytb&Q2P)(e6#Y%?1+#adR<#cSEEnavBN#KyJk zGb|^umVK6ftg#byQedq#>1enVH2)ST(3g@X)YYPwU;~rKH@=xN<%A0uREwc~i@B}^_^GV6YxksW`qXDP;I?>@=XI4a%@w zaOxbacRHa4!{9Cj4^o1PEi-xn%8XutBkvbdnGP2v-E$PU>}Dvg&Fg3>Wn+kp$3cd?%6ol> z)T~1n$jgH@}pj#^?LOTek#rtZjFxqVJg@Mg2!>tbp1t`D)b76Sx^lH`y!0@ z-)E7o`fgYOtVYs_ex46H3E})(j|Fp!6rg96hB@hZ>t6~(<&?7q8dVUvP{U}LWi4&x5^`47&> z*teU5x1(Xz3$XC%A7wC@Q6F&4b?zos2>4 zODFixnc%;3g1>uFWByeW{0~j=XLe1d-);Sa;b0im7CO%65aA0%I50qWt91kaZMGlU zC;U#MpXaMKL!Re_raYk2b!W`1ZiKl0JRUtS!r|k$gNcz&=Zchb1l%t$&v<8zY4U!p z6F&uiO&5L;*o~i7EON2LEiXT&EvFH+5`yI@YbNVm{Rzsjk|BpGn4?0&fuKy6*3}0- zbPMYGHR?(oAZeux>qbRAEOB!ru|bV7byf&`OqaqfumURJIxg?eM8=i{BrS2}eQA0j zBS~;(iXdjBT0Ry}5UsD?3-=h=LWqJjeT)tM1znMwv8clf@hBg3={lD<=}!q8k-f9i z5&j#CKjO9te{2?vP_@OZYaXX(>JPEacbKsjhPm0%mg;|fhjmh zF9*7~#Z0LyPEM$CTDbwTMLfxe)fZcfS8q}W_%R;f;m&}dL=a)K;?)CNjnJUQhmCA3 zUYf*%7)sx1*`)p5o9iF)q=x>9Fuy4g6_%ZHzJr7>{x5MqgGK&7fcqF{8Rw&D*Gq2> zXNMw7DoN9xY|Z2D7WSr@iB`%@Oqn-G>Zy=I$VWO_uwAUEk7Qh)i26NXjJX8IwDl?7 zZ%LSx_!?6w-SeEn<}5Xpp!_%?i8uDKaH0H^O?@Cp*HS)i16RtAlPl%Ne#xE(Jnd1# zU@e=ZeNS$F0Za*mEkOn(Y8(@`P>cx+Kypmj$i|B8n24*^vEez~f_$0%3|37ZFzeOu zjDjn>Wq)X(0pVblB_!FI?2K15q#@e!qIoufu$L}qX{8T`OVuq2 z6*9AKiPlSfIK#gc%6f`1EWkmGXB#enV4F>G;mD4|~FmIswksbE}kxKWX_34$+gQ~SO1DANs=@ELg* zsw|9gnK5#zIOo6#3dAY!8ceW`=H)PFRB*>cW@vTruS2?lY z*>i={y&t?Qnvmyqd1ePDcNV)N29;^sT2LnP@1$W~5czG}t^bLp^1NW@)3M%XTUMTg z4hT-5;QKVM{&CAN>2syvhPu27bwz$#m-Y8SNA`Xc`g}qf*fzJZl z1kLY)Qgjf_wrc{nWI@9%aCihP--gZhVf=Qc1la>wGmnYci?+D&UK|0o_&vhuHcmI# zZA%|yOs+Bi3K*w(ao4AV6dUOc0TL&I1iKyz$$S=0jY}p-BV8-hfY1eUT1Sd%RdY+gV9Y7!!v@gVyY*M?UAEfypi3sNMm(?k}6K<7YI50Pv7r=WQsHf3A$_d8pp0evt{? zfaAb?N!=VZ<}2#{D&04!`)0aXBGTVN_pR!_jqY#YQ*l*Be$|zJtNW-}*?c`iXLmiT zKRN!aY}qY!dOc5PxnAH;K=ltAjyF=7wQ{26ZM+;vMMkKZB}US)%nEc(QaJI2e5{~y zfmfbSD-f?+RGG5TQ_bLBo7G4xDIDiSP1`N%SetJqi~`Tm`U&BN%aIgPCz~)itP&$> z*gk<(9Dr@MV1ATL9(}X9Q$-m6+>tf}%{viJ?Zp35-=NRF#eTX|`|K{dzn$o_VRS+} zYIXxs1lKh+dN+&%Ng?f<# zmyHmlc)1@b{#!JQavnf&<~v&Ecj^8fJ|Qi`4yIm2&2IOKdIx`F{4iMBEDpd9oH0q| zVz0Df7YZ3(_lijH-8f2q?7T) zyEqgECf#tqNeY3NPE5<$O-^Gpf*%^;PY3v1$BNIA$#9KwbXb)`EH4M=B5?~4*L&}p$ z6N_;f9zBrX_`{{RbI`;vE&`Dj>p&Sn!5U?h1DJPB$)QCKi#Zj^bNC7oR~&Ikw=Lnq=HR4-wV(;?tGtsh-cI?_*MK zUR5uE3yR$O`|+FgR{b37t@;PxcHQi_|3Nrx#_4fJg~|;sMC{0i@CmOVUkmrRe-Sfr zuIV3Tq+4TU!8Q;>M$SUN;CY3_%cIV^Q$n>_&#HEl!8q#du!F1a=BD8A43r#C?AKr z;_G^Yg}Q$52?-kcBz_w4_9=u^FJSzW;TksObe5eg+s3mV zvrs<&Jaa`iFhYfcg5jAf5l{wj&9EU6Wx{JUr5-#Iw04#7C+T6uL(1whpTQS{=y#Ud z$Nt(9&byK$k=X+t8Zvt}^Tk?^S}I7f<}#M5!RB(h@3Ltq>T1=V`D~mhQp8+=Fb?Zm zZF$Oo=BW_U6 zLMV-11AnkJDP8xCcLS{aTUmyb^Jen9!L=xA?e^%lV1Mk7qCEB=_R+2*IOJiR6oN0% z$GWsospA`q%MsUC$l&Z-J|ND?X{}ZH)N_V`}`GZ_+vL-;ST^9ry%`CM+kug}?!~ z#=^{<_>A0zPa~AyMu;7$jMR}DsRPoXIzYy%WtNHZ?rtjY9+syZlaUH(%rT+gia2IA z8q0{S*46T~gGe z>J@oH#@k~WKs(dBG2U<_CzKO@iWtfXKf~`EAAFQLRg5|rF4b>{7-i>wQ9K9lXpDo1 z^>fscvO|)C=W+N$;(Y?{asLC?mVNs$=-6{}LY`1LGK_d3TAydITjNjl1$^qiP6Ygh0l#H{lw^Jf z=g5orG(!3kLhLYOhG*-e5ta!@nDW&cF-kd=4d5@~ zCatn2jF&&q3p=OK|3~qIZY18kB3_OMf$UG}jr?pm$IH=LzWFo!T7~%wqiA1nB;s2@ zPm>I5qPKPc_$;Fw7t@qZwAB;_HU)ubY@p1cJ zr8m*uzlnb;_%UXs-G4_a^AAZXV^#wH3I7JhY-1WX_!pAfG5cERoG@np%^EoVdG4Nn z7Omq5FYTfN0{hSWhXD%)Bn9iS>71P-^tZqnYzooNxNxzLNDtBc>S-Xy`#l)%Zam&o zh~aoo0n ze7D0TiF(}7vq~h)lPisU5S8h29y#3Wd`&xK#Dfg z;T-A3rxEl%gxCSk$Tu|L396R2aZ`Dlusj{`jC{QTZ~GWyoLJ^qXQTMbw(yrt;8O!r@Y!1IxWwsC}YcpF)s*n$Olk)*~ zLo*+c&;%b)sMxEM@d0-5WPCu|h6}+NAGdXY-bCBC68{@%3wVOy17kODww7$d6C|(# z{|0zMV;XcQl3SjzZRni96K1gno`rY^aLqr9?%4=0?XtN9uz$^V5}-^%P(OzO3*I6r zSh3Cc7I8|Jx#E<>+rt^mAivlFu8?1VW_F~Pd_#6@cG5`RM}k9SxJWRR6DASBZ?J-O zC{}aXi4nXril`3ag9F&245$vlQy<0$(r?PI&ZC#rYUO4=oFlv76OOh8aF6>7@wKCk zk#A_Uu~aP&`iTkU?aK0Wv@!B^M;rNR#f9h^?1tnO=d|Vt&I==Hcgg-s3%+(o`oxUq z$tfAPwg-Z-AWR)=wE+ayGkfBjwjsn(SA8+QP?j;d8|;N|^Kg*zoMM zmlClNiabL5*1wPVG48d-CF12cr(e*;^7>%czZK>RA8mlH>d{ETJBd=`^ zNjuK+bD0*LC=y;2=LQqnuC11n)7qBa*0K9ZCpETpXvfHDcEjp(2=~_5sM=D(wl6-z zZ;u0s8aX=Ho{~4~(w>V;wcm=+joh4E_CCFJ>;y^Awx!H|_}aRt(o|=EZ6%}+Tg?zpIDbvTN6w@8~Y=4mKOXl3RR?k zEAo#VNT}?JS4ZJN2(hDvkx+qh!>)QT+(M|LGsC{M$fsMNBp=f*8z&HytvjmS?1S8i z1eOL5K?d2)42BVp1G?PEl+86TltU3IAY_6?vKS@=s&1-6jR$p;-A^kx=O9~7@FAov z#eNMWiqSZT;q2(sdh$CP|yZv6<|t zcBC(+)Sy6Zm8jbz{P>$Q$C;+Oy?vYk1EU)JQO~z zd!y*FBcR;JlNYohQefq})uNAE8DbpSs+Fk6Xw`|kow-YZODx2cOKiD-AM&n4k5*cC zoo_gsJuXk~ApA6E@LGrC2Ku3_b2T0Kv#f*)WVdz%n|+Zui6p&ME~dNTm5eIanN0CD zbZC?~W-X-!>}}neejAcDlKznhv82z)Z~~f0eU^leLK4vvFR*+Tr}~a&Sj?A?p_eiV zi4*eKvEqlkA;kK2_{?z<1iPt3*zp=hT|z|FlgS`IQ(Z!18aFrr+1JA0yaf-R(OK%l zd6q_9!ilT_b{l7sR?B6b@E{wU#PGSg;TpV?9-ympPKUD{=81yPtOw$?6Juh+({CN}BC-gjCO9 z@F*kwuIfts)CsNvZK`mNoQY3k##IQhG6f^UF13a-jhZn}HaC*xoz+y{*(^_G3Pw&T zQ+T?KVEL5Jv#r42|9qZ(H>yj>7;f!72(a_)YB-~LmRPj&ES<@Dmi}6efot%w^Q;8c z=|2#C)-cbGAhcLRABOd{PaRHZUW)crzwp-jTiq)bBp$cON09K~x9Vn-b# z8)OpJF{w-vK>(Sgv34tyLiaQK^+Nu2@;zT{Zz5Bi!KA$dlOV z!yeyfSV}^U5FQNqLAD?+(<&f8bU6<|e&`VTnnHe%O}m8rumvb>$FxC%e@tMTc%=yx#Nz&>6E3yo_KAi77muce#H zMX%b0$o{M0DjcaJ!8q=B^ z@xfIwZjDLIm*_)XU&e3sEBFM9WPL+%MAtW8Mc{y2Bc$dgd`7;8Ph;b5Mu-(@7#a5d z21S~2CS@O$QVL^U%ZH@L&XqjTL1XZBl_7eGE{pHlD2-UQv-_NUFa=#TMnr<4%; z?-D=8pS1tm@R_?MNXDN8-UEL${_`8p#T%qM!in|{{mfRj-nKP*Z z{0`HtR$5+j5_KWLcO@OG@XYrZ9refe#mBux9mfw^Klkom!Z_}F)8qIDC?O=`AHqHE zKZI}fVFr&fQpWKk^xAQ(sn#>qk0cc%F^+$XpZZVmnVuZSk0Kx($B)sS7{@<_U&b*V z)t}*mar|@q*l{euk2AQf{sew<>oPqU-V*!^26v|FPvW4S@Pa_U~#mtw2FZGWy@(m65xV-0@%6p#W>2POc za=7yfeo^blaK{@su`Lv6nT2AkzknYZ>(>8k@nfuu-~2|rq$L63x9W}jY$V4z?<&{^ zzx>0?mk139x?3YAV;?aY`qW-GdA^9KnMejtH=gg@d3%atEAM!3N%NYls< z{=`yfdllLnXcdZK^Jl~=%;qoR6e|5IoWW+~60b71@^d5E1^%T|3X&U#=nMN94q=a ze$>VhyO`q)t1)Jnr`-G+W7WRoTwtl;s9vm}NUOD@*M*B-RX-u}#ea}D9Bs}Z`BDqM zRTm%Juz*O&3F#4!Udxv>Rf?(7l8TX;uBwlpIx&zo_dxw9N3_z0rshMlN#gEEhRGXB z$s(khWAG>=t9g9t1$?x9Lp-Yii^_4om9cI3xHU$nOVxJ#)TiMyI5SMN5q!x+=Q5g&)wIhQ?69f~n9nv_{#e%w<3M$0~F>-^PDy_PfpvHBBr5kiD)13Q&k}~Ls*M98_-B+nV>&;%2WBTyPmkVQ8Aqj3;O&V~1 z1VI^Vpg?qWn~kMrVtn{$Mh?9TUyI945NgyeR^B)}@Zg?I7R-RNaq$)>8QUC5 z8gkM0zADbssyJ_;3B3PHny}Sv3uinx-R<-~ zp=(skDTEahCZuRK1Bou2L|A4bHHy_ln41U-=J#2WDfZ76KjwFdH`{5M;Q#asDXqv@CUw^+Z0OYQCEY9QuItut-5XGmP<4w4}D?=|v$F(IZ!Yx=GHgi0 zxTclBVfZK0n}0&5)5bJzFb~P?-u?X088qt6cVP{f>lW}gp6eFU%Qup6XQ;a4xh{<3 zT&M1EuIuGow+LA|*X@d5%yqlrCz|VaXIPCfcCKUW|8%a~19`(yxhLG?{$hNqdog&F zkuulqO|PA$G}S&#wM0@e5_8>B{M3noDd)O`T_W8`{KFo zK*k<~k6U9j=DLINQ$GZsc&RiW2?MUH6$pd_L zu45C;a(qUPz^AdGw;{w9#K<=^*B!~Cj%q6EXcnb&9V4{?Z)~p1IlE!(^?{D?WIEW{ zG01O@#ivjPlg$Kghg0U733DAc>3q3+(@k|rDqg&!=M!9KlW4ASojG%ruk^t_WIb|i zoS0vvw&PI6#QB8>_a;4`z#QTH!fw*}<#@QXw{<7df@FL01jJU<-EH1Mk3VuEej5AZ zB!pDo$>32&VusnGd4|#MHYX#2=y}ZwxCZPw*ZAy0ANIR0M-w(dTXj~E3!FU!rSX3% z{>jmyaN%rn3d$%%$fj*%h-{k2WNoh`Tb-3t8Eo~~EGdGgA=+{_WjGxXKRKC=1V57? zh3gUrw`#QQ;B3czJlYu^&mjC+e6pim>^Z#)g{6X1@vWqWXl0mMbrR-G;U%e6Yh2J! zmPu2=X-tD(BfmPBPKVo0goLWwoWV%Dw^ek0i=3MRSK3LCf|$OL8s3;C$|&Z|7b_8{ z`@+60Kw)Ujik?`ZXwBl_Z^q8LSyM%x;7ojMFLC^cvEqw*x} zN|aLZU^jv*IAM#Q2CWuZlg2hHu&mA8Tb=1As@|$^PM>oo@H8`qC$4V1#c~{l@mk`h z;F;B^pmp#R__C%3cU#7urXjRtApm)+rCiXrXcgd`S%YXf$E84brb_G)y!@c{0)$%= zsi8nx<(-th^TlAuVemu zaS=xT$zo1hmDN^NTJ}YUf+Bh^4ryhUQ@|f)CS*POWvb-ZfS*O0QH`cw;?K2y3zy1*=V>5hK`9e96z^q7T<7_c zAojmc{I(2p0et5D62$gOJs)7$2Q`ecpahcXZ$K7oOoO9@NN#1p4~5PNvfx_Q09o)N z{zmqc7t=@mR+sYxaIqQ3oSe^WvZsXc^oNlcHj}iSsDFfRxB5~1)LEpS)4?^kdLzo& zB#P=KjQv>4Px+2<#{16pvj<$Nk=fX8|SyRcE$0aio#&3~XnQq^PM;>M( zf5!ED+%=y?I<-pl5mwR%C1mf`cWXpfyjk0t9eOL$z^E&@0udFp@cA5^jqSSGnII|6b&`-uQg;peY~(cP?Yn}00Bp}vGL*~T zSFod%HD6$QB}d;}&kV`at=z!Pxn?Q$M>s~=wiV6mLR<$W_;Ute2Wy>}&wS^6FXMCW$q#8Ez%?$1Az8CHkR94gvXj?9gz1C@ZH*&K5#U1lxf#BJl=&f*l_EL? zUq^@+ECj8LtN_vRG8mMhdhl(W{obQ^$DK$dX+vJZ_&(U~{TRQvDeHf1=QK5ak$`*K z#;W*%jS{bTqAhUB+=ALS0&R#CIlXP{AjIh9Xmx33s?LsXAje@5vC93OX(7d8dQw_s zg>ILSTuClN(f04|obmOL?y|BUx%KO`*LHhM2bM?^cLXe=D4#ALlqAbax^xg#n5433 z%<^s}uKD=4AzQLHP^Vl#pgb<;7?fA}-?u}qj|kocN~%5wQE0VWvt*aYqY??K^Ofk7 zhpy06a+2PwsK+z63RRJ&-VNBc0c>3$dk&AGlM$e_Z*GIXFh~;$Jm3j8p0fQWe%hjn zVbR-AGj_Cv-p3%S$t6fS4s?CifyY=&!5q-N+>DjQ6^363?@8X)892z9!FARpI?MS+o1QW_Cu+g}K;9TKWzlGoWo%pCc0`cQ6c)b4F;oJ{iR_70FKWj{4 zzKxH5M7GahsJg*m(6yB4Nb)H7MX1@;oZ15aGjleV?8M+&x!yFv0CRhd_MY@K$ z<7o(PFju?>i4*3E=qqGnb5iDBe6-yof!mbBG#Hni2o4yr!d3Ahys=m!~Tqu~X&*D$2J{v!gj&xgk za4G<_&2y)q>I3;x5~RY%9_Aj}wkS)F84|e+nH7bw&t2TX#WuU`ew__tn=t;!Zy(G#t^CxjO(r#nQZ-M z2q>e81Y9bcsQj3!KFa*Ryt3G+3&HJc)W^Em>ciYeP6T(|+4|a0AKND9BzBVavE((Z zUqH0oCw_qy&*1$Wkx*eo3CFuMg9G8-f!V{^yR8ZA9 zEUuiOHe82<5W$_^xM@`&=o7G5*E!wk|3t{Mc;B?+>^ybgV*B-3Y1zLV3r`2~s}6S>_?11#J>7rL8?M@k)r!@ediM8`k{ zFch2z5C@|Ti_a!rp$O1E)F*|Nt3AmIo5X}JP%V34h-y6ua4`Y8!B!a0S-Dp$Ykr9| zZG(MCgc0==Je`n|1;ZSRkW_ES7AvBjM%1h)$H;8(f4>Mte83hS;#YMmU@Bq=Qk#DQO71+FA`rh8gG< zh;F>PEJC5c zJnG5=!#OOjHkt_>u zud~nGbe$CMLTD!sqg2hgsXa8ilQpyTRd+?jb&fjK13*S)5wZqg8+VbmF$fy6!ES{3 z8KDF05?Z|K@m}=v^T?p`y60?%lnWa2l3b#^CWC54*-)xP`Jb`xY4Ki$BF4u;$xqlU zRfb>;Hy0%=Lv2gB3zao850dJ$COb)8ce6KHvYKkhXJ?&1AaoIN6il?Z4|-S`CtI+z2l3N9 zKb9f25ueiVK{87Klhz^bVOtFAmg<_$YN3N%HycA#MiJUVd zP+q_{C1# zJ$}bxEz$Bi=kOu`j#alt=8@4gNLO7_2An1BT6vn-9PA-DI~gg5$@g`b=#nhpG#pJ% zd4G5pNkAakJ6ppS_D)WQtH>xhWeud=a$Y3ZsK4=iGn4v$4+GFFKpI>AcM;_UWe`Rtfn?@AFtJv?UVZ?g(yXbBQ*` z_jx2>YFZS8yd!y^hwscr;d@D4iRpWHOy8EeWRNB2>376;Z1S$)>hqkbd2bE2)2lCt zXLfZEG&L$aH{?%TJME1+W^9xn+yw66*GMLhsUnm44LtIINx9;em*b_zIZt6H?0l>- zsM87O)S1Y)58+&O0b=sF@!$NGrQ{LLEy(vFG>g|_!t|U7moR;>BYFq}P~{lk{7yi$ zw44>4*HWgFHR{MIY@KT$^yLkD5BsQ+5!YJ>0m^}4$}OOEKNG7CRZELId=Es{)-hD; z#BY~u<@7Anw@WJSsyopdj9==U@}jfd7xm&=X4yceN&vP8=YZ!{5Vi>CZDjp14WaVn zM&29QH8FMgI7!mEgt*kv^bo&-^%u`9&lA!4fhv7 zWUiCN(8fC}s=L|`>Abb?2WD4oi(nv;Q>BK;5SJwPy&hO@GMI*?@?pwqDIM<1Ayi1;-hoel4sV6{NEAoE#iiPBj zzTB?AV%)_n4Vi^)>2NnamiX_Sqp&VRzD@J)5TbDZ_^}bJD?J%o^28y|sQm+#c7gu| z^FZdz&pjFL?kz`t>=|kbnIsCX+&tZy35TA@bLc<^56Q^AJzO6jjl4%7uV?;A6k+W_ zO$5LT{sljNUSl)Ozws3@dt$^mA_RGDsa#?=k!uYPs^X-U9zA8Vi~1v%gB~nIr&jOAJ#SQZ-=CEjC1sK1PFcbjcl+S zY`2`jAl{3?`@Gy5z2hm!z15Z>?0dNQ035C^xt~d|!99f>3i8$~?zj^-*0g(W4d;b! zZ}*A-H@R7b9SInD@j^Vl!QLze^DEyS!%b<(N&PE1G5_MMl=x9A^WY((luUHN5@sfC zKkWAOUPMONU-D$~!kN+3+HFdXyRN?{Z9}HFr_>eTIx*h0+TnZ_r5;Ioh(Np;A)cL8 zMbOjR+*S4jXFm<`zMO?=mFcw2*X!5WBf&~clD<%qFszY%6Ev_C?s0!9X#nd%uti1& zAHm*xurac=B{+d?XEqn?E7%EXd9kzG2b2e2;!dC5+}>?%d)H|L0nr6D_EC|;gFO9GtKH>#+_nSRB2bsAFEaVrWY9^5mMuj4WZRxlFwNfoF1Z30}t5T)Q)TZXca z5u7o<>Bh&JM6nP&Txv@AwOf-+0x6+d#;@tY2e@Nc7=#04+%J`!zQGZkaR1eJwn9C} zM)i0G=PD?f2wiH10LLHl2Im6sj#@bXrW^=o&fAf%lus2zz;-bsz#v{h!V|YwS7Gu^ z_RlqtpPu`a@&w?TU>Mkm3$CI6nn~w*Zgn&K*69h+Q{SAPR2_DRl9q2%aIQFx@CL9x zY_t!Dsb1b&#Cg_*{0bwsyuAFS6?o}X74~U)6EM7ap%UsihIR|(HNAJ?_zl%X`7o^; zZeaARk;?*T}-qRy=mkDjUEtLHQY<(k24gV2EqxgnOpUPoVu zPnLo3SqPVs6s=kas1bHB`B~R0eV~uojb=N1*f#X>96Ip}69?*3Q_f0+pGdgXzJT#= zbuPYjSOee=95nbk@* z>@SQAc12{PfdD|DMWDNey<1~PFH(Gx3P4u7;b)l3G#2M;aYesv>}r)#h+;!rn$;tU4Z3pVJ)t%1cSF08hh?L0-;+aiE?zf1e>^-3vA}txExb!L>)efMPJXlfF$rlA z8^gyEX2EpAYGD#I4=`PgXf}H@9~WpCN`ldnj56rx$Cyg21{+Kpn)zpm=CZnfe~`1} za-}ub*~xkLmqWVCJCCBU75}AnrkboS-$0wW_DHl@B*~=VrX?s#LzY;SbY!j3fP@Ak zIotN5owt26Zu>{Rqi*oSh&_-WQrGco{GI|_K<&gK0yVk}{HAd=d<COiF_lNdjwK@u@Hfb0I+&D;%g47vof*lIb zZ6<~St|B*zRE|! zN0e=JWGpAkcn2s0!G2c)YgxfsnY&Mlw{4tLJ{jVz-MK8{MG$-#posGC8C9^W-7Ny4 zTgg8;McY(%JlEO!hR{weCAv5x{lZBvdD>wO)KPdDS}m@dgu$dN*t4SbR8Uj4KN(%La?3aeP#MQSNk;0W z@L5dNpduml`Wd*fRHxAtbBNBknC6zi>iE{W7RD2vXRN`!NV>x{Ill~+wl*LYb$zZI6szQ+yO zsN0%q5%W6taj$@iXs8Xi#_DjUgMQCKz2&^M_7@Q&IF^}dgPEWz3_@D7AG0*1w82pA)$U;?@sYYUd~4?QJyj(ij1Y#Hku{zk5@ zwP5jmhz}5}Zn-8a zY3i@6Jb}>;r;HL^6w7*u{e}%O6ofIMXV(ydgz>~0HkeR$uhHse3v9wj2 z#W_d}!A6hYE|O*oc@x#0K5k@^ec*%&;W|hP16G(~W=g7JW}69*+7jZdff%Plod!mF zex*ttNmhm;@~5dpN0LuTKb=PtQlX@4uHW^*a2+nRv})=8L!jv zTZdeNuJClL1VRMWLRJ2x>tONu$`kH*XAMkPxBY3|&m5isf3zYi;=ur{^-(DRVd7+i zb3wa(z%3P^3-fyAfSlqF0D7JxI%qlTd;qPlu`S7JdMg`JvA{x?lXH5Q>|o*>Gd!`% zWgZjHT~Gw0$@S*{g-L`W2>|VlLEj_Y*zTI7O5t5Hwj87xd zK8%p+M;JWH$lxtNT7~*^;bGu;A4MqUF`YZncfbQ{o`iQYcuL+`yF|+=$Q=h5OQ9bA zV+fDf=*Q_v)jz?Xbp4b3fs*}G@|&(-s(!t|NA4f2ei}h}U+e;_pAmPR6}dI$k>i2g z^JIaa2&rC%xccSz$euCRftR9>K~>Ocd=|k8qR|T`+}afg#>O4&?>Q*lHJ^juYfs7Y zGi60vRt&>~EAcbymG<+sbg+}c$d!5=7*S%Vu~jqfC+8OrA8 zUKrA4yK@BC6-aS|I`s{nEt%L>ZKZwdgn-}#$=}%88gXUcI-#?64L-x`q9}b@;*!W; zyB6{0Iwq# zGtzD}#+E27NJU5zxO?dNRgSs5$Ff~Rk- zxn*hcl;jib^(}k5f)Cg^n99eLXsPEor)iZ+lgsT`xf>!I>^&YUgs`#5VdSrpTadEZDxrOr5wNYYFg&c@8{;gT>s;G4jHtQSAA>AhW$dZT>2@;f{u}tI?2r$B_BGc@)T*Dh} z-wff%1&cs}&9~=-YyRoZ>Q-#kq4A^4+29e z4mSrvp>i)m#spYJ5YIXk^Bo0U_BM{@sb_%X5T;yNEXeRF=5;|V_u=h&0ve9Eci?|V z{8Khz+VGEycgnZR@xM9TDuaP8;NK1Y@8LI0&vKZ)im)2~*#Q1=KCI#wQsN|hN=81E z;7LZM!>f7Pf+S5s%nl8pxKbMi5xcc*jN2sGcD@gu@!p)(+0&Pc_aAaj@RpF(;$es* z#~+!&WD1vP;QH8tyEc)h;A|rgf(@f&HaFGcqWqja)lI6tZ)$8nae(o z^Hs(VQ+mWLq7AbjL_L}Pc-kP5PkDXgB*5{8OGWr2_;1)R#C?DdAOb(%!H=DNv8;xN zC-h_ikt$|0GIyTKt^&@6o&mjw^-A$Nst3O>;grdQ@n^bMhzRD^7?d$5aMh&@zdQIY zg0RoXB6Xh@iz!;0J~F0#N_#I3K#1&^3xoqU=whTZMaYWO^;E-H8oDGKDiBaq$w;Oy9spE2dSW7z&uXC|S45%ER!FWr)m0bKu5|B?&UFBBAdjv2yslp`}AUg}7w zUodqLZo3d+`zqQ7IRg-$^T6UGZ0qi+!Asf2^Ge zd|gHP_b=xr_a-+<%Wbx{0n!2~LkeYwn?|86n;;+pA_YZ7K>^{ETnf^Y8^R)M0TmDg zS!7d07Qq!25D*s@iy(_oHWyq#QBnUc#P|Dq=A3)aP1-bmK5yE4X3jh_&ph+Y%rno- zJTvEU57T!dPNUGww4h6Y+5MSC{tS;z98imuD7%0837mIl_irdaiKivVH-xJgkloLD zOA_Y8r+CXpPxGv5i_h?a5Rg-+n@C2Zn|SQub~dx~edu^^!psLNNE4r8U>*M#yxE%N zN>VZ@Oz2I>{RF{2WzHz*USvHa=bj^BOk(&v{&^n0Fdik0z@;?;lht|$J~SXBFx+1B z`EEA0yST2f8E-}1 zr7u?9A27bG;Qb-FtZ)1f*FHm{Jd^i4mce@G*wt89(5fp2ga(`AZ&H{2x|dZ0L6>`g z)bRRSgd*ARmeW=Fb~;XXU7Q93#!>q-hbC#`@J2(?pE)>)gtBiTYG!6$SAUrC1RaxI zo`o7ex6a-uzk3W4r0<;P(_}5>A{+T4QIfT%9oxvvn~pKMC7`T{UxvhFlbnrZ7^V#> zdj_%D0CLOW;d*_xtvHYCGXzYt`2TUJZNx8`Oo((pU-4dS<~-WqTDRoX&Zb5nQ-`_}WC z!|iZdR%>-8#!)^Ot9Z@*4a70G`0PeIx}0B6RNE?%;PO;i;}h3$X}!{%H=D_RU*;6e zn(q33)0olBjpd^%&&K1S<#2XuCdZ(9VDto3-Qs?NOEj_kRFA8xe;iF$a{>88L^DB@ z#>48=I`9m_xQTdXrSPb1jHp#S16vYMrABQmAMPYOVH5(irE%C)mA-7-`bJQ0J;Aq^ zBYa!&9L<&=CStiMhMm;eUH&aK?0&~%RMp<)X2HEt-}SIj&sKg9h#jRr$my=UCa0_N zIu4ET?s0w7A`L6WSm#$+kRP8Qj$iGk7Qex;1rw9?)6TO#;|5PgqmRn^qUiAFsH88k z0tnxPZK2=l6A3FtL43M0h!ny~@K~2ZeINHJlK!sxq|dLO$&K$?FAB0Z%J2TDoapoF zv+p8Ye{o)#u4R3;nozhu5!L6Ewfw?2l-5V~@nTsa_GKeIX#n6>-X}GNdX}$m)p_C;pMyJxqY?8=2-cDq{mK@9)Bi6N^XcrvE29z-tu1s4o*-} zB{vG0%RqY#`Ov{dVE85vI1X-RftB)mEaznym!QdPbv`|`he*h0F}kCQL&(Ofx^FKF3u8;H-; zVug`#M5AI=zKAQSvW=H7%qrH3XS{qFhiZa+VKt{7m*h)aOP;1!zKlaOUcOwvPCT)E z8HcAPUvjpVa6=l8sS4J9YD9T4^B-6(G4qyK?we7n=4ni7ugDoSn(~+;!C%HC>&}MJ zN6(i33$UD7s2HlJZHUMG6dhwd!c40QT!6Qbv)*b;6xB`&w+sFL;4|rzC+C!;V;~iN zjI6Rf5A(Ehepq$GBDSri6&M9ISSrnq!SGLroOPFQUW>5md)CO(oe_@!_kF^mx`TAq z8QJx)GO5OI@bPtqR-&1;a_Ux529&idzBKqU2zOV6&}ZwmgEBuj$7MLU4vq%TCZXt> z5dK*oo|KIx04v)IFg;baMySdMfJGhqF!iJWnFUEVl_leBtx4-86+ySGZ`e0Vop{nUtw6oY{rKb&l+DF&Cal7sXq? z07XLYg3S<850kgt9={9rWHsHeyzWUG`Gm*6KNG*}Ec=JRakJ{lK(V6bRjd|Ufv66t zS=XUFa<=W^6^D^0K%b41N`;yg{#m$9V@ihNI34+@ttEMWd5U>HRfsQ;ye!> zIl+!6eon^|bx+Bt=+-_)w$c^z4c^G>y2riGjNcQwZ!x+RHJZhvP!(3J1tCCZ6NX&K zx*nxVcqN&W@XC^I0CZNkq2knd#b-&@%&V_VG0%MJ^Gx$9nVXz6@)SJkv)SG} z*NZk(KFbbvDwu{>u=U5rj7GIvp|7LhW)dj~&mdO%h`qm#J;==t|3oSqd9ykTE`G6k zR&3AY9-wL5nYD}0+$@mz74)G!FU~B>x--G&SQ=N4bf4sdGUYu@; zJFbR(&lbgKaK=1Njvx{TLN%Q|W_Pu50Okk*az7*%>h z60u-eFyE>L2?Ivjh5dhQBT8cleZlnF?8|gIIj~39C8S%3bBHNA!rcQh$+e6?FRk z5=6)HUZiDjt){*bN(Oq@qc%on5gs?4YV2CMeABrQ$vLWs7CX9#SM!a`RN~#O9DDAL zOsplGHI;HWqkbDvFHD9m+E&!-wN{gP7t*vwx|Q4iSf5H`34ML)HLlO?#E4H(l*-2l zM2Az4>~6KmSFAx{@_eFZAe%7f8 zk={H9_b&D8xYt@oSdIkOCXQD5{I{n}ZnIu1s`_V_--WPUq4isP*Bu)B5cO+S`OrJI z2=eGwl1L_-7TF?pmlq8MJCNB`-VPcI7-314RP@0AarxCQ{JC@F_d;Uz*W}&tz+!o~ z3~x%_eckdGd~Nb>mssANBpk>)#a)xU>&)zmRQN^>A7*Prt;xH;(oA7>uVhE3p$SGX zIY=7q3hDVUakJDUTfglI&M=x!CwUUv}k|FP34&4?bp)5+&M9aehyd&2LCM<#@%xdqLYkYKu?8cC`P zTKn_tvM%_2?dUqSEV|%H#I1J0x2v^rm^yDLzbwDn4gTfwi$T@?xGO38xvK3-H{4UXRPMRL+g6knm?RGZggx)`*cNPmYv4!4(;Z59F$CO>T zTCVSY};(O&3Xv(>Au*=ZLdpfPj!h3$zLi8 zwg{w-i@9kH#_o2mzLd3iR)K!L-NNZ{|F;I5e-i6v0`{52F@!l)VByhsAm4^y!fcB6 zd>I76{?FkcgIAG0@9rZ}SNLA)Gw%=-ImZmo8!-&+eqv^so0(+}`Mueh+={-gp2 zA#Us-cFlZ(x6Dho&sp8gGK+@|UEA)8@hri4cfSx*;jyj)lxBu&818Qe{A>Tp?%pa-K7?fqOgioz>n}LI6&H${FkYAOmpF z8K)8|ifOkWU}eU*Ww;CpCh%GXTg^NsQxEZLW`5o~Ei=}-p4}zMx{~tFx_GT^LHQ$U zFIN&)?O!{(=bYVMw;D(NByKN<@^K>Kg(K=iyTV>~+yRQf-0^C{-K#K*=+*ZpY1WszYROfS_`N+~I3^&Ooi$ zMfK4FtwAIJ+#Z%Y*7=I|PE;XE2eP*dD`vGKH}9T#8_oB(A2&MrZQs#-0ODht9A#48RE4SGjI(8rvJ;@XcO)Mqq3poyPKmSNbiTJ?MTM&ogSYJ z#QtJ>j82a>F??-#j}I3u8s^F| z(|~zme8R3Su7Oj!HH}7O!zY=d! z{pwWpo%;x@_N)DRy0Rg&{G8nZTwV^r{j}X5p;v@=?%~JJWV}6`&v$D1qmSBp03_FS zmsu#uu{RVw$ZJEmn{T_EJNSX}H7r5&5dQFMMATMwf@{b<1grRBLp5UfRk923^Vhg$ zc4Q~{N5I@h2OAON-+Q*+FSA81lfjCUzVTyXg6vV8mfXx;H-`y9`78=s`6)rzh1v6A zo)u-71U#Yu<>CV@!wxr#90xQXLiW>Asm%jqbT+ec`%^gIg zhhLa0w5k)U{vZcbX!+lN5cU0=nJFrM!>migCmh zVRfPoQNDxD*xs^1JIc-&Epb3*^4{CdbtEbwpLS>sw<-~>tyrbx9A$JcgNS=~!0FRz z#Wu4R_1&d7?E|O~SjG>m;8|=N(BSO3d}S0jnPQ8Vq0>vd0Zg@$S{~vg(wwhReG~yuN7RUG%HEXnA1a*9na~;F}wRsU2ye_HL-WkjQh@0bRbm+)3QN zVlmu6J@jcJvHfjN&vl=$2rV9_4&(NO6TZovrT!u-u=b7_9Vz{9X0^$6RGtw!-(NZc z_YO6=UmCXsXzvJy<)+3>>n|X)VOzO|-Ov^fka~CL>`$pXJhdGrC+Y`O$&fg{yzv*f zI!pkqE|GV8r^Y|)6CGY?cbGf{sTID%#G)s<*y%@2Ex1iRl^;qM$K&WOw1dNt5mz)$ z97n%F*n|8;S3HhNwp7Q_o~`%H97tLWs~LNq1w{LQ9)~+hcRdNjLHTsrXypZhu#UAS zGuVcT2F<+F06`y8cs#ERqSXe`m{R_P#tMy}*ib8u4Yh84Kz%-7EO&-_z`k@}w-8Yp=rXHly}YKZ?_VS?AC$uW8JvB z{K$sMyrbL?*_wcgSK)Spq_uW$kGmHYjPo`L`CxgB7xQfL|mWF`gk*a zTsb7QYu&p!?%sLdy!YvwZ3q#Y|_2gvbEmp8=D$Qcp!1RcFqWETxbQ4 ze4j`CxH78hJBZkQqXN2ppeB3|@0C}0RaG1e`F7D-5Gg1;7+`x#+(ui%8|Eg`s&z(d zN?K`v^aXCM{E~EWfw|peUlsTnYf}O+8V$=$Nh>Qb$V^&Url#db6SRE6VxeTw5-ndZ z-Q~ZcD6K&Jh(qaj!U?vyHMa7ZPJWy49?LE@m8$GoP`-#d8~qhwRb4!pTPM0X z`uuM!&8O*2{aQnrpFso7a1sG|_bMRd-S2Rihcc2f{;|4Iw}!IIX?1HTGu@+LD*tk} z2Eq3eH(o-iTUPUJ0@1U+Ry0~qQe3UEVd*$@X(h%tv}M(ISmMh&7F~&am_0cpnX{3w z{36*grF{v<4~SYPt6vaNS+wZ(d%45E<*7xt{nW(#f`|ZGbd#HEVoE1uG%>#l+NcUQ zY=))SSic}rgCrBW^m>RsT%yy7sr3+%QmwO|t-CXi!)Gk*i`nu`m#{(NK8$Y6cbLD( zOZg*Gs^3e;%DPLt=!@Y?1hlwA2x-lSFAG}W(bnYNMak?I6Rl0#+3%v4<<0F4eA!RE znQiP`U*7CIwDFtkYYsmn2m9Ph#1pK>0t9pU<>*L;6H| z4~4qt?Q^a1Ira@RSA5ex+d+GqWp}({-CHbw$%?DVVtHTK-I7~%M-cDVhg%X*8O63O z$VI2&^w|wA^KvleW-;b4^5|==2-M^b5=rkPq|NT!Y7XBoXm~i!uJ8jqZBh1vc#K=^ zjRxvo6NuT7U%At20gGS8O<0`C+dZKdLsqlD{_%6E^PA0HQ1G1};s=?7GAI1NpPSS< z))7);ZOykUL}H=DLITZJByz0kLn127X9}zSgj6@%9%-KiI{=VR6+tGs)e^Kdd%~o; zQ@_?wbDpdljjZ&B`tu#Sd6&t((U#8~`EgGhGT{o^6l-Z6nOi}GVK=t9FYq~%${%T? z<*%rEa@oom@R-kwpQ*~Q-5jb!Vd1ylp2-c(m_z@yb*3w6WBP#jt*W0RC|6hc!#pdm zfh-^|frnq`#dV@~yBqZGuIJg|ns-FVu>OF7lmfy(?ZD#|8b*&_wTXlOMsPXmMmf5U zSQN?D_OK236IlCYM`j+v2~$q}djyfP4?iL{-`;NP#=ulQ?x<w)-Piuo5`bdMVCt`?AoB1A0F67)%plg{}wH&#r z)@y5fQm@e)2|vu^`SIr@%shlGDBnbBN8eO)Ye{MW?geCl>Vj~+D@`DG@2uMfTqDo5BwMoBY-hu3ls1WzXD@s11hqqgi zFCSft$r2lc_(QyDyA@ylw~X-8MdZq6nxb7nw6>8V-k6r{Hq@D&BYa0wF~)yYIB@j z2xEFiKdmI{FR{&HxmvbaHRNlz3lUjqyAXR%Y_l4xZK7%yqE=XE7h;(tb|DW!r0hb} zekyw4zfOMDOVt+SVPd6hvu*$uxAB|s(#GehxhfccJ9@@N=+QZ&MS$dFTDl-V@)p7{nFmQQj^%DATib-|8x_#~hR#^I5lmEz@OD>IkF3w@1XIFl z!$_GDDx$^qriAu7U5R&xq^==vN?1*}22;YSJyX>r(y{JVa9zmTQDGX zIMSkgw7k+abowj|hpX94FyTB;7*Spdqe^25y*-m&eb+I|(2;3EDe2wSRYNyPpFCTL ztsf|*CHMWnUfYCi&`x`s?C%!2!#DHP?C(9y6IS02AweX7 zW`A;1vp=OvW`9^#@Ez8n=#%jTwLLfU0?oVox{8gM>=$$8`#3CqU769kutJ}QUVazT zmw{O%2dv<`bD9eB@oHq15oyE~o9ZW(HArgRfOMG?`F)F@uC&p8 zEO(LtPBom_x`m5Qg=;Kcj!(;GsX3?^Qk{do5|o!fY~?lrgYwlIAm8qg*S7J>F9`Hi zduL68t1`Z^C%k`AjSAd52ku$CR_%TdF!~B{O&2#0acJweCSm6BVag7@GrB#2na9JF zzu_>C4-*X*W*)*8lz&auqj!2dUS~p2Jz~RHPsQIlH7Xli!#yG! zUc3aWu)zXIy{1taaUc4bP{)t!rTK*7m4zLEH= zy_mwBy0=EQ?xo#JG}v~x*yYe&E&hA3=+74EG$M(YJ7$MIwMizO%ruhyjHNWx`UV})_3kP(m77EnEydba+ z2QBOL0^ta|n~U&?GkOk;dACY(2$7u&kETvEML!0&W{4+3gQm(Oa)y7%a|RRPgC&L7 zDWGx$V3kwOSm$4pWnHBL)v=&rx7p}fE#m+g4@vSdm87W_!YgRqF@a;b1yEr{bmvp9 zfiuMvPdp!utb;B5m`Y}thR&~AL|tvjjESo$TQ%*LN_!cz6ZbxCKz;|>>UsOe8gB(O z-iE9V7t$Zv-1&G^>gE64bjth&usu@IdTA|!HBBNTr} z^1FoJKlzDuX?gflSihQIY8_QoQ0IuAuxhMXszO z#1C>d7TWq$-fa)3*hB^9wxwt#KkVJ7cEY~({MIJ712Nic6qx7tL#Ce<4=~=wr2H#* zx1!mVaXiDC(+IV5)$I120A#A2vwP(yK?>`QviZba+q$XBtC90@xxwb^AV zd?bKIPPwU(Q|aQ7)91qSpg4>{0`+BB+4dunN6OOLakOva!N#QiHz|( zVs@_E9k?$Qw-t}Pn~wewl$U|8@^cdAT>&n7eWyrjc8n}Q)$3XS00b zf7xNPD z{YuAIMq^5GHsLW@FR0B)--rzkHP^z1My)WMkG_omOrb=mS<6-Y^6rBEgdJx)twog6 zElPQb!Xwo=i~Q8wni`p`A0}yJ|405)YGeomYh(p$WcAyEVr{%FXk1RV6MfByX+Ckc zI}R$nf8^^Vs&Ayftu;~s-TP=%)_USCNpDcyB6(;f>M%*e@Mt~VhjH>>q{+LS+TG4Gs(!o8_0U!ursmxD-qj^R;*m>(VupW$I#tf+p?_K*5a zK^ol%5P!ytls31U5C5_6P0#H_P1c=iQCasrlgVZ#GuP_qmw@2-uW_iOUn725r@a~> zAC&(})2zG-0Ga+994Dz2BriMgVv0-Uw~Ati5X!fayfxnE4I%-|7!uw~CRlgzUiMO< zu)U%D1US~T+mOp$Jv@!<>=xPLblX6+X>t2>L9sDA9cXcSahmD2pthml^L0Z(s~-x;7QD|x zo8l}yMAixFbjgU}uh|Cdqcw+5iZAPz-a(uh>${)pjhl;$?hLZD1%5PsS=qMFC^p8y zIA@BW8OU`r)eWB_8OEIo_#hu{S>HWPz?QdLOCo2<>ly2hb&X8p1}rbXd_`|2hUZJt zpi-Rn%EZna)rWS4&!S_rw-?(@1GT#o$LX7ZWGU^nSodH0Y%1a)9EeRV}mot^AHQjR6JE ziI!HbhXS?FEan#O4p+z7LJeJ&4VHN;s99cA6=#{g6|0%j;lx#UE?+0>0w8}i@fVoRnIQYEY425&VMAzoYH~pT#(K4 zG=}F1u@qJ2X=#7V9d6OnAk=0ev`UvJLJiR@f)RPShiQSpzNy4EsdHkZwT-;d+8==zH!SwDMUF3Jv^ICf+?3Ip6nozm=2reB zl)f#@4crIR7Nk+IDG#81ZZIr2Wwd5(VXhgiwQub6Nn5R1zTRq0JPco^`g{f1YL|0x z+PdqeFUqshUTJebWPf47tyU<8y{kLf-lN)`5u(bVxKNs!~epka;EB&jb{WQ!+p-t8;6Stjj z4{2RNe?jzrL>+o?{mil;`X}D#O&;z(Qo0RLHbN9>+LJ;42Ba$;YiEgG5Boc?*!vH> zVRVpR=k@2m`pwRmvzn|i{R=P{|AT{Gb^|n^m$k#6pxj0gD{mPl|5T)k2nu@No{yej z|GInvt+(=D3)(Urgys$bO6Emj#Z#mJ6ND9!l|(?Z%9U?|#EQxklrMtG6;{qky9f{q zS4&j1rtss+jf<)@tgM98QZMy~Xi-pjoPd0x1O(GW`IzP4pUSr)uDQizgOLT%+?D&t zs|cQH#;Fg{y~9sIfQ~kQ8n?D^B37~V!5%L{=ij{OX!8*gI(Zv_1mI}1oUzV-C*zM& zf$B4$LMylI(B4(eao+OAH$kh+C@P|^d@%{aOL$;gsD>e!PcfF8PWj500&^cG`|%0r zt5)g`&kfHVQwJZ{=PA_It3#VO7Y}X5M6aE77?WYFG7UdVMwLcCq|~Q1YtQhj zRLoOVk=pH}B2AGo*jU$pC`PQF_>fM8_@fZblF`PN4&SRge8=smSZD399_Jf&vDwe` zwSY49RO9CT-zM=cu=@Y_CGu5rmmPr&wx5~r(`i(MA%*=l zh-X%EZ^2oYWRiOeL`=23+#T+u$zfjDLO7p0YsJx>ra8Myeh=e=!ny}_W+;f-@Y{8O z7>7gx%!&ci5`aF^C}aZ*EP=e?zv0?KRTBAiAzTW945TYU#vOzBlseZxi{UHYv+KyP zX_ny-8uef(GIVV3?VUEXR4tnEbAH%R4aPJNzfElWhE66zxI2Sc-_YiN$R^(Fcfxqh zhek<+!t4vKY0LzDtJq&|a5Gq_X(``^1tVu&l<&}srzA_nV-$k5NTX&NSyeuXy<=0P zr>&K5`NLcjMm3}@Uxi_1_yx6$=FV;I$Twd?!8A55D9@zyqtiiL9lzO!wQ(JrLRrcU zt>34O(%qoFzqw7yV`!IhCRxPds-q zgxrAj9vOEtInA#x#mvKq_oGny)M0gCWU^e5BGtYx(0~;&@O||HDvh z`U=#$FR!@>T7F)85q?84JiLmsC!zQ|8O`}wtFwd$%&?b&+A4B{cf0VDd+Mu6x? zb8r|#&ZT0?y|lbm1c|~b>aO_b%CA1_m64%;CmW|G)PiVZKtua?2g|zMQ79)PMc$Li zo}|fGZZ=qBI{*P~uNrYQN5R+8A>GI5aw9V)r0Y?H>xs}eDxf=>Xzmz2j^)8glpEBW zXH(diw9Ar9fQ+=&t+2vJ>Ru#?AIEA-&a_q=qx(4cOpIL@X6c-z!)viOOJXM%!g`Fh zLXJI>-4)wB(msLs4MhBnM!bOTIMJ<%jL$-gyyBeWcFJ636MS6pa1+rho8qSvYki{h zTOPr%LfjXDv6r2*FJWWq9z;r1>mO^l)j<`kTOIo34)^lZtq#XSNPGj10NMnUn-XwJ z7vJiT*H_h&RptBXm7xZ^)%!zNaw|jlPh!j*oKBFtjD^SWQXXbp;`xoDTA&BnzWgb2u%zS-W0DqP)8WCblNmA_dXr0R1HgTi|0l(l5t?z28J+`4k3-%9aMwGd!91 z@H!s0;6AiG=@0Q(+s5&C8O&-6%3ou>y0Snh{0#}x9r{n?Z@QZ6@+#*$F~~@fxq2gYkYb4C4d^l?pLKZ+WiYO%83iLm2E8g z1w=Q~j;@_whuuMWE*+^dA6O9SnRklet>nrqF;%2F1d)pG7O91YjyREH`yUfYO0%Z?Gm*TnH8MEk`0~KywjK zn>W%N%n{I0l4_cFid=r`JrqQ0%}6JYdJ(yiq{U56qE0|}eTycyD(_fsW5G_OXeKz5 z&`lJ2GX%M1k}OZ+T@a~S;zo2FbrM8k zeCohEYgD=L%VJ>Dj3##rp4!~fGq9<$op5*tHU;hkF|dtR5<~)EXE`i)tTUfXAB=%b z?kmE|){@#=u{pezSEhI>X6eQ%$^SFKENZWjU?~Zc5d1zhuc;7Bog2XgPq6XTbJGLr zn5pLP8zK@rw)bh_BbTOmE>EO@e7D%%q~<|7|l91Pu1J{B?o?)o*6+M6_Yj{UZiw*v0$~#6%Mum z?nKE!x!^CelsI;pC)aab!m46LNbx6l5FLT7{|OMY!2t*Uz??uN}x^b zlHB1Pd1@0INtT?D6+oNVa#J<0bcv0$DT+4VHf)Av{Cv9xsh@AB?g>=pRX!*4wDzsr zGb`W}hY0mOngQYr(oyui!iD&wfVrq2@)r#doC@tN47HsdEKBsHhVUC(!iL-E&eL{%o14KPOlW{`4H!--OX$`2>XbS$@xnjY3Z z1;--5eA@``YFqsUD1-_{i9}Hl?PgDr<#y6Q_f<($HHRwj3>vpJE|Mp=Wq3uJ96%TPnt;kjhkXl-nb7gqO(g~)>m5t`kfN;a2IpUd77 zo~SXU^d#{t>%I=Ieyu_502@a4B&=Gu2d`R87Is88#O9(9F62SnWKH5!R!b7A=bo*% z%KQyka+KBi-k_n*_rYn&&EC}(iM)<;072+DjSxS|@8A5`va0^_5oUG`Ll2x2FRvu? zgdp0Ngt7h}FQ+TSmeVT-;tL}G+z)?v65I~ILCMr?zKL5~gA-{qn}g&Gzmun$&B>~r z0mu&`0cbXI#ySg02=yth1#q>#X|@sIZ6j{K=j>fj|3`%JZk#;4gk!D2+de z1Yr8CyT=A>!sb!DD?woswvzPd82LNT8;86YBes&WUSPKrH3l2F-!qGFC$OsJ(BKP| zgUKm~^mNYvnaA8@solqgu9JC{-?yphIKnCPw|TzKirlm!HkR9Aod>5m>YmhK{#_Mc zTIKh|QrcxEkd9$2MXNkSS^Mflt2{+K#ZHRM1dvuKcdWCuj;T3!kFe%-?bdsRAks7Z z?tzqQk@VS1*_ICPRUD~f_f>P28sT$psw`DPpEK&%3gs-@vmg>c>X_WI&bGRoo80$7 zHnJC>WEfP>8_Vq^StNzCZ&X0nj6~8`z5S%(he@3!_jSFSI76phM18?xy8ufee31JE z_@63{j1R4KUZN7+uM$ZydZ1dO+0_!YR1O72Tp|>sNG0-CF#d3C!#a%g|G z)ckoifz|n4YkeIi3+gp8wXIkqleemo)w=G`^rXrA)e9Tsr%AY4EnDptr`4ZWszH)z zY-aH#5C~1p2$E^I-MHyJ;s4OfcO-ng#CbDY8^^KZi22ck`14w3))&oHpH?3aebI-PIobI2qn=;t`4$8 zBb-X~J^XND1d*OYL)*rIW*EhJyPVzca-8J99ZkyG^0zU5=$W5om)WOKS9qbp`F;}s zd~wwakoYQc6C)q6O!V|NSByB+yWYFq4`^hq6R#n#3inMR&qrJHv$p->(90*qV$54+ zfn`0I5@bG@X*|;38<5EynA!B?Oiuj9T17cfj`jS-9o0wqj&+1~M*OeJA{Me}ljH_r_oT2Yx%Bb(LDR&T6rae$&)U7MT|28|WV=FU`-R?(7a=k-+3s)*fw2C1NtAgC z^e^2@lbC9AS+307gQ$eQ!>6Epw4a`>gUlS@So0UN<0`wbx(Ud7(@LAD{X3DB5OBY^UV!n#hV*RTJ|iCSK4qfrSEZrVw%o* zL1c!L1+1ZlfVq3N0_p)KJ8(l$XRPN#HYOw=zCw}wy&Co@%6ypT0*ahHVSE?=(z@>6 zL9`5{+}F>u{5G*aQw_NpFC-2k0nl#bjCFpUvy^u4wo-xW$Dk^yKtVJ@h^?!5Z*%w) zWz%M`Q-L*yCB!-d_$V*o!vMoac$hl;sG=@bR4F}15$f>iM2ty79j=I^4;VTDqz=m+ z>s&t>Rg9(v_ZZ0f+7=$8Rms}i%aB_6#?QTK5HFS!9hhywOdEXd;Fpq(Vo_B#_H4Z* z!}nk_oVkGaX8EPb3@ivJ=U5n2Rt3dn!?OvW2 zYt2}be2Czoi?u~5DaW3bFbqhZH@5~u_W~&Ij2NkWik#4#c2%behJIHK|8!k=anB_F zXX?V=UJbuE4nKkt4;@Hw5DA6bg-qN>p>*gX3sm}AV0kaGcd+QP12`9wm<9BQDU+E-;eQ0byN;6jlZdRN|Cz{@ONbfY zJd4n`xy)YFYxxj3jdoc+iNaSd6-D0rSz*p65@To=7{oL^Rp{#~5(z;h#NjGqh>~|W zE>p!;oHc2K?1^~cTMve9EVq>0{92`ARW1_-A2En75~4kbUb$Ssk)C#eL5%4c3bwkB z2G`y^^z!n!B@Y!>W|y&4$jMD#>SA;G-r4LN&g6z5i8#I;0Un(UZ@SJs4w-)8rN9k+ zwIw&L{i-YJy1&oK9X^w%uKU93+nt4q>4zFV*UlWazQA^ z-kQZslDF>1n+ktY%t;@xK`*w3*HIBycvFqltH)|ev3hK+3Qg1Q2iHr76#8nf_Y!Nd zf2k3t?g~A(VC^+OH=lEV``MISJtag ziH{g6?>MN$TMU(V98@AEhRW-q?WW}$|E*o3<=34?NkQ7;(2fvp!&MP8+yxO(YUm`4I^&=gAJFpiBzhGSWWrTMScFVZ%QwiUjupg(w z39|cbULx#OeyK4|{jGXFW|FlI>38k|sJ$mTT-M}5 z6^Fj-^LA)BL;7O4=_V{TY|X3phI{ILj=k$9f^PJ-b7>0prCy}}HBjUB;Nu=T{qFUn zo~;KnI=B2u8;id{1iJI}IIJOJlf&BceY6_a$NZSFm0Iv;OuF0bF8ii#7GF+9zStpU zKRA6B866)uewra+g?;S8kX!+h?wvJbOW)y3?R3Xa1UjW`Z^(Ie`fZHW`Jg0R)Sc3)N02Ka)+1e~Xg?!!`yP%wD_LL0zCwC*WB}diG?RCZH=w&cK@>kmNVMxsO-p+B#zB|8 zD=q3h_j&M*W#AaO{TzomMq@^+WZ0O4N09I-9(?73Lymad(~9NWDBtF>2+#N+=4L)E zyLd_3?@}|hw7>8lYZe^~%FmK&bUE5qst@IDe;ae}V)jH-rXJ=hzaSbXoq2|*Dl)Vw zz3$D?zCTvW#q5ojD6S?$7Q@3mE{uxTiK}*x+O4oYzGLckykw@k`84OkD48+|&OK`X zS-QI#f$EQ{%Sa1OetK7Ue`a+D4l81ao9B~nqDFe@_w(=tna1t>m0Uq)Fw?lm`}IB4 z?}FqrwfutU4zi8Utt;Q_`ROPzmwsK0!(~=RclmjkRQalkkdMyOUI_|+p>n4J_`IXy zl*8**}=`fw&0Df+KNPA=R2v$0}-k9Ig@B#RYIasc9 zkrTq^eEE3NS8h{Q-If!Vm;FPaB;(zMx{K}!GqvOxiOynvMpL*Ns=XCt9OjJ`o6RIK zd;w^wiNwVw9}-i~_)%E6tzCmA4l}Y_IX&L+MRDRTQPcY$UvSChU|2!!C4lS%bBX`U z_;6mq=`J5oEy2vB1Upy>Jcc;&FNwS5H&_-_riO2^i$pG zZ$(mLRuS=-Y;_{tqn|s<;u`h%yM*6{YuY3IU0hDzBW3T&JyIC3$L+LwkCa++-91v) zT9SLDPKI9H#ilk^(W82g)TuzKee;MQ`Wiqj;=m`H!rA#y^~VL}Gl@F-b>P+btlykS zM6Kg2m%DMcA^e0q#Fzi%*q^KeTo^>Zg0(r%t@lQrN{SQVFE z(zx_q#Z{AE4-*{AuSfBk{K{su_{DJo+J)eD52O~!?3yAiX(ZC=Dm7iw`1?s`Wg0h1 zWKzM_4mfp?;NUX{pi0T43qkW8e)NN1;n0GWS3bH^a_Dl5Wd`mg*uUHNin_wzP|57j=d+OF&4hCk`! zS3s`%xazE;N41ZC0SI5PS6}dJmJ7-^5Hz|<`Ro2Bm64+*@tQWu!tba9y43IGG*8x=4Jv4fXAz55GqDudSv8ttqa$4rXB z@Lz$r(@^~ixsf;VFH-eny`1&XY|b?SZ^=)9=W}M?KXeAugzECT<-y%uLToAA>7{T@(1kc!1D&<`BkDZd&1o`%gPHxgyk>F(Rc3^m~(f6 z17+?dVM)r@klBhtaQK3>(Zh5-pEeskBL8Fv#2?z-%fhC6mCXaLZ3FjJGFquT#YeRj zeeZrHespX2DC*o+uzSGIU`0?Ix+S4IYn6Q**<8SB$+(t5ks~E>`w+P$-?@mj6-H{q zs`M>E5?8TM9ONu@djj$-V8g?h;J<|`{;40Lsu#!QT4k1R3(~qWnJ*A8Yxl3R&TN*> z9hFx^RIc(%9J`)`-Y-9=G``48$%DHpzaq%*73>JJAA`Z}n{dkbleO>VVqi?$S3GVp z=C~cwv7ehFcyGxM7&G%19=%vvdyu0LiWuZ`*Ka{>T84Jhs6y?+QUgaKM7Pkz^`%#h zSLP<)&K3;^SI59#3vEN?H;T#JmfIkN!u?$>(dyhF;7~YYY}R3CTWE0iSYh;j5|S`^ zPui>IyNjS)f1K7OY4jQ_Xf&;j1IGj~mOo4_nWbVJ<#k2z&~OznTNkymynx_-okTc) zlr!gA5Lf;r@6h`?GRrD&$g7VC6x=@`ohJGRLaeR6CJ)gmgJM~jc?jD$=ypHz)gl@9 zw7eS!J=HMgLsdWNm7%3K59xL0N7h@{t1h$dand8sh*ik#&Qf_sty@w_7Q_1KUBCWp z@0>*%2!fd&KZ&1G=A0BKbvB$sN#^)1$5u= z=WJKcOE}>X&oX(KgvIhRXLWvKmCo$oc2!DLC8C{zK0!fk6)td3TA6b`NDK?y&lRNh zyD&Rzu#uoFr=E2C;^CfVKXLU-#Uj)8R(ByDHtO(4K?CV*XZvhZ1r9&Q;tIW1QRDH& z;#9|%8JSIY^yABKxO{+E+3+{8AlqA5WI~L44G5%9rkZ3wtI2Abgvz18sop*TUHaps zJ%IxZ;4$Y}9ac#AW3Kw22Q7Lj?O|YR;J$huh0sH~4+-33FlA>G1qSG1V~Y@5Rk`>67%G5)`(rr%k~J+uWPL!$C22HcvrUU*W^+OKRYKzkOYvrx|AYb| zTg~3eXKkZN)~$w_w(!a&5ij5m91D?t$t)Z0kGDU_hW~;I+3*0p#q>4V{^pmAzM}{) zbhVV!yL0*Q-xk>sXglt?kQ+agPS+1S5T(V<`I!sjS^e}OiRj1j9Ov&AgAVrqXJ z-$&1e&nrH%S~xnw7xjiN6<5}O(v|f$VpJ>Z??Od~wtNH;ehW44xx5eHtp8;aPrzE6Y&(F_8o#1bKaCGB#C z*XJpgoGF(4985tZ04$M{Vu@14EU{CvVJ%m;%{nSVi-c>>^#fDSweF59!)OH<&*v1R zaU7rj-CX^axQd?QGg1#e-(r0300f`AaI_vS0?Q8RR+&i%yza)C60d~_UeA&<+{shC z{-k()t(95;cr7QzYo$t>pJomDE_Wm>CS&FA4#q!llGdi|J=`41SWxRN@_7*VQGi8` zK6w*YyDQU(=rGzn5%4RZY1@Xstgt3VXmnX3&>55V`Q4vnHrbC?&i|+wS=Zp<>Z|ud zBF7&XJ7)ueZyVs$`s#+fO^I1b4YM|qGu*>d%sNZV`mixe0GK5w#Vn<&W0pGt7Oe2r z3{3OKDu&k(Vy9`g{-%i6K@uagq`YT-rrIAxNp-zk-&Z<{c5CGPqE-^vEWTGaWIY`SRg0G0;P&sFwf`g?wm#& zS*bMna8LMA*_1d_r9}O8?UWodHA?KW9YJLI8VTEurs~AdTM5V4NezJ(LvfsgKKWaId3Qz&y)Swoz__PrcP(k1!;dwgs+;(eaH*a0C+`OlxAdrRVTZ6e z*}E-3=?55c7jhF7T z!&R;qNuDp}P}z+d^i`@pN9EWSVqP8@KsVv3nO=KxnkrjTmvP-=0DZ3NUaaSomg*h@ zXt`sZ-%gCptnQm!GsyD!;T@>tv99;7mkDgD<_<*`FgTo=V$M6)NsjEXCn0CgDUws= zhm`Yt!j0Y6nZ1msC&~Utw1z2hBivnu$J%>2!gRfXxiqJs*nFd2D)YtVNYAcg9BYdh zd;1f;mdLLQY-7Rs-a@gk@=y}RgKeSAW~r{<>=z+wfLyJy1(;}kauycIXoSPY4gP}B z%kaf5;ce?`mp#>@uTcmvy_-_IT&TUmd|k(PpCIoQ6vh*21#(BhApf3}>0olGR#j=8 zRkcBt-ZeH{?ogdlRh2lUg4kU_ekGkuhIC0Qrp9u-B;LT5D!jjEQ1dYF-)K>%{Jqmm zXR5Zj3xo#6#}_+keA}+GL>o?CqIZIJDvhtSSVXytRiYhzWu!_}+GIiyq&6^LTBt#) zN_Q}<7Rm+Qw7$~z|N8HL69Ko+I?J)q zPb&*B+fHZg^i*C~U^a%9WqsF1FYZ(=AeX8Wq7ytsCaWm0O6FxQRb`2Z_Ow_iczP_X#8XW=pilh-_+G0^{(QZpeE))X$?NJ^TjZ-wm*+ zv^_6GnKL-kdo~_i?U8IY_#~c^Y^Q)9wq%YE-eOQEG6>mf@UpB24W0wvOS&kZ;P(#( z-gEpQxB4g~8hncSDLqUuE!h}0rJ;HfhOL08DKYF6Wtt4t;@o^v($i+egiLLe_LMYL zcAzTas$-NsUsd-883d648l~l?Mro@$KT4yaYhXID5M+8P+Y3*mhB$B=Turz#j|h{i z3>u>+A?CwVB*sr4dfNu)D)*r+?@Ic&t$eKHmXCD*X3iNzBH0gx-VZBZqWZ_l?CxX= zYqfmfI^~rH{YlDmF*!_$x!u;QOXt(z#!)o0j-t(t!)-{G_GWB0rpY5q+v#|`Ql_!k zQ&gzzsA}>(6-(Qds_ylSA^~J+D|f7OZnB_c)y=x2ur{v#zKDsN*}Anajj5Ax0zL4>qq_`)Do;&}1}M8O{`j^gwQf zS@%G$0dSJS=wlyKX;A;42J#N;>7@hw7;Z(FbPQht7lPjA$;&XJNK=qFV&nfXejC@U zu`_<#!jUvAjqE8%yke9dzu&>wd~p5vt+6e&cGQ-+4@`I1_`MS##_s_f)>7U{czy$p z;PdMCHEB_#^Jt-W2CnAJ?dqA@x3Q_RlCyF$C}+6DQ!6JIsAGOmQ6d3YIgvBgxk=J5 zl?qgypt_MWfj!AWuH8UjC=C~)mu`K2GdJjDXWOcx(({fdZ(3jF(AsiD^60@9uE8S>h_yLy(&3n-CfS`5}qpSMYXaD zKw0Ia%BoaJSsTZf)leEP%DPdytSLx?>s?D(MMT!ksW0nYR@OZLQPy|hBxPkGYA;@= zR8}RYtb5BD-jk=w`l(u31)!{QQe{=Dq^$YzWi^zBi?VKgmZrK}<%>s)y-Mdl2g`$WxZHs^}9;~P*ypqnT%3VR$H*tOh!9u zpTb2;S_;VZo1|F#R-e}I)}HfWM*TW-u4AxavjV{2x?Q{!B-zEIcBH0Ynf~P zPwg*tWbT8<-!vW{1_&O%9|s;^NJ@CzCB-H^bK}w<4YWf z^HaKRS!)ZJi=)dJ4aSP(V!dRSs0OJ&^Q!%b)8W>q_@&k;#Len{M9ode@YkdVd-x-;M122*GZLb+mNVp3u<8j+8Te1W)y`&x%{Kt?~q*kI6~(F{MiS z*d59<)z-QXxwv=vA`G$Az_ z-NPt1RdGg3qgC8$u_3OSnehT&9cfE1ju-9nk=ndp0`!}Fe#6~}Fg|F}OdIY=&D9`l z+Dp^v}`6o*7I$D+^`#3+E1ORPeEcsf`kW`-&Ey+=t;@>uFME+itB`EGs^+NgOhRK z!F!>V{?2l#s_+v82GJ=z9fE3|hAct_3s%S(K8dGTaD`Yf!}`1cus}|V1xgjOplZnS zsPgY(xUjs}C2u62*Pi!hjr~*0TVuLRR_N=g4M=w_g|}Oj>w-B3S)yE~2v|+ojLZnJ z(vKgMQ;_PQoSuvB46=RrrR21jn5wr;?VEMqwR(#HQEwF-){LXKb{d$>IJ) zU?n>e-LP|n*i&lF!cJA5E@ya{r>gQwRpl+Vh9m%0DJNBxN|jV)tJpZS^MfqT**be2 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

    s;AJkg8K|N*5{}$fEBq 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

    B|sdAYT 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%0Djb+>KIS8@&eoidG$&IePhOk?xGu~H@t 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 Q1`@A76% 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

    3lHZI_U_X%Kdug3~2{S}&NFm+`Jkz&3% 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

    =Nx_XL(S~1Op96VWNmB8!SBcj$+v=oV z2r~btOy=66%p_`x%vJ6?%DC%_+$1VD*CgQgon_qhMQ##J`b81pTZ?daSDCwpqB{~L zl+I`SL`#-zSu2XAYHH|CL&9^aqO|Nli-jE>B}rsb|#Zdmzs-Fofdkgfl`rBByJoLS&Itym5DN# z4AW%zO`;cYTz!8T+YqvCENmot1+aNXbI2Ua{7bS!B>Rg5UU;C)eRC*BYd481>QT!5 zU>WyNa_i|<61{E;^-vkxnq=EVgd)*9hHMX)u?-{Jrou*|cdUpVmc41*LkG)fq)*f` zUZ6PYxjU#)McI6$%-`Yk_YRCsTf0fLyV#dC=08UVD(8L+kCr)UDLNrh&j=q!u^%hr zU5mV%iP$81$AgU*bV5p$b@sj7BeEH|ut(%WI(LE(U~rE}O7>1daJ-8n1Fqt!GpF!G zSx_Sxl&mp#lc>4F3gr}@EMsmhGLtBUm-pn@J?;x=fAX6{Wba8pGNqy~Ixb0A`%xhL z`jP3WvS8YZ!H}pjQ3Vh-c!lGT+{>`50Rg|*Z@?Pv0BZuO&UYL?$eBHFEP5f4E$uEn zF#iqS&f+!4H^b{jT*$ApK6#&d2~aihWcCo4ST|CNi=GGzNLwJ59aL4r0wn;gtr#6w zcWext_cO|Xu>A_4gDMyBuxr!_{hL%QaX1N&idgQ1R>fGgz5$(*qhZ*ShY8wYTx}6W zW5UQ-+=oK?#-Jw(PnRX*I!s2bN+inI(K~@9?wK-PugFWHcOuw|`By3Wz8;QW#)A=e zMS9O5My>vqh2i-6VP^!M92m_k?+m!db8aI1EiTAlKI5@=m5u&y{)8 zPKIs78xp-!z=lyxC|C2i;nu&9zZHYI52nd`7Jl=}g2%;F5%G?u$oPDj=c9`!I(9I~mv!ap2g*WtdfV29v#v?Z~#Vg`U3 zSA9Pfk?7bA3@H${2f?3-kL))MF-`s~pyXn>5LK6Ig$`#2pzG2#shV`t9Dhex5Xeq2 z#+;@4Ic!7RKhn;i*l`uPb8{+hSbLz%<{TRM;lQs*6<#Pyly#XXJHyA;ZW6uIm@zMw zv5g_yY+)nOI~{D^8RUJbjCU+~cM)C^y`^B&OvF|Y=oQYej*N#ogL&2iBP%E>#{QMo z7BExD+x#53PU{ZA$_%HqMipo$HPcabQ~q2q8J*x8GVo zQxorTJLpbs(f9-My3SQmNa^fNNtiYeDoi$o9;e#!J~kiegtil9RVju~890vP9=x!8 z=T}v}lO`#WsVMHRl*N5qC{k-Ti84#7ORC6)SIgMfFS3)U?5UchX$9B=)ZoExnJ;cA z7F7Y7EyNN-%^cT#X!)`LiMGp6u=Lc-wu0LK>s>)S3SW|;SjKI%;Cd{665 zr`^TbGnOWJw7DJk(25X6aH)X9azM;?W7$v^G#HjfEdLOvRX-;8m?`6UAJH&$)utX=qb8}ns z+}sKfQGanC8y%|??~f>39Pgy14l^j(wV7KkIuA1XKNPvK4_wwtC|g?I-K&=P4n_Ua zRV%zVFW!qQ6;r?=-s3n96kRdYTVRqz_wqv7{c+}C1zTKOE@N$1`Q(vNC)B_EuiS|) zr}9+jaw2zAkmT|FEv%gHGIh%CF}@ zU_3MSj(TCrr5QmM;|J}c460@M0wsnI-@svoJFIVx!FE;=f?p~8#9HICx)x(*q=NgU<42$Yvy;#~=m=OE?n z^8s-UcL@+umXJ5}nt8;~Y0@=K;hAE4yskq|u2CxsI~9VF9efnNeW~4b1k6tE@_}4( zwa<5842V@4@~FnT67*qG4F=ybfu-pDE3bpa}zR=A#`ARl1XUeZ&8n@Czwom5bgJa)W!j`udY_F);Da=s9(L z9)3Y}dnBXCZB2+`lq{VsM2J1N9V?E
    c_`r}R-TusZsmX}jgx~xrA?w!l!)Je^el0bV|L=0O4Vfeu}ks`qpV_` zDklX3;*BT+7BFtORpdLRLY8=vu}o^@CPV^ElSZcksP+;TrMqG1!ckxjki`c%}^;ug1$ z{|_sEykFD^q)z1esYZjNLlO^^0QpD}9mwKSS-f-S9k=IW62wBrPspQUpqh5G1%NW6 z%KyNT%uumg37`V!xX^KXrIvCMYP)i;mo~^N;#PwOiEZfFk;&rRieZ&^{evUdE12*D zZ=-eRo7I^8 zm@_Mt2FP0qNz~tu7pMjCFs%{Qy7u8?$H22Aae}fJGBB&?c(GC?33Rd~vK@rNYJFle z8!4YF4 zf2bDHs56c;>@fO6vX^3MksWLr{*~wzZC2xg%OiNazHW)g9`x~OWP zm#f_ZdSMbHz|b0AOqZPiK6`KRc*wZCV(C(;{NX9lz?vw@6OAe?>y2RcgG3{ODmJX5 zunP%U?|}ppWJ9suS*GBIC=z%n5*gHYgkBuZ@QbJ;jm8GJ=*R6Cv%+Yr?V$E2VHiY{ zIxFmdI;pDGfPNF|=;)HNtBwVxr4zZ*DrjnhQcIZj;gvdb{8Vb7!ClljyV!0NW-efl zy?uBWRo1kDbSY_IsBL#ZEwG}-45JJ1T_grVoTOV+O(Iq?D?VFerbJ#z6C_Yx&d5#3 zekVLruBmM>2qPJ>CB8(q#7;y)#LT3Hr(i^vNF0c>3Z0a&#kqDJb2`Y{I8iGML0Uu; zO1oZy3MOTp*auV79zvy@sN^4@dmP19E{9UTMLS;D%_g z$MJxTj`oPF6^ldkj86;X2Qfw>v?VOE4weUeY2^Ghr5ypfPS7pJD%G5hOk@LrP9XWIrR%;`Sui$3WG{n|5BaxV=hQ1hzM7>*K06 zPoN=|Qtm!JgQ_0(9x0WXHU|OB`fC$_ZES)esN*^*d_`=Gd!gdIVnoabxl96a?cfte z?i@@cwMI>*IcLarXQ{L)o(IwgHY?KvW;2w>66H_|2qU}@h~9k?_oyXI+DCx|p`6;n zxdhdENSrxbB?Z)Y#VxdsscnIb;=>Gj3(3cDu1a*hK?UD+;#AR96IgF#kq{zr#N#o!CF+1gv&?n3Iw!5j)2m# zI`RjF++dfoq0q>GI#ecxOEaqW($$_yQ9jP@jqR*Dx{+lx*2$GLcdIYn8kDq;Xmo9q zPYz6AntrUlIqnJ^PI#8mJhu0B7gq;`Wiz7>*AUU&q`omIVP{kMCjbQ_>_cF2z zWpKyJqk`O-y252Z%VyAth57ac#*f*JzTF(45K2>0ugGLCam`joHYb}9(K=W=YDx1# zO8#=>GFLbi+m{?XfsmLEtDvzY#D+x3K9p^%y-!=H8|D*`N-f3ey{)bUTS+vJFD6444l-$yTq4%O@ZuBdM1;GJV*;zk`rvZ{UV5s*U= zO%GWPv(h0(h>A9;n2*p<_NZktcgn}euBtSq=Tv3{0L_=l5GFu@4tC(;nFY?61#!mL zdL1l`KBb7D9GTfuN38%#Tu#mO=FMA<>2-rRNFlR-F$lMPWwLyU8xKZGO}Oh;A=FMvT!JR3B-s~ zrMOWA7y`b!(gx}j$Uo`0ec+0`B4M+0e1=_fCReW2-@%kp!;?m7>9`+Heb_kMcd&Ch zA__}5%R6&-Y32*`qlg_Z7^t{KCy>}yF&z;Sk}XSIMIWR%gtzx|=B$c_D@pSPiQ98S z&YdID{w+0Pua&W)mJg>RjY!D>m8+CE5=4)>otTj8y!re@!|_h6%iYU3uBvi04GisM zj_NVlCk1{NR9_P?@@YGfgMgVeF7h&Uqrjb#VC_1K#6k~l(FYTLU7F=A5P5R7i(Rui z+b_okYXefFXzZE`DI+B)y1oOxNZhVN%nfIjz>*!=JYg6u(nuiOG{~WrKC#u0DVw8Q z2>0~W-dP+$BX6uO`wa=|sl5Wm79KHkb!Qp3r^&_$J`wSE&wh-9p_$m>&8p%py)$fW zgPl!V6AWO6)hw@re#+R1qV1Fm6W9f+kE=@>f{5wh!?U$095^cg%GcPxZK93dIZ1T8 zo<>F#1xIrcqdDDIPffhO0izj24&^Y|r_3}*p=5V%@SXH)+qGl6aZ~jB9a?i-MoINL z7bGsHz@HPV)$~reJ~|{AwKvu1r~5iHgTrnFT6w&lJLa5#nZu;S@E{(0J}WmCxM#tB zJz`wINfltaFF8_ z*EKqxr)YPd`q6csm80p!rj;5sN=MlPF=?`Kr8j4MLQ#eS#nGyt$*6NA-)`1zA=hOzC+L$^72$Ptud>lZSf{kAIB8k}x!R^7 z)sEQ+Y$}B;U8;1kUD9Z-d-$YLh7z0R%yMc_<+9BSoSm61Y}`IdH&kaM5*Zai3bj?9 zB@r**BhSrIhY;*AAUC70SXLjm&#{qRv&4}>e4oy&|BuJo?G6^42og0c5j>$|4{FLOhZ{zJc8)mGRJ~u3}?^e`1YaL8AIw3942=}&%VjVz~I%T@Bqbk zBeb2wCQzI+kr(aDT@eZGn=3q8DXR+=`hk#MTpu>W!4c)b%<^*f1YCR)0qjjB)54j4 z@20Fw67FXQ2V(O$VbAs@^9ziingAqVJoMuBF`z3JSjEOY2UMg1=~kCG=wUo@SIQkz zi*%bNZBD(hW=`3tO3XnetYaSf`ztq1`paHc2f;lAlp$7*t~wc(i+^SU&c47!A#R^5 zM@-tXlxU~E)roF5Wivz%#jg8VT>{r&$!s`+Ob^LzH;IC{AyFmQA&C*8$Z!rSR~)+$U21%E23u;Lcz?{MIml~#!-m+ zTqQWKD8;5k!N`H$gt|&ksW6c&->gRs$*f0{B5qG+U5sl1DKPQoC{!z3Wv}D*3ZE}F z8jQnYjt3ySY!6cxOEm-0W-hto_8)C{Q5}wmQgxf^3B~2WPF9_0iX1zM_-bwKKxfPC z#0+9o-ZTOxY>>|Ays?PEyTPJ3rV5A zySIln=hX2y7mr~jeXo`UsBK;1(_|2b^Btvy)!cAXu-2Q^J~sTxN;)l*eVY#c;-m<3 zW!U!Np?J4s86DaMC0cGQj=I&#WbBQ`(uAzpGIhDtjcOS0NV^~sukzN$C?z5TCdTKm zuV@@~LgetZ8`se6PRv>}j^{kFXpXJZZj=fpPIg}lmx=Em;gC^*)@vq_?+pZhiMPnD zfwCqJ0!666M!CvzVYefl%dj(;>E~Tz3WZ&j-S9x_GX%%IbS~;ChQ~;4K)1*-FglvL z%-BVi!10_G`@wsnp`S)<0Rj^Q3q$z0-4dpii(8bF2^?=-lw&zc2&RMq`iAp(m)KIc zT|hEDEbP-A)$c8*@S$^u2a*`2CfDbB`?-6-1sc(P;A$8ITdt1C0@6L&_bu@vq(L8; zQ`^~LbeWb>1OYXq*ujVXK47IJN_=KHNX84DtJl%qyTv`@G|v)q)gszc)E{awV=M;n zyKx56N%6U)Pr5%y;{4)Yt?lVy}MA$7rIEP zjfVt_los00d#@CN7m1M#@7RY7M=jA;w;fqz^5(iQ3#AU|2|@I!%i08*#B0aT^Ts?e zJ0hbI$|GK?I#Y@EEjk>@F z{vlr)Fh&_<KQ6TC&JA*rUF}niqwL(mk2u9fQxv%goXPfe6XhT1iQ89# zs07+mvG}sa<*_EC8DdrOS@B9%)je1iov2QIH1^7zINOFW`m98&STz%9`F9Y!*!F3l zQd#@4XonA=96#rXij_W^dw$sy^RbHZj}WmMC(iDRWTR~diLG{*a&e}QLn!l211F59 z0-~I!FNV3@Nb9mJ4BXp4P$<;8EZe6pjwF_*kb=;tg%!12hc}C3 zOe=do-NWI3(St#4X)lCaCZ_C3o7|t~`i%NMO^%aG#gD3)+F|5VFdg@E*%QR=>m44V znSVBp-eG?#hK*TL^Spx-C8deHlV)?E9C=u+EP&_J=-85rt%rr)>oSg*afT5Bs}s1n zGiK0=9&$?*=GY4Z!<|2K+tY(!A3IJh#!7}n1ii+{>aY>It2>n;4L93QV^-g1bF{A^ z3Jw^!x!z=T2-~*}W@H!f5D=eL(4k=AOk)$KHI562TqhChr%`e77`atWC6vn7_t*Ei z*82Z`vBy^|{nM(8J>EXym#5ZUy!{#XzfpTe`wlOEcKJ#5Xa4N*jx#U2FZTJ7v)(=X z=}*o(=QF$Cc5dDE7k}~O{id9^`1iBU%b#+``BUz#yy$=CW$;|LUC+?_Th| zlkXV({?W^x`u_6sXWU)-*-zcQ<@?ir@Y$p1{NU-IxBuXp-cx_r(0Ra*uifKEBaeRi z;Cp{G=g@n%&L4a4kAI!JclqT%_m5=@M*55w<=PAFNvUJg_-A~>8YRmfiKW_WRHm^VUdh+$s=PrN!cR&8! z8|OZ>>z}STZtzdvI-upxkK~^G>qQ&i`fK?vC%nCJ(sl1ldiCCS-r4HH_g-xK?R$IP ze$@Ll_x}AKTYl!ze_r|9>nyAH%l7;ON9^zaVfj{9p8m+z-~Hu5AN#}Ks<$aW@Q!VU zkNd*55B~bV?Y{HZ`0UEdcA9nd&O3i+m;HA>`A@Yw|9q>{ zcHVp5_PezlvEA+?Ge5ce$T_J!ZoDA3$Jz-$+w&KLZ|}YFrM7*3-~ax;e~DkU-|*{~ z958v|OR)#%)g1Kv;Fn8k4?1|#WwTl*H4Qy6`B$YKQ_`<4p87^xa_ZGvo;Y=@NB(Ep z-s#t-t@&j0AvGIcKV;5X*7WbSPCRt~dsmhI>(v)a*EJqgcHhaZWq+wXC%$Clm>E}Z zzpQ+h2fk9V&xF@14>{oC>i4(WXZDiR>e<`eb4<;p=MK&J&f#B}^GfX}=6@@H>iiWC z>`?d8qp^mj(m4%te}8r34KprXeCu~_YTl*vFU=G8J#Sh4kAA!?^_dr1j=pqi>(}>Q z)cT9p?{0m&`PJ5jna3VA?e{-Ds^#tN+cF3JZ(G$jlI>^QJ9WiYUEM1mJM!J5yPn^A z)#=If>boA^Z_U&HTCwh;<+F}&y1gxN(O=(4JaGRx9S7cgM@M69W#?_Tem@y|@(0N~ z-fm32bm>m%>NmcWzU%3$(%UQ?I^o9EUpV33Lk2Tn?Kvm&O3TOlK6Br*xo4~2%st+} zL;kWyI`TW-e8xay+YN(< z$Sd(D;Cznpe$H5{z-21%Guv9$oAmuH_+;t(*ufNLB*So${;%-&MR2)jhGi|GT?@}H zr_D3qdFw7@)&{qSwj@h6>0+-UvD+=cJCHH1B2)Qx;BxTZ%$0U0SeEq(()+ID`A*wW zkdj20chYBw*OS5J6C~u_ejf^4GLNPmEGy5P-l-rd8*r$?Zv6pRZ!nkksmyyP+A*g) z8Rtc4^bpSv0>?W^)%E1Amh~|0enp!n={kim?}JVcg8y64WMAOj_;LC%m#uBanZSHx z-n&uQ6I^S+>!0B8ET2D1+m~p26m;7MS`CBOj%C1xmUXmm;qRNwp#j=$4^1DPMv+VK z90o@7j?9Di`-0D5%<(;NxSG$;p#4eE>!0BBGW7a&oQy8~JC`|}N&9^m@9&K9G3I$J zZ4TkzVbJe6l9^n?oLZpKOy+sfWY!COE&%QUHP91$?qkev^4Yb_`wn2tf~Et|;zh=N z4H&CfvmY?;|E2FyJl})9zll*S6FOhW96!(6{*`g}0nXRy`wq0*3fd&#foB-^30Z6A z*$IvtfcHt(@=L(G2ArZE1?%$qM9cambJzn|=P~}xG9TbDeXEuB`+?sJ(D6X#_b#7Lf;UMq0XP?c?_SKM6&PPJ?Bcb1g{JVg83`6gmncG1N$p@y>>WeTS5>l8hr+tIzr|U8|zHN>U)5jxxGq8&iuH?IIl~@b-4V%9b9{4b^ zBuSLO)0G!Y%H8O>UB=)`H-B(XTZDyv2h`pX{|>;QN_ZN}Fn2uQqNn!{y6+QIss3!0 z>(R@qOsv*HBIo`TstvG*zb4z~X}&NBO`411)Rn4aB_!RIu7PK&Rf-S_GwZZbFO3Ew znWLnOWUQ53$z!YWsTh+5K}>?F!NO%xWSImH&k_s+Vp0%9u!ez5MpA?wgh34vDcEo& zaJ_nGuhV?LAY4hfB^i4=ftIpL#b}OhFuLN!nKV?Tb-mRQNbPDksUs#6YJ}O7OiPM1 zNn*liKG+LmT^bxyrsrjZZ|9w)w`vAZlD}=s+dLN_&YjGiH6t=)>1({2`ZwS55qm572R~K z#rXtX%g5>(UYL966d$Woa8WFFCneEcq}rvpyW;I^N3#Nx*3Bf#Z;FK*u=b?s+`aI+_(&nswB`^6zhS?YoTi1YJtSa*i&SSAa9=Fe|!4aD>sjwj0@imXFmnO#P$T zU8*u;qY?DR3$oi4bXg$d2%WWK5o7?FSum$-I1A%bfT$b`#ON9BNSI<-v2HTEP8f4g z+1Q}p5KZqAcIHJ3?TyYa8V%^|u>iGzmDG=x-`WX`2DBCXcD0O!$SYP!93pMGM6y%X z$#kkIMj6qsQrKP>UzYWf^^;7Q1JY>mijUPP$OMnlrNH{>l{1CZ4A^LNUjxisv)vzEs!(>Adb=FmeOgS- zR!0j)caQEgOea>dP^U0)_9uwQN)~Efe0~yvoL0DVkfyKDSP`S!DYGrSr#-viR!umy zOFikuk{GMKumr2^xBNQux7nJQaGV60D}&8Ee^Rz>-45UPkO1G)y*m+u-6Pn|xD-P? znjU;4`1YwX86rKVuz@a9c^%DT!RCIJ5sNx(Jw+q87FAF`cp?y9s;R`3LsgK1UF#s1 zsPFK0FSt8-9xz2~aCtQrqB%^89l6Ns=xq=Wc^6)nUOH6kphg_+T$~G#qV>;$KpIsbT2?r04Rr$h-vDk|F`R;0TI_SM zhOlHx_ZNc5TT`YH=^qQEX9R;Sr2>Lq4CJXqUp@`cqCC=!`Yu-J?E?TT2#yt8ZV@3_ zJ8wrHq-5SY{E#u0UO==V!+mUL`jpQ(4EqZMJV@7~Ronb?F6MU4n9h-1v1#}L85Lv# zuX!t2OfhKluL5*Y3I&H17eTveQbR@iSAPdY(b{dASH$bq=NYn_j+V3iIZG^x&Dh@F zsbhl}WVz)`WkUfhZU9744d*Y27o?9?^4eh7rDI6GA50;N91oDW!7+jyEV3gWa5O>V zWn)N?r{|*qd>R0%q?{Kn436&s2-?|S)3K=D^P8hW#{*J>XdFO9+(g$$Phma>jiu35 z;G#R^=s=OgLD)20YfiY|3weu)5>^t%7Dd!u0E24*2wO)16zwq{J~)>nScEJJL^}~7 zIp%EOX|N(k^z?oWDC!SJgUa%Gv7@u&%9T!w?mUd$5tmdd<&FhND*V9MQGg`)(F;)p zM4{kjg6>%4sO~G&3Q1Mc==r}uR~(1pUBg~aT_xW8Se=3rP{~1erPxl-!wiZ}GPI&|$?Q7mF;U!Y@ou`x|Y*`%3cg7sVmDB7vj zs>Dw)tEp|&*XiUGNHlJDkSIZ27iDkW14O(Svhu8x_0{JxL%chn8kQfcOQ8_N*Xdjo z<*|*D+)J5PZ2Yl!HUog7OyL0tbM8&E$6yMtYmk2*qGM71m5$~d+2g5wxd%g0IK88k zQ?KCqMl=11bcz@5q&NRUK)qF!aVzIQ)52ZG)cw#JfP{%xH4lLwKMp|>-cRRa+EJ0+*z_Rx)jY2nXrzcm_Uq)m_!e7*x}qr`fvl7jZHM* zGZ${=;>p@~a5GYD#sD>vUB^}29LK~11tuzpBvj(IfG>wSDQFMZ~Sf*7@^TO&D+qDAFN z_$wO3PDgU-C3;dp#d`w{3n<>vGv8dHxEJ79x2TmF4P3|?J(>ASm3@r@9_*fpbt8L~$+tD&66P!rjMba^D9GTDBM%iIIw3 z?Bri2RP!);YD7Ck0Tc%6e-;g5+pgrB zOXCL;jo)~E)T0?}@%RTTF3R@+Y%>e6I!^FiG7L&mE+-zBCdoPWdEmi`d0x_q<;Gn zRw0^ie!LnLSa@t5$I3BhFOKy$x-2c+C5p9*+lkMwA@)%ipxpMBBa7$>yf#*6LA|R$ z=bp7i*V}LFqes6V07d9tm|-U#S9HzCbJmm`4+s(P!Y56a17emQOX@xkhk{4iKt<1d z`*j3d3U~eYEkuF=$_oz=jn$`*N1+t%WUR4xEfQ`oe6agX096$RRCrVQne#~HAa(L4bdr|A%~7V3 ze(2N+zB;mA&MTFV(d3;}>1lLoQ}UXYN-{ww`ISyn>=esUS*#1sTr%9p?YnK0#{qmY zqq0x{+R6A}h8Pk;IOD{qnY|fP_)p;9u9kr$H%XFVWmyy{@GWT^2PIEi=iD_NiQJkU zw%uXdsCK5=8r7=bNn*0bo;%NZEqP)VxX|WxsYIN0Bw|UnIvIu3I16G<2Jie@`;fL^ zLR%$9+?Y+xvT&1f5iUJ~5ysT)irMp-Y8_ zI+K(pt+T!9>aKBn;fi3n1D8ahxfI5lRJl%KLzc`Cc-+1F7dzGegv6>lm2sTn)dIwu zi^_WABZMU9B0(DkwpBtLx3G=mFcXrcN&Tjo#1ABH-#=0FG{vS^`!a!;ce0`9Vr2ma zjWuP-ZK|>!#n&_s*BdSzNX7fRHhAI@1cgk6z{rYZ$vLY*AzhZqC1SzXo9 zRW-A!A~n0Z`rjDQom)G_#8XQpOC5<%WAY-?OvMe9I+{S9Nx|$`^DxOE5rw+p*z%si z-VQ2r_YLTev~+A1$|_->syyRojQ=d18chw8%^cEgKA-*ZHiRWbRU? zfsD)W}m`XZdr&tY;eVIM8-oPDFhMYP$ak~MqPPHhdtI^V=$ufFG5?5tXWJ&`i zH}#;C%?jm@UmzKk;&!V`dC$@DcX7Ohp#+mBkrtTJRgwY9{EP*XB;+o{ui01t&t#B{ z&&@zcW+x}77&zq;PTIv_GC3iFyC#xrOxYmRoh}r}LXYp^(qCFB&ZKp3Ojumo_v7+>*&i@L$j=^*$xl)&WEG03bWb4dJ zUUV|3xs}0G1w*H;EFU9{a<<&;Smb`iGn)|f%=}rpLX(gs1U2EW;0%`fNsWj|I(%wZ z)2^yCR#C}6`CA>YP`_uY-};kk^?H{2JzM>*QNQQ#dsdvBd<~@i&r-J*g80fXl3ARL zW8SRA7l}gL&9te~CQ^(fnEmW9G4Fj8WCIx0f{-AUbv!VVfY#+weXD9J5JA6HAyFdT zrVw6wp<8lkS1g?mM>gR~h_K2~$T|y6wLFtVIN+I_YZrv=h`K5tH5}_F*=0BGqNo{G zXMlu&QvJc7n>JcPX`{2K%X%rXGI<#TxdK&!LFQV+_J#_=8<4(4e|pAH@t&*8EWxdrpmP!U_-IFECD97K}P_1@N-)v0p z&etf4n@^QtlPU!u#Y}UFk(*9(UJt5D&unL@f~KmfE8iP!N|h#+trBrT!jzX5x2aC0 zs-24sG`r)Vcd$3|h+d?&g=FPt3+U8G$8osJZW&z?Y8p~ms@anr{S4-*s?|$&Ta#-Y zjGeNkK2>xG_)BpW@bud=cQmP^rI$OPrGn)X!>0t@YyY-4^ z(WypT$Y=~ojcD?9QqPJFN)GaYq)*#N#|{i~pze35nzVGN`f@qEYiIuwtB9BHux*SV z!|G8jVr3>Jt6gdK?y)7w1u^%HLMz*cLn*%8k*VRezCC+WGxd`t=`q~TU!6_VlvdR0 z7hD~Y(>x-KqiG{)w5K($SXz6Cy`o)hN>nB+($p*5n;QJH(#91m{NvWNcklis~KIC6vklOy5h)?)aBrpOzqo6iC4dZf};_ z@3@iyTSrF?OJ}G?rV2dQC9JX((njNHml&W`1}7m|6k;&Da%iAKb0W8%0X8oN%ddj)?9 znG#5KBBve)s%LBD(HbM}v0&KLt|_o8g%-q%nO3*JWo)XjN>T+NsV$SI(uPHIIW>x6 z9jTtg5PPt=ef|WU2VDjI4FSDEQi#9jTzggxkD7R9ME!YFazxW#?LF9774p^b6x3!) zQ`H~hHLkm>q_v%pF`<9qma;^i!x_c~RD|_6rYC{U~y+(qMTcQ6_Ci`omdT9P9Zd?_@ElU?*dyF z#S_B=eaZTdibS5AdU}R5;`uqDHD`;_%c4`#M%I_y>JreA?I#Mr7-SzHkAGF=H5tY* zlIQ~?e2%S++e!4Us(?Z+c;$VtvYk>+l{xS-FAka)hBgBlknU2meRQVb&du{~*O}6@a_kqD4i7WJ43T6HZe@Rc+i^f!>7A zo*SgYYv@<94qGAo;&#Q#lp;py+S45Lmn1hz5s2m4R_C;;UT-HoWjbYP0ViB7mO&zJ z4;U}Y^C1=8O|dWE@)rsAICUF(9ghuOFwSPwBF61^Z8K~jCg>7YFNSLZ2c!xm^$|J2 z9ly#}67TsAo)N&Xcv`#Y?o$n?^)|~)C`L=Kt{x|{OvMN(L2qnDP&=!Rp{;P-K3r=L zHlU6T)BNI!3@n@kfUg^~ScwjgA#u z2S%=!Wzc~k@1#=ul2TATZdbbDT{U@+r{(il83;*j;#e8G0Ef%+_Fk*=DUnWMtGQn7 z6v~kcho}ppn8de(2)Nq{Z+4)v5m8zvZnxA)sdzOA!k_{0bNACPzxz7ALAZF zeqdDRP`!EGit;Gt#6@`7rHez6qBQptnpv!#=Sl8Hf=xm#2ic9Hsz=jfBm6@VQjSJq z{b+PFoNmuvUlRZ*YA$Q-rMDrv>xw~AV!h$dq+&)37+IBFC%g7Q+FG0KYsWp=k{R z3ycHj)Ha6Bu0y39gj825r^<2>=FJSSI5;F!FT)>`;;bDB<-M||;;8g>XGX4vCCg2C zD4=$jwdNeNr5At8Ie;S^73;^NfIEm|pqW(EzbnmoiCaz64JZ+1FzQa+D0mRRLJpYd zuj3{t4Y3kQoqIB})9{4bebAYg zkWaw}v9dZj?IJim+}n{wmz1@-rM#%c=Nwvy7N6giLiyxV5ultJK4mNQnOUqdPLR)T zS~_Zr%yOvX*AVmij5y2Wa5YnvDN*&T0f$_94dM{ffC&xbOsSZaDkse{N%>O(v&QFj zKau9fh&1?3pl=|S#qgx#nkx%L3upah;an)q^^}+t2h%KxwiT>r*C0zL)zHMt4bB37 z@qH%XoH^KM1>9*pL2p<}6zNJmzVS@qTv5>R*h*pbyzo4>z{iUQhGGN-Lr!`)QF6FhX%$tDe6ulP9Ys4F32g|^roMh zc#W)~K6IkBTlxyutX|!yFal-x(XgBlS@go^Y>&2T8S;v@!5D%pn%`0wAewK71CNL0 zNJfq-^y-2v8zL(CiGV1c=KuuNgmE@y0!w4@{1AcFPD4j{C&HxZg z$(H1`71P&7i7Y*22aJcZK2bVV))y8hg4T^~cP|s;MH)P)OdEBK?8rq>+bgCLrW27d zk4AB&w)tS~ad=?nAQZR@8n@d5lL;Bo0NWW0DT0UAN6QG|{G6svODT1?#>W9LqO*-E zXvO@dO=e^UQW|Qs?%!{o;~Q+0rV;}2-vFC$+$08ewW-+^Du^N)S`m1b)e#WtGdpE! zRd_Fo=lsYTZ(_z0LTEX>GEWSIGOU5Q3&x6YBddR~Lr%lu_G_?E;0jmhE-0PZLF0uA z=d)K4;ELEHDbDpWb&fx+{Sa*+xb?z;A8o{ARC458V2Q_;D+UB)=OBEZ5EEN!?k^}` z(v+;^4f_h`W*IV1w3U!UHZLb`$}mH@#31}E)fYAGMDV(m^UA`RYMdx%<+fnm<8~_> z8C@LPPo1DN(g7(l^8^Qw_0QDbI&Nhq)rIj#!XqM2_geoaj!24diW%SX!QsqQcppS$ZbFpd4 ztZQiAu|4x}3afH}kN?1Xqon<(_-K#GdGWvYWxFYKKD{wheNx$JTzlH=a-}K}58@7&;s1sEJtIjtDs>;h}CTG{oD(mc= zIlGK^Rb?Gr$*!`_s;-%-npD@Ej+qJHPDYMnH?MQ7j3Y}G%;1tw=OCx;y?OQBff1ANFb!UBO=1cMPGtJv-y= zuY)0eMeiY1l@z&1$;}{odts@7H=P*l6Dcy{8#Z$tnJkZSPd_iJQyoFbYQ4JSfGaR(8A{!8 zCA52|u4ltcJ_{W=yxry#B3v5=vCTsI zu2>kVjIjq&3+lCn<|~eFMYmF5x}GcV&IJ*)x!RKrix(a!`Rkf?;wAs@97!GL(-y2s zaI=N3a!;w$SD?%lWsI)jWFNPy*^905I@v#%fH6{xXS``;jocPuLa{L7Fw1Q*12q`T z*PA93lbqwpT~5~6kDIu2e8K~d_S&)Zh5y|l6=&mYNDHJj&DM&U7stR?nVyk?>>FOg zl)G_A3TV~b;3XEV*+lJPItqj=1whhP-4;^UgzWvarWL}#L0su-caHGm`Ana^Z1p^u zmxPKI5TG?`%3LBcQl_4T$92Y8;J?Flto!{N=&P>?X%p!8>X<{6$g&E=aK*V2|D672 zF%^WkeTU7rr&N%`Iq-oP?#YxFV5B3*5ZwG1S^-I_xwE_SzP7-%?x;=WsM934PDg_M zXOG@CV>6Di>}e_%g7;|liGrKiSAv^+Qf&{6;ruu!L$OxAr6%GpzK{x>a?PUOi6ogX z)G2w=9aJ0m+mF7BOx#|)%)46v)rDdrCXUyYOlccKEUKtiQCS|hry6}AeMO?8Pp#ti z@tQB4vnR~BXGy)!ViKL?$SOS`5s!fcf!1VnyVhLF33Xz56YEp4>LLeQH`|LalrW2uN)*^(Ah#uk(MkMJ<2$MuPAp0l@k{fLa~ubw*hxrw0m_GeP+m_zYhS1OgXcbzA%w%Hii)+g&tN9LwyqH;2#kN9TRaaEP73h8QEk3Ld>Vn^Xu?sao zQM0is__aoLP#51}+eX|QZY`+qmWmSdhTv->P+G~pXC`uf<>%>vxv_fFaUj+1er1kt z%!NN+N)%d5K-|7?LcOwTg}?3zUzy7@SwR(>sx*%TA6(qj7EN=BYs}OIMr|`Aq5G7w z$4dH)Ux}%QSHRw4zT`|-o!EuiiCyGwh}lWWVU#i@r8vt(vOEQc8dZ^Qvrp&XsBZ}y zS2kV)=^^&mi`w+pi9NRy#_%nR(Ucg{j5w(5uxX%9nzVCiPBR{uu*}>gFS5qR#H-8a zxG=d@Q<$ZSDr7Ef5sc~^Sb^@6-S(J6It(mBxTnEY`heW1Nc+D@#vq}_PsLZZ@N;Bd1)#%v&9M+* z7~sS)Rherje1u@~OS#zYe$gI%qQ$mXNM@Wx$A!}_2*x#;c2A7?wi%r{H-?Ge9Re5b zTk#v=J9B^0{i3S)(J}|~eK(O$?|y?1Ik~E?K)B}uh>WI*^cU?Vcg<)5DYTba%3e(G zvsJ81MaTs&ee+$R!W8*H!}&1UT@G&iA~Q;VtFJ;A)$6mHXsHK`)hDV;@_a=_WcjppGJm)DmIxm|xgN$@OT( zUM$##w}Af;x4&esBvi`xkwahA#n&)~r=;Gq*uI&R__Vyz73gy%H9d~267Ugg9Zc zfG;kD^0|nRyGLNMF)*U>QGfaPmv*~cBEr+s${vr*(H?TOmM{TkT?wS$S}|dlabf%5 zm75^ybXM1`3FUOA`sL_OLG$PbJ%Zr4e8Z_JQ3~-!^F$s)nQO-Nt!n#ON9VI2jE~XKDac*#)b~KyVD3lutePQ0$SdNF}8>q6MQD0+IU(r(LKZ*?YrJ`~4okgt^ z@d(thKUXRsrm_}}7mXvpNwx`j)@BS*14@ z64}>Ixh%OFG;Z%1ZTI8$iJqZzt(l@7H+ysa6`DqK*uJwnOe{8op2SGTkoItCqZ7RcqQfnp|n)!mbIwvyWRylka zNX*5z#R~U2`My|Y90EB7&AAregizI|33Z8F1&}@4omiiU_d3^P%alPcOR-MFC-!iq zRg1q#5;0065=by}D3mHE5TF$|$+4jMm<|OpckbbG%?O?0e)Sh6!?Cxyp~pMrXvJf- z%0WRpy581YzC@z$YGg^lrQFe=s2+kr;HXKyOS+41W2jHe<92<$7ltvOvAcVf5P+-= zmlah&K-?hR(Yw_kdx|ejAgZe`y@{5?Fb-rphj@@holR*y&(Yon_ml-R zLN{BaG34M@4`(2qX^A%oU0qO~8E0Fm?lHdvfZh=&O!8 z+^j9ZJ3P**r+m9e;^v}*G=4?S$i47{mkTBcRhZmB#`2y(oHN7My}7NO?&K?@sqXAR zIxXEgSax~fACKF!=+adOrM{m_eANRo2_?z-JxM2rJWSybmDm_l5=g(>H2A>hjS)U?!J*7QMMik|&anxppaoXXF>1#`IyUqzo1n%BToZ1U; z_zs!m^RfhwYRSF;9f#w|Ovc`MbvhYWPMSlFwf6L-p@S7+YgeC%Zga0C%EenHdna+8 z$#;aZC$8WunfoZIWLq{npaR!9Ia3jaademOYL zg2n|i$Gvng7r{i`I3N1~92hzZk+EFAKs5Q0t=_S!j54lla0Xi?H$HTGLM|zh1#_Ch z7p5fOrveQo=HVRAgmD^AB<3JjD-@*NDlO~~6mI0YE+&mHcsO@a)U5y`6NZXvS|BV8 z37vfUEVL|ZMlFl}wwbc5kqXhzXL_{?L$4V*M|K~I7ZmexC@csdIsQ(1M<=YMSAZ`Y z{$Y#gm66`c8hb+F@M@U~o8`!NDM>1oDpoQ^3{IcFji-mgMlR?nPWU)5MXbB;i_Ul7M0|?r{J!^p(CHZtlc|%@Zt$ph)&!bz=k*ASuy2lNOyhL)l6HX{h=#uCaokC4$ab zskrv#OZ+LLw1;SBYRrvUGD1Sw)-SocFPHF`Sf3}(jGS}}9B}F5t%3;`d?5Uet?-fS zM0@a*0hA?@+tZ{7aIbO2?KNSaSsS6zU)xnD>&$bS|Ej#X)aXETZh$#nHlgv8^?f53 zTjzR>D0o`4Ld$2TKlZ0UZyZTX&SC%1IvTVQH-*b75`cY1OAxLUmiurLku z{!iFkc+ow~Rzl>FP3<|kc&sm8dvXMAokTXqBO#BTYvTgDP6rdmU3Kmcg^0j+KzyI* zFCOTiiAfVy;4vyb(WSnQShztd(`C;p_AP@Xw`9fLQGz`{QW7@%&Ly$FWo+U2+9qr5 zBPtXtTapZsN=NDBHdw4pa&F1~U0NxLWs$juFIPi-U%o>hXs^$#7Fn_y-3tzCCB4hH zqB8Y%M@1@}YyRp2hwPZe94o{bBFK4h`$}$M=yW3bYXQu}=XSA_KDVVaD_n)-2wO_z zkC&oVwVQ;GrTrx6p^Xt@WNuQjZRVTQbP2gsPD6odfp=5~*I^q&~-+eJVEjSU6mBB%sbw+X@anh8aS?4 z++Gq|9X2Q`TH%sjd{%dz95&`N&kJ*eA#VR-3%zSVoYP&LwTfX*@YQ8-HdZ@ir3<(k zCI7CkaaeUz&Fre$+NP@6b@g?XvubK9=FF^~J#$u7iXLHnpxE~<&}+miNv-p7V2}gOR+JC-U32wmj9c zwz78Q-GH?-zin$?QZVv#9mAm#j&? zyw_EYFZ^x&^h0-9bkj)c(6>iEHgWCs*0dVE2B2mwkk>JJHTtLAZMTkiZ$l>2O#URR zH{Z!E@svXb%lcrl6O2+p>yn_g9Em9uw`8F^z#d57<7TQ)Tugy>E zmQ}o5X|-GH_}yr=@oI_HLM_O3{9R@>@mhZF-tw(~i74!d^bhb}{>smu>Nmo}U#&Ns z5BSr-KO3kCY8{y}Vp5Z>i<(gKGfDN5x@ieM&7dMD_ZvJ< z@x*-I3BJ9&@1tE7JY&#h&{{+rp@d~EMS+@e`c=DOx0a?nZ&oGKa;k5ZF$aJkSmjg^ zJej>W>tn3FfMuxFz!;+|w&^i956fBBc0RbeLX@e_CGcihajOE##i5+Ms^Am(S#0e_ zKaXO4V4LyH&Z9(}`3SaIRokjly^ce%4F8E@tpUzbKJ8WzWd8k>{E}7erZ}Wf@_Ohp zepC&SWB?xYez0ty;CVR>h@t zeXX`tu~lCMTkC>#tNT*5b!l<0uU4!6zu)JabC=1TNl^Ri`}u!f6f&86&U4Or&aO^aP`K4v8(-WREh>4$wHrwy}o9(S%Z@Y2y zI=gn`=%#jLd_gD!X$U%T!qe4-11el%N--MJ6{%X-7(s5KxHDqJu(29`eKmBQy@hp0 zt=(>h{IfM=^mg`kmNm*&P6!@6G83O8_%H`QtvTv5j00nKn4jXDXro~ZYtE_SxzKr` zbt6CBANnJCM*U-^U=Ek+tM2Zjx-k&)@2z(j@E-m3_3v(7&-AEfZYEywhjSYO zzwq$swlx;>Fg@!dmUa3_+j@GMZJmfV1N~W=!CpQ1?%nKv9{zST_Ti|q)A9T-^Qi$T zj^e4pi1-m5iRQux9CO~)m_=6={}tkmIc9u|&+3Y25dJ~>w0cR}fnoqnJjR{XJauZc ztfg!Uf*Pgq;__+x^&bo`Dd3uv851Quh~o$KD(Wk$_h0+&^He{_p-$gp7-O+_c?#q zU+%l$@mqd%;cf4Y_|a!yjJ){E;orM><$^0NzWUZxKdwHk{*vg0ML+%1N4H#h#J7naO>!M-n;eW&eGf7|FHV@%U3k6n>2Uny0w>I zx^Bs1&#XJ*wT*Y)_|6x1ERs~^~X!w za_a-B&$oE!^$%Wq=$upEdg$f1XFQykcizM2FaPA>$3EKjj|cUh^v637T>98Wzr6Xe z+OzI??E9H8Y`ymR zcR&2a^KV|=_tL|~2mkq{^7mgUyQ25C<(szp%a&KJ{OhEX&v>J<_{=w7=v?#Gf+zp^ zw>=KJ>hD*byVs^a4Si|Tj%|m$J@UEB-@bRwCvTtMV!iv1pOw73->r+@dwy*1d-W?{ ze*e->cmH6gd7pmp+xeIMW8;HA`}B(YpZ;vSYaac)_uWlj{GsoJFYg_5|CcB4)jjmo zeOC>A>h#x!o_6kEwi>p_kG5WS`VreMd-?2P*WY;Yupb{5-|od7zua!@it~mayz868 zSA9}CV!!##BQ||-{0{G2yJ^SAc52+?_rHH}&*_PC_xiY~d!MWBxNYB__}%+X{cKib zT>YBJWv`wy`i198#tgaQzs6WQU$x(jy$6ilcxLPVlV(>HtT|x%_~Adh^nmX85eM!! zeutv}se5MPbzA?gIP&&$rT_ZxgUa^#`1rD>_FtCmxVmWS+mD?z{gal0856IM&$zj3 z%FHz_3ujKfcjTP=uiw6+?#a6j9)09r=l$W&Cm*uUm#@~I)_23gzi9DVB%XYYLT(rIg+`1WPL zJ0dn_!uR8+Ox?O|t7p2~HqAV_J@UipiHk3at!#ex{FR6IJ-zb5xx>49CvNKgNBf*( zUix)&YU?d;O|`yxO3x4CncnX|5nZ*_C(%{b1;?*iamD<;?l(5|&Hvl3YYw*0JZ{B? zYmZB>nSXo_f-;3;z&jDkdLDmM_VI-jI^xZ0nSZ3 zBhG~IPaA%Y28iHv;nk zjKARfC*U0Kzc<#6wx?j-ACJe_kjKxX-CWH5Im~4a*27@W{uujXz^Fstw-0~}#ro6e ze*)TU!g#;HTJFJG`R5AY?_xwz@comR=iR{FYgo&+1(2aw%Was)D13Jb47L)q zc?RQd2YAyb;Crm;b+j3YIlPMT%P{^(wEGRlehRS9#2BT(_Z67$`RKa_YZwODtML1? z!0*rT>^!XfY5YAHxEeDGYsa_`;`wuclNe@Mdt&`J;O~+`z{T?>%>DNm?y(Rio;BO7)_AUH=7UuH^`ZsNZc7RzAc#tLVbfK%TuSMa-`bczPW3 z-4k=@#u!`S^J^IAG~j7>ta}dnRbu?{=yxOL@(A#I<5bW?g*ZwkP@o+B6$pl`f^(V{ z=e!F}D$9tsSS#^w8yqUS#J%{;dgw7yhBZ4JOt2e^TGJn;AKK>|A}w0IJRF3-68Hi* zmp$6aWTAVyyD@7G#*e7Z#($-6931-v)uAs>y7EFs?nTo4qa zDe8F*epkNtv1pe9%74ZYgY&GF#&LwHmI#0@-T4DFEoM{3nYhegw?i=wqccQe4gSrb zUCi1B)N+98!~h1X&3hnHSWO+iN#j4Y_oLB#%HW$&*DmtOIhwD!XjrfOO46t)5@ql!t_BMfWJ1~P4g0hs5 z215&c8s4%UK*onZTG4OBchR^!cViOEDy%!a1^f|fX85BOh-SvEW3b2Eyq+8YW&l9W zsj|{K14kx|lR@K>DtyJrAqk#P867n7K|bprv15dCT(DasRzxNvwKd8Zz4Gq^XgaJK zLvqlF^@{kp>H&vztc%BpVkCqr@H-a@xI!EQbBNbS7hGrxpr1d$yI5Pr_w33A-= z1F6KKyYQc49s-GhGtyXvVeq*S*UJX#LhkF5g)l~!ka=<98H^qWGSos6!oMw91Dt{Z z7Uvp3g=E#g6to?hmyD<}qS5STTKKw00);WF3aY9mKj_0PPnJK!0(>#dHf=*P`P*f5#e(MAVVMNxO>c)#wd)rsDIRu7EW1 ztpxab0l)$~G7ppP!AP7o36BDL1%6Yk!9kY5Gzm;RtP6!=9RN@gBmj;bZ-Jc>m^jzu z*$S1QHMG0;rb~ zXxbLoCPkjgLQn{tht3BCJNtR72|$R?7_1XA>?$-55JUD3X0I}By^1aYv}AYjn=$&lpA?mwVH?9{>V0uNYLF=9sCA3TG)m#<>6l`C~YI$$%2c)vXRYvq~o2BJ#%^#;HY84$J|l z>3+QA0u^q0e;3^YTbZjn$t{VaIb(+Z8Xd>`$E84`uv9cgVMTN)bN^#B4zO)D>li$T zQQH_eZGkDWvk}IM3XF@B3XFObI?nY^k*vZ)G0GCIhYHguCbe!Qp7_+Pv&T9gU>d$Y zm=(gU9FBYuxBdaJK3lJQ6Aem&3}ONYWH&aZ5jmIRaGL50!A}@M7<}_8p*%(7boW}d zWlxbokY>vcMP#SYP_=TuTCuW(>?(D8Qa+Q?;xea|=zwV}%f$#l2`QW>nC{MUOw^v&0d5M)FvW2ZHmMhf#S$fK(r(eX0>IhWalUOZfso z56K6V&peAPxERWdRvAvQ9E2YNNi*|-Q@cuEhPDa?P_Xq)03c}2waDNMaxd&}0Cac| zb`V=ZK>e4(zTU!v_}mFG!^N8ioa<2(rHF*$Pep@*yW9oPbMt}D7U1L;O1erpm9{=cqnhkSG(2feP^0v@+`>o@rJ0g1v|WP;+zr9W z-GZPD-ye@hPVkXGq_!Av=}gAq$s0rHGj!QA*u^h8$03S&0Ha{sgCG%*9D{hldeFQu zy!i^KONuWgJZX2PFqyN^p*&v)j;jd25i<|!o6FI82X^iSR3k^WMtr!$XlBNfg0o(M z->JW;pZ#KcB)ZKWTsK;NoQ#UMaT8nd8@3Q2<_r!*0_4&r=8cLYFy_H9Mcld=K$d+q zAO~X(Ne~k^IeTNg0T`>F_tge4uBiv?{ehP3M=>Q9Z4IKIN|bY?3AzWO=e7A}6eLft zB?V6!K#ehHA}%ul@DQ(Ae-iMXWrHwtG9<^DJ`8YqG3J3w%c5uxsc0to+M_oT&UO*F zFzk?o1*XVADNQcY6WnZ?y@%!KIzH&e+m)Xk0$n#=MnD&Bs6?MbdB4 zc@#T)W*5Cph9C`sdPhUl1ij~hM9r1xQWA_ubGkte>hy;!!E3R8h2pi?WG;}QXcg!X#%w3k8-6 z$8lv!BnFk<&SODt!Py7pdIcDU?n?}tq>SJKbg9kXB|KW~_YC~#nhk(Qem#Kxc)XGo z-fpY{_=bGo&B6>TDuhy>a^cEvqw%Xke^f9;$87+6_%{JoaZV8UrvQFPKHx#>p}gQ> zP6vJ}6E3*Rhwcv(I6pY5TUsz&eE%bm9|FLX!2uxg`y=buplk8qx;6roar>cCN zh%~critkfm2Q_C&_XhzeFR_LJ%12GB$H5lPH?!a~(v3naEgN;x#5ijuPOxi&O@n;r zKS)tFtYT!dXiq2~3s48-2j#b^cy>N6*gTiEcpOc$)~fV+xZ6Myrm9?thzbw=j0+td z50&X3k;Lk~3c&4CI?yk$*q#FM5_I0f{E{ucAHc)XTn|0b?RVbks}R7bz|Fp)|# z(eQ}nfw;*K3#zgHDDe&ym6GxcyI>S|AyyuQW~OmF1_IkKCe;qk_McJ`s9(i+q_6|- zQ$U)Bbs}#uimnul*X1yDncm-8BW4&QrpcwquLa-G4p7FBIm`MC-DhWaHxGk!H4i&V zheEB_1HK3sN1F2?k!G%h#yt#fK^-dte&8!*h`uRe?+3w4J; zi5vrZeIdv&A85n0+&TZvg#|@$O7eB|>-}uO+W+u|!X~H~dNDvlz5#;E3bs^(E2a#( z=1%~uCK|se@aR5B)7i_u4_A7cf@1|K-2Y4oi{Lzs#Lq^_CICL#-&x%R@#f`!mJ6_w zMcD&#g68bSZ0CNsI+d5C1|H71=(H%fbp~`ApvN^rQYq3`b5#ya5VvXTb~K&B0BsLW zQ@X(w>Xu9pqy8t2nh|5xqv%>QP*<{CE4nCC-VZ1ZF{F4pW(~m=u?7W-P8tO~MO_M= z$$tzhc^6R@;hAdV0SIq917KUTx7m-@>>d8<68Ag|HOennJq1FcNo#P(BnV}rnS%)S zA7{rT0%6*XMW+MIFTqY`w>`HhZB6DF*#R|0eUzRQ$VR?a<2O<(0zT+8JG+w#s+*(g z3gDbRs0oXaqZt)eA^xcK6M%5CL2!axxM#Zib@WDb8K0|5HUsTk0*cPv*sqF@LDQUY zvx(sxG%nAEYa$0G9O8KcLCJj!I+x_{?4OvsUz6cpv70&U32v*mFKeWnv%}gyM2~gAX zf%1?}gFZV{`9OX=c!1T;c%#39j-#?42POShWe90_l9PSYz@|FKrx3{I=mXk8Z`;-GbW0h(#}g{vJBb z+1yUqGGWpb?BeDE@x0G-(7n8W_Y9Cj58JFT+mKaycO5$STeT4!L5tWE``?-h2R(bl z`Pc~W>?62+MsM;+kot8o0L;z?KtiV?W>*#cQg>0!Svy0aG?2NZKI=q)s2>;vS+c*7 z;{6v|w%K=oE6PI_)_-2%Ko`Qk_yPdNWzQ-+1bM?W2%+qz${tWQ9R+U~p}vWZ{m6wz z2DPK&)F3NTiFmi!W>1Ia%pRAfkh)e%!>1+dtf^o|$adxLl1=0107O3`%@4w(@S2$+ zsNyD&gVRD3G&a1TpCO?XI?l^ptJ*y!iqyqVc2$`6OOx#D4-Wy@;olUnZ2W9B3kK;o z1>D2jVF25Yy~7berst_l%vz03c1t5N0#7;et=+UT_yWzSBrdOb{N6Mu5D2b}xZAeawipq{%R>6i+00nuP zTH2AfORq7SaTkr0T0KY+CMmgOeRhdX{}MNiJDX9TdczG)WHE9iXOP4a8M>qrcY6KW z=@H-rY4R3!GcV9)^f;ilAew5+Fby?Q@FHC(3IG$K8?Ns`cCoHZ2a-BxP!5F1J@nuW z*L7{UE+x5#A~8&!g*$unXx%^%6PzJI5#OJTv`CR~v~o}kHd2FkapT7(Q!CLLyXV-u zR4lG;Xqw;HSY21Wph;Fxe4M>|Wd|^-C;*kjQ^`y((>eSt!9?3PQ2fXR9cvJ8vkM%2HFmFz9;z>PO!g%I#L71d zmcsq2KCH>!yq^A9?24?ikywG0ok*@p;o?A^emE`IT`ud_J`?ATjXqp({hWf#;ElA$=Y(SA0I0$f)t_!r>^{T!;aAyBn0p>}!#9PanoS6gz`oATE0G3UYUMTqwFD*@jwsNaZ%ZvFgwX z0e(hJHc$)LuB9E*!DcRE^_?^*>Zn+i~CZ(j0!Igk#hHyVLBsQd68sCNQ2Y`(1mz{nLU|^|gfFz^`$9pO| zI_g!C;TlwAWj5M$T^tE^W1J*LVXji}6w|3i>XKba=WtoMqeZYJ@M_d$hwE7@0NICY zatw!E+blRgwyRflCp@7FTIh%-xI=Ymg@4rG5=zHjf;ph2P~uT2Lslrlh3O-KxmHML zB+vU#nP1Z-5KN_PeJ07UZfu&q%+8@9zZGBTv$aKsU@1i z&W=GBQk|wUs6mO+oEqWtyA0y|xW?kL`Z7oP(SDR}=5S^dwLJrmRTvyg@~wjXQ&cEM zOOxtlE%x+aYpoyHm@=dXs25qnyb`FRz&sPW7-y?v_iZ?%r5!oh@d!KEg0@AghHTlaK?N+$ci8(3BvNXfNi-^{6y_a-dp2AP?JC@a zA|x=Tunpn`D7Fy{#M`LmMcRSIP+3!U3c0g4TnpOFBzmDw#X43Ru6e$j;8iSra3r$y zQ!)95(WCg4a6wQ7gJYNvmIVPxgdLzksxxJ_Pt^HCTun!DpcdfjFh~ne?H&m7bX6{9 zWQ2}yN?zkpMko7iCY6C1hO=WkP~p&L(asImk{>iHcNM@Sp;0gPo6)3f-~Rtd_rNu& z1gFwKfnZ>+V&;1m=1KsEs8FalmlDOu+*(mvEj`ZBvwva%y$;820cL{YK@zZY|EOuQiHGh+ED57hIYJOn%^gW`5u$JkKwansV;D$RN|AKv z*cHmkg)PB+&ykLTD5|pc!OnznVFuT3ZW^_{DV{*4sffs65wUj7BB~TImLqB9-=>gf zl+_pwR9biNM6~GiAPbReQEgv8q+07G9|1~>fZr0bbP7A4UNReE3`Vi*ToV^;kY!1W zUU0WFu!o1!CZH9<+8ro?1C};UMLN?BIH+_LFH}~(&`4JX)B<_UQq0jr2g=MVHAUtZ z$l(2UXtM;i0=2KS=2@-O)cUoPoib+?=F^=)LMS8iB`dO%Otqn^<*{8I$!IKX|9Ob; zO$)qJ|AgVsgRimoX^5i)ia~p_LEF^K36A3H*c+3|8LNX80T)u4^t9E)6>%4+U{KK- znuQ4B8o&ogN;Rw+=6*Wb1|12Qor$&-7A^ZG?L{rut4K*hT-6vB4}fTuol|ZM$r?qK z=z~K%syG9H`#lm@>54wh<8UzALMknwx-C%KLn}sVjYA2$hKnpzd4#RWF45Yce{POr zj|YYg3!rm16uOCQh%8$LXPmMtZI{$&EspiOEj=PGj1TyEtkwCt2rDHcBU0IPTv*pVv%Q;FfH^_d#) zD6G!NYejybSWj>R-9W56n(>g~E#qsm&)K35bAn3miGtAKKqZ1UweeVeqQ^ir!$36X zq~1n&Vmv>?YW~ccIf<`;)Cx7?w|KqqA?gpn&deZwF>|p}>K?rT3tfnMfd*&}7F5N% z5ukvR)u$DGfvcSz`*yoh;vt5!feTk*0|0z8X6sLTVF#*_)K z>Jy|<96>C1@i$Q^t{dMTlMx*Q0*WJpwlPz@7$|95$A}yj?P44usUlIvq`@~7&cvo? z&_H=WnCT|s1dzK#7K7DQz(V9B_S3QV3pi$Nf#9Z=O^$t&ZCYit|0_}s*av$RvZATN zC=%(#yCm>wRGQB`Ao*ZBFc*n5VD9tjAR@{Hr-*W)H34}qSP^YvKzL?HJTkro4pStS zf>8oK@Pq{Po@o|OAq+Aq&wL?w?2@HNERQTi_-1K^`iY74&@o%OU^%Kzc14!fs-OD% z^=l8Zmp~X8rq;qBU{dN(cp)pNk)$=7dh!A@WEkJa+say=I!Wts6|jPa)o!ReTo%VW zk@3L$1iNMdryT8NuvH`nx{ACLaR9Sm5O5{|PJy_rN^=zWT|BG>u%Vi0C}`IH>)E{UvZ`Atva?=oZUX=#$G+bNbnKD(p(be`IolHuyOENuqHwkC z34_*e#p*+#eS~GFBmAh2paSl`W4D?LhV#3moHPMOJG9=_;5^C{8&$SR*F8iF9yOs@ z4&^#y)ymD!2Sy^3a;XnBnMZ0c5)TzXMHCadr(uHK z^bYdm$~|ZUgYHy|dUiC<7d6z1`3T|H+Rr*rUX;XMP+0jcVl z_~>1h;MhChA#E2pLkdOqw~aPKN2>hRR-B<|4GeLGI5if@fVVD~#Tj$4Gz{I~fO4l{ zC=dw@J7|v@tKA3GodqHygFwRKairQeAPj&gc+z;6u&jZaval9`HMgNYNfgJ{!czvL z`-gR7Brn)(jBokC8=Yj#mxe5mr;&D!y$InE9_7JbGON+}@CA`vSKFLqyC;ss7iSF0 zkXVckoW&(_Mz^_0*6mr}=66|HN0HgxLRJQK>{+!1`mD=XN=iEET~`}rxYs?whpONa zm}4IpvLj+R(J~T%z zJamO9W^Oc(v4<763Hs$Ad!7AnfV#*WMv~~YR4CHZG{Q^#5y(ZJDXLOWw6Ki(A21B` z$sgB>iEB8HeSjLqU2UB{=L8&Ms=8DbBBpROdkpFk(rpJ55_^Ihdz?PFa?CN4s$(m% zNkX`KJEj`q&bF+D(;Y$SEUJyu1`m!eLtJb+_dyNMzfST**s;RfxZ0?SIf=$^({ z75lVb+wqFJJkQi;F{_&=Mtn2UYld7PcT)1(m}446P)3ri=!GM3VpfHPsG3DK*)x_Z zQd_RVJrHlfJ1}bO2u`W77r-D6Q0chEes* zLG|P+^+kpNrOfs10oq{hkYWrEL~#ZyW;YH5<-{GO?Rg2v9|zXdws1fgD3{kB?ZT>r z0L`9f6N-zBnp*c{f^+h&_tYi2DmxfoIE1g1 z-3P-V;J_DnE*Xi!>|emGH2It z7~b;^*x5krSuh2z8NiWJ=d#pJ5{L*mmP&U(VynYH45bf|jO@hPc-BHSVa~Y`gOF10 zt`vue7+1l1;c~*S0z+7nFG2&M5*XI#wzOS{OCUb0%eSRr0%deIUxExFvUoIU7;Bbl z;yYdP+6b0tyn0`Oc&Usjr57!zbnG*1NDMAM`>Y~V)S|NOzu`PVO~A32%}iQu@;)0M_siNoyj#Y)&57OM&fE6FN*gG%rku(lke7Z^yXFrb5iwiO{g zLSdABFs?b>lQHmk_-%(&}wW1~ze4xaisamFIYaaB4NxLyU$37hL8kllp{)#$u zlt8Z80I+A*3+D!IvVl~j8SG}kPaTd0P}$qDn?NXG07R|p_#I&T2ghD0lNe{{bTnvy z!L{JIoRJw9sIxNnjJZI00rlF&t4Vg?L_wXL`G^?YsgLDRA_QZ!Y9R=G!+HRFDD-Pq zAv5dd@TXB{6(LWThE}7Nsa8W(AT`v7PW5S)G0M?()4h-~N!H9+kBx$;h->J8rFj+! zUr%s+vsib0vSm=-Wi8A|t$=4JR;R}R0Rf~K!B5^r{YK@g`{P2z*bFLSxV)h5CsFJ| z<@;>pr3+O|y*~`U-{_t=<%}_D`&k7U%<=s@HVD!mk@AHB)Z!c*NY>Nv21{r7C?-CpzJ^d1b&(3yc)U=g^_$WF{petAmDe8O42rH$P@&bdi8#qG z)*wB2^#p-T$||~mQGGfpF0k}<>?%F8EW+<^?SVWsqZr#oGc?z?%s(&$0^bF|Jk=Q4 zuxR|jrPas!2K9{LDZA0&-=+RN>$!I==H-O|gN~7{P;yKYZ@vKbQhbjB4Eu{U!blyBfQDWP=uHt7+Fs$X~aMB1W#PX*mTjedM%G zH-YNF&S+VKiK@%P?jBBPRF~24gvixX5ecUN;FwB0WXE2Fb%>r*{Gg31C5*MlN>u~V zq^_Sp>QF}~aJCR>%rxWhuLq$`70w5C6nMw3R^W_Ol(>kNWUkCCjC4lf*CgP9`Hi~B ze04rdEj)g)Lb0ayMnnPX%nfx6MW;nkn0>d6^9amQzfrZ}I=tV3YIrGnooL043%)Od zZ*jd$99zYf+Fb8QqIA2va%lM|$f=DH0tDtD!tsKI$R%^Es!ra)*1@y)R9xPV8Q{h? z2gB(@54@<~*!t8Fn-+ZGZ2j6&XNpszJOnSy<-2qzFSDpKEJ_-A$w7ABU7u@4@tg?@ z)DFZqHTmo5toC<(@Oq#T+Z@$(`pl(6ISdi!)|)niDD)kd;FY%Iu{gn*RQwH3c7{GB z@CV-+o`+;Cj3}3CwRv?V7{0~LfhK>DctH~E(AnKHm?$wWrp!0*RP-XQ9FQij+u4`n zaQH}SmTOhn7NKi&=o6jb6&z5Ayh`Aja-wsz3k846>`?AvU-9L0MtT9;~!n3XHp zM|aDdHbG7n;KjXEqM0FXH(*O6O(M6U$M>SVjHN~|e4vSm$AzoDiokg;Iep?p41js_ zIxnb8pJxpQn7w2WB34OQNLm{Qlmp+!h1uoOI@JI%L8N_6`4r&iovk~$tZJUC}8@G8;`vj zCFcIOmsfqwQh(}IdDYv7{OQEG7Y#q<4{ukT(m3MP)0$RSp8m%tnoqyv{>Ygd&iLf) zXTCG@oKttb_4{+LyXXhU?>+9^d2dZQH+{mo^Typ*a^a{$k{5MNzxJXXw!ig*2$6tEHZQEXUwR8Jrk6iKQ<@RZd*6#RBdhMf^|8DK=$4|Rr+&c$f zvE-h2uKN7#=BpD&fAaI>ik+@GV3*IX+4%nT*MAW`>BjgGD{rchrfea%IymFTHW`ZO^Xmxc%ENU2*#vpVr)Q=Xr13G4!t4zde3k z=FTMxp1!l`yoq-eo_5?_TYh%H-KQ;?cK0)nHr{<@=ZXJa)iP@RRlD7@;gRp|ci%nJ z4!m#c^pW?i|8wfTrWfjdcjafV{H}7*#}AFT;em~%8?8USy4$}Vee?A0$6l+9Jh8`1 zPd`!jLhF+!ozVF7{ikJ~UVX{O&;0Sq%bvS^_Rts0&i>KM*2vdiK5xRfSI+FY@YPjw z&wg#2KmF>jPrN_w^>MWaztQ&eO>Zn*S^4*2Z*RNl!A-GEh0p(V)9dSBfBX9n?fl-Q z-_E>u%czC#Z%jS);e~77{jhkaqd!_R_S%ofzH#5jA8&QRr!P1B_0!#NJM6Ra`~LZ_ zEl+*q^UMBvtz~uo*q*iT2K&yxHEng-$s4!6{ZFH}`P)Crwk_Uw-L`$-K5N*6f8KYy zUw`=R?K-QT-)`2$PmgH0_xf*5ExBZeDQkD!@z*=;v*YpaRqXiaRwwN^a^~<|79O(Q zt{WzQXV(qq#CN;#{M2sChx~E($1@*|T=Po9o^N%3w$}&F<$L#S`q8NU=e!bmaAx`F z7cxH{Q!#qKv6oD#A6wJ=l~>z4G1vlcxtV(u%CM5<~Er&UdV zYi;!n6EB|ktK0vpZl}Tz>W1!h?t;pD)-Q;k`qIKB7Z=q3eB{CPk8irG{-e4#>Z>Ln zdD!^3et+1)kA^pNjQ*d7(raRkr`%VtXsgz?#g85O$&%I=wm#zISYqjK9^QM|GhZxP zabeSxqiSwzh+g>N+tCO9a8C2SH?3=~jx27u^;dVsB2V2NTlZ0Q{FRG$NR++(!^Cf% zxjeD$oZh2vTzb~g_f5!jT-|<7$7>6})phFq&!wI#dnffo_lWc*8=KSH-*ie(b;Avr z8IR2D-Q)Ma={@>q%a5IX=dZqV#!!Pf(wVf3DvKWPoQMqut5Op@uYtPwZ$}OY!$oR26#~?W0?x zq-X?XKzFySsasgqg~(dfj>L<_DAu$a#+Za>lhAe)+TMu#q#Kb;=sm#s9>#kVV;zCH z6aaq5Fw1%eegBI2B+>V={ZI-OdFo^6{~CV2jJaGm(X!^F-9kM3DcU@XdH!lA!)DyT2>!?9lrZm>{G4hBl101W&vi=5GZ(}Wu$UPd}0qwA+J21}6z|li^ zJ{ohp1Nj!8+S#%mM!Of$<|%Y#LjC)Jrw1|rcY%|=0Pn_cp&!<=wT*FxU_D&-E-0mo zxt3#IpJNWs;rr*%_7$`}40ziUxaz~awnvJ?7lF&UXul9Y-@zKHfV<(q=_BJ&Ru=Q@ z1B^Pi}a4aM}Z0 zyo_<*1dJoFXLn=W|AW4V;rVXp`3L1D8 z<37o~#yVRt$5nv$9qi=~0q;uChp8}Mut@UseV&c}TBz*_1tmtC;tLxJ-{fxipz_k66Q5BR;74*(n!veLZ%ikD;s8FO$lqx6f-6#ew8H3$?p0nx|W7@Wg0G(IyAgv97}WZcVxCG zmQ7P->eAMDV7f4WmqtuiCBJ0O;p5QZaDS5MO00qT1WmC|^1FjMb_T@-D{X764`PBh?nZ?5AeaIG&xnBi5Gp@1fMmSg&0&U)Wu-Qqz2BNo5 zFw9rjQVzpxkfvyD_!@SD`1%TKXap4zIdNYdFI96Te3)ts^A$GO8^)yA4+noHz^~f; zo$$bCBa3O-2v5~IX~Iv|t_mOgehgl_`Gb4XBAe+;AodRVIsk(r;T~v)>4O0mzP!Jn z`<_8*Q33g6QqazZT$aMjUIJc!(qbmV^H)J+JAzTcT^05wk z7LtMugQ@&~1O2;q5H3^PF-LZbsKkY6bc-pjlNM$}HH- zH)yXMVmg@uKta5OcbWkkY7QTTcM40J*xyoG1|TzY0~vTrPX&PKxdC``Fnke63B9rp4V4%dwxB=3aJJc(xGRMu6pSp)v2vDr#* z2KVm)G~Iii>{NDHzzUITC&M((Q8zGr@CVRgTsTA)HcdfB#xNDi=7})Rr-U~hXmgs; zwRoVe*&FlU=;RF4DYz-0qRW`@F03!h^rp0Swt^FrW6`P^^J>RMm+yk6({n>)PF9wb zFlkhS+m*KVM)&c#x@RwVO1^3Nb49}}N7Eep%(|Qz$SszpAss1uYau%1#Eh5H^2&V8 zyj`ZppJQuQpz568zfsjDJFJ&EV`|R+1uDZS^D}gq65b&=LVsEGF&xn1fx2d6e=WRA zDeTvOg5G#Ra(f+JW^)|kSs4~V0^rJmS(LN$o<aPu{}r~O+k$-ExVjb)W(Mqw zl)(6#!YuRI-Y3H|&s*BEe@5T%jeBPxK>ob($_NYx#9EWGjk(avOuSr}(p=>1xb+b_ zmFL3@t1@KqNhk|A`T zYt`9D^Vtb~0D<{OfxgoyX_y9K))IMv6{jsyg7Yxb=j|H-~svVuO z>W&4w&c#G1zO&uEpkK5n!uENq-g#9`!YS-<0B|R-qPM|$@VD%Jxfq>F%!WvNe#O6y9P&l6@04ncn z2DbLz8W=BSKuV9n(7|@a!en6} z8W9oV327Au2-d&}UtSMDq2gyas3gA6cXak4Izy&~ckYAUp-IZx)B^$f{UjmmFhC4` zbONaa;e~30=7UutVmYk)BH1AZTf*0W9J<1A$lo>F>UkBNauo0M#tU-5<2dLpq3-l7 z%pmK8Pw#)wZA!jwenV1KAj*ioj9a^uf?4x*RYvkkw~!$P3l;76(6M+h9jW^3urV<# zYh>8~o$9ek^@!W4-BijhicY#DsWH2QND0ciFnx0uKsfm(%V#Y@#}o{-hF?LK@`1YK z3PCK#WsAI19#|=@2Z)*Z#`kC_oG2#Z0!Ut>@Bm~}ZWQ-Y@)CvDHAufFpkrS8Wk++4 z?9tT2=!PA9PE*b86T^XOhWnC{v8X3YTl2h%aQYt863V8PMwsh*6kb2(zZTBQTyb@!Rj z`~!g*2yK-BB3REi7Y|4_@@_!)^1;s`h{A(r;oe3gL`NHRdXRc6ft~)Ba%gf>`}0{h z{_?k6imM60QHpi&z$p+1z7A458y@23f~DvWCxw=SaOrPzz%^pQDvDwPG3J~je1Kap zK+{(nz&JMHfIpwJnVZMGZ^q3?RXRIA@S3?WCBtVOKAZ7XIM)jAkWI2H&=lo;@0kY0y&EhtH6>V;sVZugyGIv_Tm5XkrgMr(NcW@XG~Yu=BVDU?8`0feBhS{g-j{ zeApVfyBk|d1yXq1kRm5oM{l=7a!LYaf^`GMlup@xd^jyV4sbbUv`}oT6po@P@)-UK z2eIf-TzUy#R8a7~iH11@Z~v8VE>qaGIXAdJaH2JQF-HLm^lR7#d+I2>>zU{Z%P4=> zY_V_^I>9o^-zkX3nRU1t=-1QPA$xWB!#e~N2Wf1aGM81k?k;?%grPc|%oUo*ED!_5 zbM&%@3CF^B7Yae;?&go7goAh-Ai!j~LFAX>6Y7N$a(5q`%0*<7QZ^qLRyL(xY{B1- zL7n-Y3ZP)O+(7-$!m;zy!v@%XPgiFzl;q$Y2Q`5Va^If(9fpDZoqMKD;GqntE0-ly z+aQ*4ZU7!X1?l@XbcCInzau#~KN)-uFnQTT-A+!x*{u+f*$#Zruwz)CezRf^Q|~<& zn;hDRh>0ns9JD4YL~D9g-?<&RZL(2KVtlAdu0cTIgiwnP5LICvycj}w*XM7%70noM&fo_ON5lC5(mr?~;qx1; zjrsrvn>YBu^0V^XC7ZQ@3+48lqM>k990h<)gP$EE6TF6-hD=a?P_7py?gHS$2M?Go ztasm+;yETlIegX!9u846c;LA?c_jeN{VITh++BAhRKy}+CMJSSnHS!rVlINhLM@6X zi`jRvk^oSgA3*=Z(%aB!cJ59jTjg1(aapzs|yc^O1t z?v5&smBO06IM&b5r8ak$Fw)AeC+e3Y_K_PPZ+r8|B76bA9H=v=-kPKHLEp~1-+q-H zKKk7NkO%L%33kYeyn8;Fv!*$N0YL^QfrL-L4t7++DxE zhG;kntdx6z@L8P|g+R&O$!KF*Ej%{LeXx5I0F~wjlzU?lnz2d-(=jM{o@_x)_l>sX zJZ7#ilmATDQet8Wl`N3C`y=g0SeJ?-i93_At27!Wi>_qEXEHTO+mHMfl7=Sj6;R5b z+=OX~R0$VT{VkSkNjqK%IBC!spQsaN6m=j4R8h**RogEbixufdj(xuE=2T#^3Z1OY zOQIbpifmGVF-b-jQxus5u7yZ}ie+{XD=MQma)U%oG5O71y?u{=Qf{J(n3q2@8c2XG zS)!3218GVA*DRq?C9emrQCd8S&FNm*7GYjXA&!>m%bKI9Wg~`-7#7yfG+QREx^e!AO#1~>?3Vyq9uwt zkDYbMlp)#A@c`*obO{1UX<`!9G5M+6#m!#a8EuQES*O$wFpc6)E~R2$K%^Na_mQ+4 zDCu-uJyN7%%lv~$!W5M+%B{$5vXO-kf{atT);rSh_M+Ht4RQn_LliOw4)OjXAM;(j#5E*UPR2KRpNNCtV#c>`JyJ7+BC#r)PF=U%cm?iZDZ8_^(T7bKaozwVDW2_%aX zqDF>Jq`YH>K`pF&`P5ZPL`Xs;X0>;zH0fi(Y*3NC$Q9_=%}u_VyQq*V`JZ)xRaT{t zG^Z#G%)EV4v_=<#W45|rx>6`+e(7f4YEDQIF4X*~sWLn(2>_9K)GfBl6w)YQaV;1* zkTp>Rze)kC%>`t0swetXLnQ~XW>*l331n)uFnt@u9<_ER)rQ)VXvW02*$~&-pibsY z^a@0|vR^v*YQ<^8$P^iXR#6+xc`LO6)F8=sH}dnvk&w(&Ksih$T22#P()=}DPu4V;QB{yma|mU-^#8k>=+Fs2jD6&gBjPbNTT zEGNKZplQ=g5gpLmKiQry>6QJPApuLzWL(>vsWTUxL36gvs;t@4!pNi&{N>LwXOjFo zS^ibul*#8Q^6ymnw_N_6hJU9x$m&SAT_iwV<~Nvih=K> z7(2HabPoh*k{^&WDc_JE+|mewB+3*=S8~{6MIW08Y5I{FEUSI0V=8KaUJ^@A87$Pb1sWq za0L;1xGA!s_D#8QNs%d4tz1m%ftan=fHUMb$DW-}5J}=)oxc$U)x*Tj zcT@pAs8A4qB5F!9gEloE;1$v|2|HDuWfxRuK?pLlu8%;%(a8`2HJM5?Z)H&tKGf`^9Zqq7FRaqCIfI!FIW2Q{93kJ$8^a4WGMEJ{W`yCs+9%xOEQUIa`2JtOx7TgKxqOY40l!oX2 z72Vxc(Vpo3%cP93b>>Ay!9Z;}9 z_@NisAejId=mZVv@g(D~s*>j*2%+s|%tiA{WIXs03Qv(JYN|{$+5nT(#bvNGT~SZT z3xd|&C*ZTBU+jnyHFOJfy5N+O$x220H;6RdqDpSXBUNz}F6rp0NAV9eH6#N~MC56Z z_Awh0iX!JqA!GPMuJ}NTNZ??8mGuvi4{H%AGI?9=$;Qc!%#Y2ExWDMBz)mzJ0G9i_>%|qUK*lOwKpmFF6v47{ zEISF+OrpWUjMf0Xj4wiJ(U$lG7;7m1IUXOzS1+oq;6`|9wC%$iNfI5x1gaDovf(rI zqNqpL$3pZ)Rpf0a!GNc_kh2;2mx+2`T~+XEZ@eSf4MD`J-YS!#YrZc%Jc`2}1Jc*JK%_QY=FWEV;ZIVy6rN?2C{+_gRGy-8L zfxG~~v09|MmZGv)q=<37*0kot33e4Sn5U31ThWTDQp2SEstTKu_P#Y3OADNoNOvc@ zSpEeDaZK+RmXf6oCCr9DFqH;X;rKcd#zd409EUYdgPXC_MuvDnqrdnct^MAJn=UqUro1rUmfIq zU&P=CTwAfKnP{rveIw%)rf`WQNo`-~r zrM|Y9d{}27PVDKxe{Yl+WlIsPQ5)*mKleN!uw9s|0R{)d8^z+uYx9j(FLBYlEy?bL z4%luEEbt^zNaLE~P2<%E(Xp!(fphi|1bq;DGboFOM)3i_y_QG#2W?O)THPAF0`- zgAUG8;-o_Mp4n#rLlwA&p<^d7hBiB;lQD0yr~_(H+1Qyb9>K^SU}_>xUGglXU}xZg zPcjImq>`MpR`&MsHK?E!M~HA`L{HwB+bB%<};iMD!Nb_)R;(SgQ)Qg zdVo&HKE-1@&d_gE>MauDr-E`xS#OH#?5a&sCF;yT-XMZOKU^_Z#d0ufb?i&*3SDPV z=ud;zE}MUBpspLhH2TCnf~y(h`GQ1vK)kd`xXu(L0n&Z@Ox%DaL1em10^NJYxA|7v z1CHHds&0yHB9xj&q}k&-3>jKO9y39(2=QN(bn@!b=+!|yd4`r_PifEwzgYin9(_@= zAcL&Ao2fhY zNwuOPk~-A5U$$`*6%lZF_Z5)|PDCUsjgo-3Ahktx~OU@gf0 zY>FxrK9g3-b&)pk9#D5h3G93hu_?S@m|Dwf<%eOXiQR8PI8QZ_0@9JHN^qkJuJ{<5 z64wYFsqE(1rGx62C1C8r0i!_fK_&w26B~{0L6jX>W06K8&wt$68f}3i3)$8R^Ib&( zWG_AP<<3TmKnyGf9VtTW+)$Lg@6r&Ee*3c0Pzdc;O(H(ug_8<|=9CfNPN&?^oMRt? zvNlmTl@$9BJcb%n2^tgxWK04ci+9E-OO%rXpT0uO6f3-pHF5}yy(F}s1yyNd1G|S3 zE6wl;^^pR}NmH7mdIUbvXceWR97S(kSkgt>edD^s+NP0gIW+C$h?-@%NHaoCE`qNN-Gk~)Pf5q zv}R0wrBocI4lEkf%8e{Y&I5L&i>ZDAndtkLN(4enS1EsJNDYD+w9vI=P-23{9Vo?w z%{aE-kWkfMSoI9bI@-D6T5*O&c^IifTcki0cS7f&xdWn!`fkmLsc*)Tpaw-0M6Zz> zZe%T>;!Fu9ME=szUWDINaGx$Cz^mox`iLsbf~zYnEgPRxaSZYETbzGk!=(BTCLCZ zj>)k24vybA7$}or8Lggx|1`XzM4XN_iqv*SI~XPGK!6N3$*Mxr906v>{)Mgho*e0M zD}O`8g!gH977+5FCC%Vc&IggAxjb1zV7#xhIfyo4vK z3iG&iB(=2)39JPcY8U!c8@@L@icK)2srJ+utcC~#f@2@b^5=1%#jBWuE5TL@_jo|{ z`jtG_)AR%Zs|Ncf10P2iPak*6a#-y%U|-AD%jzj`VTrk?4&aeE2?+e+QLaonB2eId z##iw?%=mXIO6lSy;ZB8%!<4-P4OGbHp|=DXTOhGodgO>sW0^i9B|>~vONW%)g4DoZ zoCRjD8Kl7PBGgsuiod89CB^N)#iARv9V*8sz3|;3Aj7Nu`YcP9A@9P-ykCR%x&RkO zRzd(0OT`(5$-Ee?urQAtQLsGC`HJ6!My|=~*e4FOJ=7-XhaJlHIcd9+gMIK+m`)1K zVZH8w3%hb`ntu4wExSk$-?3+c+G)9BbzY4v)XT1v>lB8s6s@{ME{LaWIMa49-2tHr zETt)~Fr4ri!_7pp$9x_L9b3q1KvAjANHkKrHe92TCoGZVMH=`fIOI_k94Wz^bZ-}~ zv8|X7(F|HyEo|$fQ5bka?9_5v@)M&4C}iHzNBQ7x3C6&N;c%blDVao3OTvVnx!Tnp z1!r$Z;3}UcdCj;aQuNNNZi4}ZFvow9J9fY2skQkj#XVcfrVtP!Tuj%)b?h-NSdxhs zzwtZ-$3EV>gp)Q_y9)bivl*M}6_^(Z+mYy`<`Kek_pPWmk!s6e7#?QNkmGA#%)!UY zL1)k&ADAQ8X^!)hEHWVg$F9{kf*}ji8TdP>?TRM8I0)VD{=p@0kI5jQFdV5swI^%6 zL%&?JBmA@|FkW)O#PmJN7P%n;DuqH916?036r|zs_`kQAn_{WfU^}TqEAY_HDh8a zI6gScf!T!nluLsvpv3euzfWeV`i7ughF{PKCL5U2ggZS-q!U8R2le-AT|z(rrvNTQ z2izqU(e?PxQ;Aw1D#wj04aSl7o~fI{$me8Qdi*!fW*heq++t?!a}SO3HolXw(p+sf z!ykPPAvx$X3T2*RXV2ZSr<-c2!aF?miTU)KF#_~?tJ$1PGeovyzfU4kcS0cvG5b!M zuApl;Af0ju-V=o*h&ZdEZm?Ut3kEvmDu$Y%qgVzyy~y#66O?iR7GoKj`~D+TW1wOv zJ%j^?hG%BtoMIUMRF|)O_cJKvc*_Ug2E!AXo^}v|x&cjyy22>g5eKJ2sNHes69{O^ z@z!LQl)UX?wca+kLq2Bl@g(Fu?HE)JMIBp+cHV_&cMw%G>uU;L1FL5QfxyiAeZX*2 zIyevjP+{nxgC2`Ks;i}|`^>A0$OdSOfz!YWWRPL9bSBX&Of7P`z&{b~xd(15KWaCs za|B8?wLe{taC?DCgxRus{g9^{JA%29Y&2`#Yd>_+{9_ z_^R#%PWcTT2GFR0`1TW=kVXO1E(p$i%L9fd7Z8Uv2@V{F!xR&0jG|t3aU~_%% zXcd?1d8DWb*9RcqBa^2T<3_L{YKEL4jVtQsM^?uB{9(Jj8=%B+hCrq287a!ta_p3M zlR`C9Q~_ZN0fIzJ4e_f$|}83cn0!maCYZvv(<8DppQnRKcbxO2z0-+d*XR#io#KpLRaAG?cE6)8A3F#_sG7{SAdE9a zbeH#)1nbh;pcg$HydP~&(!w|mT3HT`hZ{@6E}WPkJZFX^(EW2?T^`m=gu4 zpbYMsAt(e&`cE8_*<&RTL$lT|`$fQbN8R>!jFIGNV=4f%eq5tm%xgp7kzRljqfEu7 z)$Lk+CklZ{Worp9dA)sME2R{bgj3cSE>JE=b~b}rh&OPD9AHUYD;3AFA^gg`w>Z(O zq}Z|da|MH1YRFNRV;`l+GmR6N7BLSzCF^}Bp;4TUC3^ZWC5C|GvG8{Fskn}_LMcaA z#v^3|pP`*+S9hi2ZLqMhVv?1)kf|gxzY+5eLPxz}0}r)}ZwSEh61PAM{}$8E9JXpY zlVb=MB2}P!aUCFB<u{c;ZCcpt%2BoNB1cQtjMmS28I(Dlc&%QPw7k4PAPqLR@ zihCy6tIU21ZnHMrqQH@ln!K=(9@4ST(}o=GSmF{tESk-<;Wn?IOSwEkfdz-CEDV&m z9GwWww8rBxd*_+jy7GIyAJLXc#Tf#LG|sCiDk;MYBK$D9yp$nZek_|*LMy?0_sWw! z*{nhX#(i^8No-hBIo`39!#roiuv)yf82%Qf_32W8W`lTBq#kMv$`t<%;0(uC^N9_5_MI?=xsZ3AVH4+ z;S;BZ7NDVf6S$L-i(oSdOSJUJTcqGOKgr^q&bD|8cbFMx`~9GB>EE8byqOQ3W9s_G z42(Yk&sBL04;7183a`L-qz8>XDo%#nv|Vb>WCK+9^$}=zJg%GOAocQ%HYvKUn@ovm5H0FIPyvWQCxeAvb%V(15$^^7mrsVjwNaZ9mN<@ zh`k1t#&cm;b>emaA$SpKlQ+P z{w8MYzLto^2pnv^-`+AitA%2?qzB;!uGfAY*O#wQ+pJQ!br!#yK5B1H=`J>8j} zDnxKw;M4Ts-F}Mdcx;)yw4$SpZ@#vx7wp<`r-cWG{UnIA7k#E3u6&{c<^n7T$G+ZP z-0#I=B``ciBvRYc+`i>VR6|}zRY3fb7aNOn0By0v5^Z>O7raS6azW)aWX2<$(GCL% zaiEq2u0bHQ_lm7AV+2`B$pF3{QGkmoqUtYmnh?*F2=RiG-hvD8Hba4a zbHKb13dv{A)I3J|H@rsuC)+#MH@wCB<2d1ff-+(c%q+X)V_BFR9%l1R9!CL^!NmU- zxPqocl$^zQpbDWC;iSMH3*7q=7IKoQHn|2g@ay3}2_ZBs5`FuZZ{BLt{j>hY-d(>< z5ta{uV&R!vVrriG=H}rPA%N>}t|N!-0b>8RO#C`MaTA>##7uFB08<=OGrP@mN)w_H z%_O(c3~7IJ2?Fjg+^g-|0*NpqoYVji!U?fRISDBx`75Hh zvVz6?g}7Mk)OzRImGCe!xOGcwLb#t7-XQ5x`c?(8rEGg;7gOAN7o*$(cy%Z+CEtAv z$lCTKjs)RX`L+SCGRm=;_}cC$j=g&$RTLc^vnF*1(q29Ym8Di`JoH$B-dd`*hMLOt zYbQ^dGAV1OMrrXiUUfb=K=<4;yeO{?!J`jNn6A|0DNo=PDIx-D+F=%+dL3kv`vU6d zBuD=+O%D))NyLBb+lH8U;~LPK z$kKpyVpan8~sH@WozJPavy^y11L96#cYKr{v3 z;w2^}QH+RpeSgX$IO8q2&?-JTW3RmoUn2O}&crdeKaalG?qqbs9M zXIfpOEfTF;6vt6e-?4B|bO6JcCA_I8S6KZw;vIW|j#@*^;~qr6>^vqssxtBbTkffV zj&Ylp&w)kU|8VR@jp`yU?J9Zz1&m3+Euu-}L@Fek42`?YD&u4jaaB63sjh_q8F>G_ zX99zEjo^0p$~%gOr`$>j={okofpAYhBNt|vyR%{fL;k4ML;zdgQ;-PB1w$75-hN3X zk-;konPw!0y5#1A4{I5S0Gg?~sjPi(9);&|Dd9>c;|R;m;ASi&-o5W~f>)!(V4BEG zA;_eZ>P^ItRWup8gXSU$P9*fa^~5+HoKYXGxc7X{T z`LNE&4?-G#Lx8WWGMm#?rRZfmR+(XhEm%6PUYf@-`pfnG-%a2*hnx zN8OY!j5xi>6qh_2A6n(^`~jbhIx-Qsx6d$>7w7Pu5%K$?C>|M%IqW683zVHmNaSs#>fj8_WC-s_t=Gtch0=S5HGY42oQqjbbDSyEAwPo zs6G_2pmO)Pqk#C14Yy;B7p_DN-i^ZBaFmXiYFNm_Kd*e!8(U&5T$M>P9ILp>sbvDV z1yl*;B-bAq;R7OhvBbT30h%@#CB`8gO>8Y7{0yqNOC906B!{LxdCQY}kz$A}7WsBF zYzwpdx+umg9k>}DUqx~yVItRy!YSsRH9?g+7dQ4{0f|)-Mamcki516aYL%Hlg?mhr z^&oY3O(+23w3gHS{#Q%Fmd z!yK-xE-fxEpHx#*R#RM2HMzR1eA<+)Cj1 z-=_l!gvo*+J2d_s`&xvfL|BT&Dp#@1-q6;TCA;1LUsT|m6FO5U_ZTw_&DjtdZtmx5 z`Y4O48sLqA4}1P-OvJm!h5IDNW~VN64||RA;y2q7;BKM4H(u|Y95<_Qm2X5b)w}@^ zSwGWmaF4&l2*c$>xf>&lpmc`&ZVCmzcr8rp!F%GqOh($k;)Qc2!ZM5Mj2;aIk4RS! zF1>fjM{PwI_2fs6C+X@^F(zn6Hgg4rk!1|0bsn#h7Qk5g zroT3fV1s$uRhhyV!*gWN$2}?3MBWc!cy~)_rHo|H4Q5tT-$8#wTYc|My5Gh-TJ5Rc zo2&Gt)T=_G!Y=JpL;WW_+T^mS zlc$tcPpz3dw|H9Bl({w2s>>^;O|G6)QZj8?@wAG{vdW6e@+oEI#U<4xWAfk93t1qJ zn+@6aj|=Bm=ht1PXH|tvs9$h;u_Wc-wL`=T;=VKwA)TFN2Pjj{E}AKi^{e@o#S_a=lJ2o;(S~t1W9NYUx-gfn-_x9dB7L;OE)= z+=y(GU2SQ6wA#^i8-BN_(dx$M9r*d+Me|WB);bZtU(Mg=b|g_HMg9yylcD%zbI4Ek zpKR@h&-`8g#dG{QR?mo^`Go)4)|Mw))>hW`_^rp<5&zoOFl$fzRBiUb&lMT<8D%>8 zXJ%b829I;LrPLuDKXGBwRx3*HWe!H3Rvs1M5?U>Ets~zwm^Myp%JDknXsx@4wRQV4 z)_}Qe5#fxi%nv5CU*COC)lKhiT=>YCky~0PuC>Nm)|Mw))&+RH71xj78yYJnSzD@Q z-}p?f} zIO(SCo|yc*ca9p}R`jDi{x<2_o9-I1cI%1zKXcq~k`H|A^CQ0h-G? zzBc2Oal6dA>4A?fIyw2HKYV;}{a=5yEcWUimsh{^kCg`;xYNP^wIP1sM;o>oy8Jrp z>X~W}Fg5FLeva@{|35eT+F>s$lc}nXjyj|VwmQ>LG4Z%Yg=KxYzZIF&AHe^8Ge`{B z!e@Jc#ey9aCl`l&zb%Lfm4B_n=h#*$KAS(KTbAPEVyn?wfq$#527H=tEktF?75KTp zs=;UexogXJzaTko4-V|X@BGO>yUV{IVEk0PtG~dHD*UwpHHykhrl=V!#I>SA75|Kt zUQKu!#kXn58n0+2pAX;i<}EMSf-qPRG=<7mtYO|CQn$1zwo0vHlt|;xX8dcR2HH6E zHH9cuX-o<&&Vj{%QnqfnEcC9Ypkj6eZ#`2UO_m1zW|k_4n~U}%K@xf2rx57 zx2&Vjt?G%#7AsG;V5B70m&ABIfWR50q#PetZpw6`;3t35an`@lkk#nxoNje?4!RyjVG;=gjtGLC=Ct*M}*V*H(i&!yP0 z82&4^%J6iOd^=Tsi{V+B?CduSk!$?FKmP#?6x$Zp@=MEFr)M>15VJcGZMNU{H``mk-ge{Yb$0E>(M|2h5rbG~8`7)cPIgxp&aawr z0)fZ{s;^ZoY{Xj_vA8p0#IUg%etk9coxO#1cdgxShWxWNWb}6Sb|{i;D@Onix~AfD z1Rv($r!_}?hH+rb4)aqS1Z^~IVa+*JyazfEv~J|5`-5Lvy8te#(+b>mE)ag9{~V=w z#QZwwpAmQJ(q$X`PFuZ+dI^}4v$EMq5PSw}f3ymXFsGB>WBCeoh7f%j() zW)Hst;YZjv{2Fr>Sdo71=6b!k@x@Xg*UwXLz3hv``#v8>20w$+XAPehx6 z{(O?MtVbSv_ipw-4}Uuv`*4!k>3BYn`P2}PBHDtt%W21VB$^8&aE^KZV;0>-{8t#M zRH^1>A!vzu1xUtL&uktohsOJwk}Km~vzn()jh3~PO+jFxG+tajZNL75We`W}#4CKt zOGBv>;`l+miu#Hw{vX?Q?nx)@^Vvz)JTd0GpDbDa-E*Fuck)?REI4J$jc1*<>m?7J z{%+=?vtRz<(`Wy4(_23{^v#3Ly=?J==l%Hdea;{Dm-{Yw{FYx`c-wm;e)QQFBQO4P z`1dYex!{V6ufBEFkE;)>za)BL(NF*M(Jhx=dD|VAeHj_K_Ns@USUc>=nkzoqb@dgm zEEs-e?`5Z4-E`LtKfC?Ghp%Zm_VpX87M*zGnu~vNa|S)crJ&5pbL z`l?ks+&cQ6_ijD8v-Gz2Kdip}@)eEiCe2;CZtdllu3PfhGwY6cZR4FczVpSMOOJf& z?oVq*0G_UVY)->IZh;@PjX}yZ?ue-1<3YVA{qfENmp*pUFK>RV_N;pz`~IqPpPah<%0C^yXz0^# zy?DdZvqoCa{Cz^fv$4mXc((MDvgaNjzVP`2Td#fo-4B29{F_(zz4UPL!GC_K{QXzT zuIPPj`KGP@vgMU4|2pa9Gv25yKJ(2NI@i3l;K_gfZI6Sl`ukPq?zQPpLtomoW7{Eb zk9_X(x9^?v$=m0*SnvMhXC?3Mck80}o*&!$Uj53K-@o+J-9OlA-lre@cK&7m*!bYj zKE2}pr$5{7nnyqHeRtCrf9N~m%X`P%|K-Vhbq_st-&I4OI{mewr=9zkt%mLKqpjDS z{{OM}Ch&Dt<=+3^m9bD{RAx>~+jPolIuDf6G))KS5RwiQiY3EIdT5#xbCS|vK@bE* zKtMr|%j|&2MI4w^ydVgIfFdZ9ih=`T5hpGvDF5$ot-bberliRIzwiBgUbIQh*=s#( zt!F*+TKjp{?z-;z^M~Ga^W{UYI5xA}&-Z$Jw~^~F-hI)2FYUhNjj6*9U(zyc+h0!K z^N%-f+w1YYoA!U;fuA2ZJA2VVuUB^;a>HG>AJ&`s&S5j&JTf)9VQcE@7cLn7vu7ud z7;@czj_~%m;qaR`j~n^udF`Vn&0SEnb=>SRyMO+waow4<;}0LRXZ3$CdwSxRcK%*X z>Xm1wzV%;6O*`cE)2FT6@U>}somxHPmB-JV{YGom5fg999C2&c^f_BwSIn7l?*a23 zylIcxWl!F{X!!BJS^WK9o^|vgZ@;kgoc=Gb_`@DAuXy6`S2cd*^fgUWvez|@{Kg}z z?poEg=K0^;vS#=G^=sGtCA;>wH|5uU_T<~vp1;qnC(PRV#0Rea-rBYiV=v5{J!9vN zou2OQ*f!^^4XIDf&R%|5+s2kxFWGop|5FO6mY$t=xUHxC zrL%iKnd#en;SutjDtMy!+X&F0MGj#aBkU$SPvnX4t@^@Ml14r z2r_!ZzMi+`aL>DXJmZb@ypPj&F~OUk^6&a;&)bX7YxeWJcE%n;+qam{dgjy3I8V@L zNB+JRIKzQ)^Lst-LC^F4Yog~(R zd~kPg4N(lff0B8A2i(2LT6V2MhO(C1na81gcO?RTGHsq_+}(gTdo16xrr*-$0Os%l z<4u4n^g#{x9=y%>>}`d?j;#fbq@*=O1SMj{{>dDtia|)bej3b9*nJKf!z+qyNfX zXa~#&;2}%!cF|Sr>l*O*an`V!^?VxGud#Q(XWp~G@zeCrGu92-OXjvWZO`Jn-DtZH z_{f8=r-1npXyse1c`Ez*5HK2naR+<5GkEV94?Qrq6+3#~cyQdvcenEQ!~Fg(boh$FnzZKsTGQGl1%*bINvhzODS^k`|3RZeYw?PcjaI~B5M$Y+(X}i?FyuZfoFd0pZogdZdcIZ2qTf4?rt|0X zNG0vt_-PR-@A+gp%6nc3E(A&u75jW4pRHeh3hjD8`B@q_$JT)?A~L2dD92qyq0w; z$|RKRPS=IWuG%EQwu@~m*xMc`aRCd-(_I0w{xEp)UO+W&FDM(iu-l{c_ZS+94?hX8 z^XhmYy0UwlW!z3?u->4o$B>4?4tiR;<&gjxQ~J^Rq7lDB?$RwJOy-n+ zw4P{Y#`_?9tj+5!1K<~xrn-ub3Msh|&wCJSlNFg$8PEg;pk+yCRQXS z6SXy&F*ci@O*9=^&yZSnk9V>Lj6SaSiuq2YELqh}e=(Hyt%q*&lHGdEBfImK&*BoI z2_icN5OL1D3?S(^kUAFK#s4hx2uK^8k!KZ-!OuhVuN$a~xUb7Bq?IV3;&*o9eT<#~ z8FnFA!@sQs13bn6tIG{w!?O0L1Z`))OHR}{(dc$FZ}_^GNhp~22~^F(%Ak+)B3UM( z+{waSWL9k46OhZ_096)q$FOA0O_+hX8A}>tB}60aST1(US^_o;XY&Z) z77P|#VdwZ#rlDx(R05<(rFO6oh(oq4%Bkf56pJG*rrV(PCv;pK@3@eWggOzpXxDPR z9leofB1iF}8`;UX$>3{$Y?{Q5EN0TZjHGFc@Fbwu^U1P?204c=iHVo$BB8hp0FnfX z)2riGv1t+$*P0?*p$>WzsmPkJ#}IjAO$h6vMM;AWmSJV@GZ;NJaV!)9vtwJ{(KDT{ z9ERmJy?m!6&n83C=g~E>-^uX`cUh`QT&tx>UiEtq10W%>ma(FU&aLVdP>E#e6}zFY z(`{ya$~qO0Udqz6Be6}EJgtRb5x5VQ>bPX*IB%^4i168lbs~mcNb>|S6z*W*D)ZiT zbV;D4uuI&GDK=p8-V*>gDL%#Mq(H(ugNjjvzl{}(w&(NrDpuYCdU9ZQqj^@#?%EYE z;M-GpuBVa=7EAkL;U;D&R7Eyx-Y6?)7)4wjP3t58Bxv3;sF)+#3(Z{Sh<4ra+TC$Z%Y*}vO%r6Qg+xs)F-Q@#0k?~e z{hX&~*eFLX92plcA`TtcgzzrRI? z1^S@%B|FI3vl_`RQp#G`v7$0L=e+j%A+1vIcdG%z@-Q7Bi}D3yA8I zv@bQH#Yq1pV=0ROdUPeAdKOt^$;C)sw4329%VoUV05_)+IJ>LzWn`;J03}=h0RRTg zNkGL*QQv6cNqjB^`0DKguJu@ovP2^BXQM&MUH&|v=T`z< zD8R)pmi*%e%rivv#wEcd7o3dK>_iYWrRLF!YoVBU*edT$q0z#^MlyC}PDrElhuoq_ zkffQFFS1>e2wVZE_a#9&zQ2t}>Exq$NNqK6-YA?Rj~Jkm+;c_2~qJ-SRz#-qylMYXKPjr2!5 zQak`C{MksA@MuGu3Qszrwr2|4I2nI%(Co%W9@sLPGO1*2C20{l>6%AiID=^;-BP6_ zpw&?AY;A{kEsYMfjpDv$9L~RJGC^)mH{@VXf5Z~H78_S6QHu$~xzZYabjw;F2+3%M z4K$p*0@0+ovnK5cE7z^x|B)O^xqlaw2O0qzMWSy~f$Nb9+?O^s$r{0RbXi)tOX+BJ z+%t%yD+9pe|2=^5c)XPr(QbSW@QszgyM;Mcv<)m4uFV19$Ehj zU26u{b){hfsj(eKVscmNB&4~0vwYuU?4Xtc>AnwuDiUicK$WQJ+W?$jX=cf1q8p1? zSvK~rx8kfH9EIJQY?|b|_(6)bVJ#!OMMpyUT0o7f3@UC@JxTM?$>!y>#ouXKuvV+r zOS=ssVX4ZEL{xO>=UnLXN24<1BZ^p~R{^wrRtLrfmfA}o-b?5G-G@T)y^V)c%RP)l zciefGtJ3Q2!36?3#iEgi6yWo1|hm zlK~nl4Uk+`vZWo|GG)>=e-W?&(ZoeTME6OWE?oAHCU8wDIaZRw93}4j3vsrEe=$1?pqwyzA0VB3~KcVZwfx3$2+Ua6V`8c3FV#x9| z=RjQ3YP6u_q)EU_)b-F={HL%|bP;70&up6rKSyU#L@qKiq#Z4%CRtHRB)ZMEjqd5fab*hWyvZ6qtp#UI&(18dU6s2O z()hSpC5W^#h={^pqH__dv`)frAguS zhi-r!_ih0z#LtZYZ+y3aN0|E~V2jwhGy=r*I+bbj4mg7M{N|E+s++`S8%WWXOPN9* zWK&Xg=u}6R%uSRjqCRw{jYkybNU0c3SL(hSl?6H5mLnVJcJ)vDt5$( zu*yJGo>iMLxH(OxSrzmpO*74!J36ww#FtAor>bnWNG8#y8RS}c8L*>SdE}T!O|5B2 za>b0QTC2`vu$t!h0x71c?^%jpO{Nv{v;cWi3w73m#gqC;WlX3MWKw2jJF~M>G*A20 zZsN8%(^1irvdzaXt!nP+Fg3?=W`*5-_k9h_lIl!F`8qpOxzy%9GEAraMNFhS+u3aK z(gwoy3TmdTRqYg0?EGwbhEPkrQ`j6Why8J6LK ztpPvOdl5MFO&1s%gP7jwkbd-6q#J5K^o1j!&Yyxs5f9Q6~pIR}7s* zkR3$`$dS#I+NhZnd8KMQI~!EyxwUs8s8n*-JQ)W!j^X=J35X(>r-fbO1bU%r(DxM%`Zent3!7>1yCD{>XCDMnx`q{lxy6>&>gb~x?O zM9Jo!K9lJ+9pHCKmqVB)SJ*>ozT1irxP1dT_;y3ejWRPBMS>_7<3uHn+?;Le)4Is4 zU{oF0g}(cYP)hr?Mf0;lm>2F5gH_os%AF}A1KAjmuDw$8P^apf_B8j3d`U&UL_AE= zNbxPAI6fFJ^uz4UT^V+DpsiNA*#S4(zfonlr6P=!$r!;=l|$kM3T^~iYJ`*p3l2;7 zj99#KWy1)oo-MgB7W|6ZNZIl}*_4_zkxw7l*=HnTtiPU3kO=5PE#-Nd4JDT-#-Jz1 zjJnA=Ai5jTtnBPdP*~I*$<(RHiJ_bP2y$npMo5f}u=bL0=F+>FA|7@Eo!~bs+af3! zqTR|;wij|NNj)<#6zh@WcvS|NotMT2M#*NaQEz7I7PURhWl@Bz_^c zOh+JrjD0~CihNFA_DL)`g_2zo)!0CJ8@UZ_Sp8=9FGT`+sfTD3rwdB!Jt#Ra8fB4S zbZaQtqjOQW#Wsi3Q>gr6`Yc>ivzzMpsDPn#m$KIe0wJ1O*!V!F7X=3v2vPteXwv>9 zY!8x}qG7NSjoc1uWd&`mJaW3sbXogOw7}p*L(Q==udo!!7HpOk#zY)meNAb zG)pVFabTIE#Sp}1fyYVN;!Y;^N+lG!a+?gsER|s;poMI|?i}1KC4l2&lwWfo(_>a~ zXQrdMwO`*$hnW4NY|pebWe%D{32PEW2lW%}KIQ!vh8U_39pU)As&H0if{P^@kNZ5b8$kZzUG!Kg8`971UCGbZM+p`Zo1f!`Z5xRt@2 zluE${W&?i?546EjNn-{Lo1rFy{Bx`3WNb$sq9rV7%DVZ~o=uqcfjDE-3f8*3|Ik`D z`)I9t5JJb$BnXygd?L!e_u;_hMBEq|X@8_+ENBLU+&BrrYQ&~C?%K3tDz+YYgwW}%dg)?pY(nKJjoRUx{gd(L-5F!c^(%q^AryI0!dSudyR1Ioc5OZ9d1sgiGVYSQ%d3^O^C&HU6;#e=1JZc2#> zy1f%$2`?^g5Sj;{@!^dy{@@bi=Sh%XMRhjSoiL=Jy${WCnlm#O^CFxhS0fih`@o7d zCqs8GvLBjd0{2K*BD(-gufX82vXrn$AU)ZyD)D=;muyeuqSzQ3Xb5QFcE4Op=M%yo z6EOG~4@qg6+C&5_ndGfY$9$mUC31}jq)bQ={K{hf<4IHNiwHtK3nolR%~=7>sWGj1 z48~hw)4TgRQAW}Yj3Rpq6~Bop8<`0)o@5@@h9C?udJywQfcWVX)}EMZy6@)H3AOe& zesLe(Tk3@6C#II?QYS1mzk+X%@>e=>SHu&z)Go>@8)E~diZpSjF09Ix1TYa{9vODG zR_nBtCX$#-P115~)Dr$G$G_>+7|=e}pWh_WD8v!QG7zk^4q&K`T16O>fHA58=4^(g z6AZXr6CX0W3aPX3aKI0vf|dV)C3)WPmoWt@e&#JX>8LdIB(8)w$rw{eV~&KxX@ z4uVENDJGbh(9Eulz;do5m$G4fw0SnQI-5tlrkZ;D+OqU(mrymVobT&o-!Ui`$)-{m zG8$dgvpO@z$_a%J))ghPkPeZ23T#KwYK5f)=0N4ZsV$9YWMhLf08JU#n1Pn|#|L60 znt~x#I{`QS;LyV#PgJ8U@!BncLJY^J{R1K9*`cJ5yVq2+s|^YaF0mXbY&CZVYBOf) z1*%q7a1|oe)+99Wj{xtToh>X3HRqs3048(h;&&~FADq*{TH>yf5vx`XMXEL)Hx7g2 zk#?$UvRI3gzPi9R0Z}L1 zs+byeG=D&_#%Qaouz}CsXKFAT%Lhfn83}q@lCGnKs<*z53!^$sFP&#t-wCU@P#`84 zsbI;#XbNUg)Q*LhdJrNeqHgfyudY*3xVZSkPEpXEkh7_b#3_yt5JYjAF$&b!x9I+$ zR47U#hmm8fcjF=>d&XcS7l1Lnq&ncvV9;JRX=L#XWY3BX%@i$;W|wc(K}iW&R|*cf zC1>M($CmG_7+Jh&IWr|=D_Mn-!oS!L!afjdc(h?`Y8+uhad!2n1jyNK|TCuT0J z1^r+?#R6H1D^?pkz#0LsxX*UU)$VI#rF@MUlJ}u|Z|JOG5(z0C7-taXw2GTWOi8DC z*8h#~+_*-lxTh7doWQ(Os8&4%q_?aQ=g>OQXb$29n^0e^CQKEW-Lo+xjhPkKq0Zv9 z6UU+pWZ6yry9zHjc8f`tU{X$iN&R7}7<=8;sd7P#g!Lbig49v~f|~EEObB~faE51A zp)=@6K1rN93B_q7NQOxm)5Qqg5lwim5*+m#;ch@3vD`{2@mIpWc2+a$E6<4{^U}eI zji3hD^$!lFsxs(?-3+zbG1C5&5;k8Vh8G%;!cwr`>w@`2YAkjT>7=!0fmj#WIvZ4M z652;)R7KjK;DoH}7RXp*embrQVsL4HM1t%c(BQTLF%I2_n8qZGdA!_QVz52J?NsO} zj^9wEk6ID9JV|-m*G_>wcqR~G{-hJ^?q*Lku^5EDHE5k$L5N5l$ogiJ@L33EqLSQ| z3#hOqmnTd_du^~dPKWZk34o6yV;VOHhT4ozW&=r8whtwXptF4gGB$F_SOIcGbWUu^ zC3GVolE6nWM0F%BATb*~Wp2@_pmtiv&s={fv`ixMUe2&`4^`)iqgWAU0tmEN4hB?e zkKc`U4`^zk+aqZ@8H3in2X6shayl6c;ItWz+uQ&`}(aq(n&BI?L55&qd{s7e8LsP8!R$ zc$00cA#N!V3c-Fuo|qdwL4-RDDC&lZcr6y!u1d;pe@a~t!o~6*ZEQB9W^5ov;vrlW z7eB0a%lc#MSWp?NF)SC!k(mDhIzT4U$I2wbEg)rqCEfVpPP|!Qf*J7>_GRaE9&fLw ztXf`|_CM(-H-Ie)&!g;FLLhY!46xq+e7xU3Zn?d511{v%rc=gK&}Ic88wspXOQ8q2 z=oBIwGD}8s4Y6AH$e%4)5m*s1FD4$dE92>;{VNQf`tlphwK0?chd0P}lp^<-?2$q` zc?rphWveTc+Mkk_A~|P+ieRSH)S&&$wumF#Wdchh)$XKnj%|{k>sSu~WZHH!p|szy zG!Zkmbb-5Sug5Tovxp6!Mj|+T?rIJ!<7|&xiD`cwL}BjMBsx0{ZcC#Yfg?H2#)O9e z3X)+QEAHwpWDtzzm<&xF8nXZLi9kii(u6^+ZQl@B{0noP1p5v3#VIMxMjKOLII_=*Avn6S+MEc#p-?>MaJCY;ak;3~h=4-mr~Q}xMt2M&zbM|?3R!2G zrPG`YiVnDjXioJ)-#KNY;Ts4PhGKz`nLEEuYVZH)h?|`>tb(E`jJ)}i0^22`0%K!a zf#J3y6nm&QqNxL*M<|7SLX0Zb!X2vtdWE4?2@NwaDmfmg!dKXC5N705ZFpON-LMke zL~xBRV-x*qgjaC)x3*zPp!PzWKi}M`yT8V+G0_W~9uX)ToqTR{PqVHJZ_W^p zCNdj82Gs>?xjvxGY6lB3l6b?jXxtWR^cBBXL#H=#HE0a77TU4H#~Z4c!zGe&yJ-vq^##O(;8u1XBjBKUJ&- z;sz~UN~;0xWCd|ZFe|xNY~NBhyF=o($4OIRzSyj-ibAgT;1ny~MO7lH6rU8^P$Zc| zIKh$*tP3m?DkTL6*Fn&^%0F;L;M+&f!b99iroU4mX#Mgp5IFmHMjf|ndUD)zG+Zrv zom(1Rx&>;kiDJ6MZFWO=MkbB1T2gUlPF^@gu~9xW3qrcp-b=M|bN{F%Eajky*vfCT z=rlZy46u?#2eVIlYsEqYOETb8ch{^rSQBE$aCvFeN0U*|+uVR7iqswx^g{ZW@tciC7vc2Y=0th?Sx4GMzzj6k|Zkzh_#sM^`qX3ED z;%HIYAJrJNlVCKH;J~3zk|=OGHJ7x1Y^3Wfd&FdS;If7%8x0uYnV#OPs?kS_4mK$` z#RX)&1r#U<%rON%2tiBnhvF6Aw<|RLjIhy8hd>|(UAqjBX90pjXk1&}#9KVeRNG!4 z7>8yi*m#~_YpA42pv7@6r9jl z%Unn5JkuqQj?Mw@CwJz$IygS)#bdhAcZZ0~CcV6VWolt~#d2zTdU`T@$qX4+q$3d< z%-2wC>Dm5U6Z(j8FP#s=T0=DCgv7X|O=4yctyf4q zWn7Sz#h<`-IbDdbksbqnA84pI2~TKaTzPA0``$%%5@P`l?>NT z6b4sYOem812fGl+`X-{H+&687&A;V4S>H-O4L@!?xrtO7!S~!;ybGd+Lc#d82 zR9x~iw3q;rh~pwd%>;~#WqckCNhv*~3#jjI?jQ`Hg`m86@yQtEI`Nx5aUxw=zwWt0 zO^2*9W@eey`5oIK!$Lt9sZ<3+IvLC$hV@ua#3-f$DK|a7DQOTQ|r$C;S(+AUio0^y!$@>#`#ZwXwC&6-S4&w z=YQ$4Po948=!+Kre)>iE58rw5==&#Mdg#%)%erRYc-fwNJb2m2M*s3>&tG--YZRxP9OJ;-i1Q>85`*pLug;?Z#W`>(9OQ2XijD^{Hd-x^?N?F<(9V z15bW+$qUbY^|9+$e(kE8SKt23shxLx;1}23@$r8wyzAQ+|L(3G@1Fau)9>v2_L}8S zeS77_6YrjI&S`h=@aDMhoU>-ucb@)H(|4}lbjE)zXg&11H@xqj`yTtq;rHJ&YyAB? z=a0YtyT9zYf921Xeee1=fAzh(Rj>bG*q0xAbn2tt4_|oSTR(c~-0sI;tV=zy|1X|; zV%g8ypFH!!O;0^|PTy0fUitddKfM0xXYZK1b%}dU)VDL z{1r^8xBc#$@}d3iyCCzvn=k2k--$zh`2HXFy>`IXUo{^1`|dXn`b+wngZsCA=Fm~| zewBK7&dlLI>$_q^?eN1#UOByCCvx@IUxJe zm~|goHg@LLZDVJB+#C0;h8@Qra{t;1|N7nY6V}%cuYU0KhU&l6UXWgL-*FSK-FnmqQ<%ci}#(}6RVWKNi|>%GU#y!^t=v%YcECuY4^`@ti>nm^;nRSylD z|EtGR3l>h8wP5z|udV;`#LE|d{f_@!w)cd;EZgy*iz zh97UcyWzEEziU`9<@jUA{QiMsSG=}+W9RVyZk+nXwx+Z1uUfTJd&la>k9lKF`_Fb> zdsbWagl|4_@Vck}wQBvPE2p2d@b<>$OaJ;x^F!aipyjYz?rf<~t!};T>)&om{q#F+ zcfMAi`PJonW~aUKsq8nOz9zftyv--ye8MMAzJF|A=jS$D(D~wu_jY~s!DoA(o%Y9` zC%T8_uY9y6zsD_S_trOlx$lU_=4{^ofp2a;`ST~9GWXkG|Io)z`qpXZUVH56Ypz;( zxNAqaargbGTR4N-m*l^%C(*=-B)&e0)Ty7P29W-{i6TOqc=kH&&LUM% zBYDrB*vs=y;NMl$XnKnF%{x=?F-48W_j}%q_jul=WRTfFLbw`A#=MU)Ch=?%Z4agG z&EzY5l>8un0?x-6??;Tambp{`zjLVP{gJ-EVLmzfK7KfLD#EF9)}dDis$By!XNhl2Fp%fNbtwKP?+ z?mcP8n(ktp=fTkrcs`st-bHe^pYG#%s$unW+WeHRqZ#u-@bobAe-)e@1iYKyOF!1K zv(Gp~SdZ4dFLh3t>rCeLPv-C}-#<&+U(xnh@OB`$>StbiR0A7a&Zqqfek+yn0&urG zIDKpkH9whWKQNZ5fe-EnT^zQu!#+v#lYP_x1tP z6DoI6{wSN7PzhnPbT}@a!MBbz*o5cpCP9zQs+2FtyW|oJx%@1p7w`puuY6~~yF6uo z>o1cR_#prv|IPtNX`f2ZyZSxI+;;+A>9ADAr8L-!0o$_uU<)%a4McBuFw8sHQVpY2 zeXgQt=`}q0&eqUG3M3&XL$+jBWXTr9C_Rjilzayp91XMFutm9@s0I9%?ca$Ad?B)w zGfVJnAqtm)xb)giWALThKX@c93Yq>D03QAC08EMmRmYw^7;vSRcMjbTOp4Ta^3G>@ zY^vr)*-;lDXlx2M<;hBxsC$j(^ODfiT!PaU?$S;u`V=!$nHYtJ2pE+_z^L|W>i z1is4BIItK$2Ox8kGf8GjE36WuZPGlVyo)~u#Ks+RnE+Km z#_G-F{=GuegBP2fDy-R7YvkG&Fwx5tbt^siujw$lG{j}hS_ReHn2O4W9)a0Cy>!zE zNu@_QhpsgPbuHYO=joIls8e!N_Q!)5QM!wYe=7H+tewMYR%X$*nUWr7*tlMAB28zP zhp2>^%J=WoQgXZU-VC~rDc8Ml!OJR5E1t+V@2#e3nSEB##6IMf>V}Apti5#-9m--R z$~X9>O3k8OzKv#Owq`vg!Q%dnEd|)=J+B!{a~4+*EKM{2O^4~FJ0wRacDE zYa#Ye#%ncI*NP_?jh7_1Tj?@a;|R~zut*YsR+db0TbPvcWk5_G2t@G;=imx2uhdPe zYlSfTVBbtZI?&Sio=@asK_!sI=~hbv`p&pX2rLa~G_eA+jpfiQtt1P@ zYA$hh#=A-Zol47)eElF}TjNvvrTw$1rxy?@sJNk0WASn`aT`_wyWjf?AS%{>jEJf9 zs$T+tq5E>HjsO&r@NERhOkvG|yCw;F527dYDj_Xtc$S{{On}UH6$X-gV3ku6NWP3Z zTo0Hs#Rp4|`+GV}Ej_N56mfyNO9co!N*VhS@t`tQ5ld0l1ckOrk2A1!(?Mqn#@WG> z$cgr|^!gN*s~MjGA+s|3XLrukm2+TAPqzv%vz@zNT)9ymrgfJ~ zci%*JR%@dL#obGH`U;)c$#R_vk+ULYcCuXa%Io_9oyU|PD~Z$H2v-_pbOcq$MSIq= zOS)>J)OPb^6PmsJ)fF z`yNj0HQCLXD&0I9mwcr8cA2Un;vQG$A!~~4+(PqIvU!|jq*435muM80<|@Ymgxm=l?sv zEw41Eq?DF^ZZ{Mroh%A2ENqSDs3Ouo5XeXgCR?f?MBD_?Q$=4c253bZ8K~~v&@osK z$+42#Ef&(=MIL<;lhjiJM8&Na2wNPZ7VOC8?1%hC06(W|#Ugg`HCHmb8aaAdU1=F{ zKu!d?z-!)1)|O1_{D%Nt5ktvgWg*1iZO=301(oKrrGTn9 zn}Myp?+*;3kL^|@X-k<|{234hW$%&;iq{8BF5C)vt1woKAAs}YV9H_Sn*f=e93wdw z@s2Fu(g^+VgaJ5cAXDcB0GJAUQMfQUegq(?XD3$=u==<%bUY#F69_YgUa!JrK7Xk=!{G&-MJsVBa>8xg9ifC!7(<3T?&wb zA5p2o(u=r~?ug}5-B+0%Qn00T*Ei@|rm|wxwb1H0X%g1^K;tDjpkXq$hID6SVJ2Cp z^z=@l+w@A^;)bLx57TG#WyZUVt`+MLS|fR5SY=LvMT+({I@VMg({+^UcO_>&%0Yg` zP7U5GrN;WNy@rX`ewhWY(=HX>RGFyW9i||d^&2(*jo3?MzFZ;6f$@6cY?Ssu zUCg4s?2wdh5VW@ueJ@+|vGf)*aXa z813dl7hG9=(4<9yz=d3iems?W`m1?3$3LWb1PubucbCPQb(n`K229FqsmV?E3)1_^@R*lYmO^&>!f4s4CSViXoKldS{~%Ri(N{yHY5s zenTfjRpm~}tsL9LO9JuuSKRTK#lyCUiWGZrf>66#C~hLDrgEIUA@!+WBh(^REWO&3 z7-;GC3}mT6_HJo__X4nU@Bm927u(}i?79-4Yw!b>p7mmYY#2O{((}_U48}%1gTWZk ziHLzVw=yfg46yCkz!m29oTZ`CsMviK?#bY1r^tk^;pUAUWRFEk1MUFuaf1ge7dE=@ zEAiZBgmP)%4+4C_;DMLtniT~V9uI8G#C(KaFI>v>3cJhBR^bl2f05^yQs_21VJc?PH{ zKS1eOed+@Ul=7XNHkQ?*W25p1yLSO-YI#8AH+F~hI2nWKn3Oy}I*EeY-!y3?$iGX|uB6!E&24Sf!K1YQDL+i0K&}gyO)ZfwX+kQu zndA)01R2Y<6?`m6l}e^S$|ssiAxfs8WKgEoH;6G=wFkFg|L)kVgwrtPixTjJ2yP=y zh8e)r2v@F-|6At2GROqq)<$ml99yIE=hZ8MTy?Fp*R?eFtQ$6T*wBjY)|qCk*CdBp z=N~aA_*IyODMs$ioODXt7+P^8ADqoF)ud)AWN6cAD&B3jZ*;c!bt#&9Q--OouiGCvpSDJDWbc#Bwy{1BxEsBFkBS%^^5&kyd zNmmu7HV=}C*<^o+4HBLD2h?rIlBg}El&k0IksF4@S@b7IV*izR6~(^IQ0uc284+drCpU^nS@r7qs$-SZ_*?37jrQg zR3#Oq;t^*@*Q@-G5#Cm&DdAgzwlvw~ZSLlb8Y6=YY7N?#ouw3+UVrZ$9F<|_!ma`7R$|_2q>K5Boh$?0N3(%Ho z-Yhdkf|8;c`878(Z$3{lL36^8x ziNIrki{O&CE`}9TL`FLj@Ut5;EFgr|Jto07QH3DOaYo;fV6Ts~h?e4QUe_}EerrWw zrWO>u%-YBXXX`IwZbqhilGCj8AP1i<)sxCmx24OZQi~NeL~xLPlFLw^4{MZXdPRdTYys9^z{vmD~?-xWRQMu(&QO1Oig-{87bBP6Sb+n@yIr1 z@XzMfL37aUu%QtI>7;_ingRW~Rrpj097FJ}R7uDxk*1VRwL=s2R_roBuT2q{gp~FV znNw{NP(zs%5y%`F^xSGb%_Up>Qez0mQg=(-IUd*C3>E^OG}KBZU8Pkkg|X)BPA7>WNosHMbDEX9vgd(*Uzhk&b}9yW@BX= zvY>z=g-t*M&LzJ`31?L^!Dc!4Z}*+!mSpoWFr|@lR@_r4WFjcDCGA(xl_E1p zgw1r6Ufd{!DCcAVntkf5V(X}UPbc&c;tF?OKc@*JwcAYG7HM+nfL-AL zmmb}kMw0SjqVTl8D%uCp5|OUBDnl(0(xM(D6aZo@d{e$!=~#n`BrK=RM6o0aWwc`Z zE^(7802s|pIfV^sV>5@)mF-o@q&GG9Y$T~~7o}?D21*67k%sr2i6@mrF&|`-sEC@+ zMbnSZI#6Jtg-Gp0Oa2=&o!yb|qE8fp&Tk06I!b@(8|%PSALZ|QLNJ9B{hIJe50S@GQfVc@_m z-$@8ugrRoC98ml*dHhQ-M#*vk4dJCupe$KF&Vu_TTPiO&$-blqiLJA!uR9`twX6^mUC)Pi$Ig6U9bF#i|YqsxF0vURB$rN|Sw|F_JXh0((gG zWHW@jlIe-E2jPZC&_R^lea+iwhf?<7=X|Uo8y70_gyJC;VJx5pVz*&Awf4^D4uee= z5{7(p&i|-Xw#gWH8AHN_9$1lfh60$SDbpd$YwOr&W{q_sg~aEW-5{2OnN2Jx7vO;A zswGBqSCr3reO4oe?a^nAk#)^zp$RjCvEWxHgJrK*i2M(X-wf*CJq+o~!GD{~EjCB_aT zRAMJGqgKZLj?Z4$mR-o|j0)25lV)7EdVWBq02()3y!tb8UW?w70re;Lz2C(x&{=X%q=d**7 zBzPXscxX|&uu2>YM*fzNMTL z!rmM-`{N_|3yTQTX!PXik5!J^(r>Y@9RUJ-M1tnaKX=Ao33@VzIiXG&!8DyRY>RK*jFCpO^S}X(2 zCzN>UI`j-n6%SLH$+$!&R+;uswUWuqAde-{nW}E>%*1WH;E^m`TlT$N3r#4+@qfNI zl3YROf;#cGl|Q%M;^Up-cIh0=cWWv(wXLbbp|%=C(W<+%k6IUby9WG!yG|aMDWB+n zV0o^Nld_Dg2v;11Eo;+f#4i-XA(^e3Nvx0PM{Y4FFJ61&Y8{XuUZ1#~${SF*7Zt zmoRk7I7BShuttgXNq3q6Si(~xBHhD*YkMyWchf|ip=|F&Ly#QCm|G>(2C|U_ePNK6 zrC^Y0Qr(i-(9FR%$79C_QIaZTkt-IaxmN`d3M({$kVIW8Q4-azX=ygrx}7|MmySN! z4jr99OZ#({QD%xp30YyAy2yh*5fKW@8OA~mGwYl^h`5oQj)ekUr5mjb326)%!BIFj z?XQdk7)BKWM^R`3+iapSrEPU3EAJ^{3p*2DSDd0VgK-QgV7h?X2~v)x5EB%f_P^p= zeivFJF4d>t>WJvC#DmtnB0*&zq^jrZ1dB*@|E88)XP#5pu=Z676j*Xslp+>XLc^Q@o>{_50hcdF3{3R+hEkh>!E^y9}C1zcU{z*cFHG$Y? zu+I53vL_}N(imy+$v6|dC@|RD|XVh*iW<k5A0jZKW!oe? z@ADFT$&JpG{4`@LYp3*_a;2I*YbCIFwXS%P@UOYA6>g2ry?EJx)NuqD8$R(glaY z=_;ra`I=>81t$!eZWLQFVV5!_WAA~;#1CA&v_CQgo-hryOO6sSyiYBM?xedF)e?t~ zI6X4Pka4{TN{d}Gthc9wTPUd!M~p}?VR;6xO3W}~Wa;f#pn$sPDw}5RyE3bMUweC2 zG6?o@t2KJn>4MFQ$(0C2aY;v~229o#>9p|!LLtY7HL~#c{wX_~;Jme@$qFH*(%}ruq{_>DAz^^728UQe1xCA$dj(GDJEk#^Bl0)VME${(?Uh%7{a{~j9FtahE5M`mkWPn1BmBh!~`XEd#sx?;+A}LcG5=9p# z)O^~XUEki$h_X3ZtB!OayZ8!>d1KF$p#aoycVA134&KuK6Ml4)#3HK&20@K=b%bo_ zpkii-fy|9E<4cZd8f(@^WW7)Ipc6K%m~ay}?0T|X8*h^BN+d#}TxKYT;z3DmcVhzX z(EUz{28EQXnvEID94#)yb6oKxJj2>tWz6neZ%?MB#fH|~`(&?jM-3WV)5Hlz9zQa< zlj<9?qf`I_KzE~1AslXtvjhAkRvL9m`=O?(>dX=1@LyH^Cau@ja^>fJfi57r$oG8-xGcX*mV z+1vqQH37jyXheV}?-aP<{S#JVKEXt4fg>if#Soa=7v15q;3`aYN=;4JdCasF_6(lY z*tFkXNh(BpVXQ2BrzN~dnF$!oa)@Oy?Nhjcv5EPw_<@HcP9(${T&JH}Q!}M)#?0x} zt*ujLRP$+SbxV6&dv)v7_9>Z}nf6&NQ<`Hw&GvUrRda-CLK&3aZ0*bU<~HTcwfc$W zz?SEZ4PiUZv`pdcKFf3+Q+shCb&G9PAaHx~=EyRyhJM{v;AyfjP5fsp2paO%k zrqEilvt`LZ?pKakOUG9=)-G65SFfvBi|SXdToE2*rkTn{a{&TXe$;Yg00eFVv@8;V zmniWqF0V8Vf0^5Y0#-rlCXX=VehJ2{u8syCVf5!<&AoH|*d~VO78affB*k4IWBt0( zj9JaxI9*0J8e=E5YE1)jF{r%DrByIu&dmc4O;qAQ!)fvBA^*P~Ky`+4qrm?vurpnq zcL>)+i3dIsR*&*-}Y=s;(=RPXRZtvoS zFu=SDafcX|dNOBCg$+@VkZvmF%qu1Y@?qXG$4}~IPGg}C3OaXn7|REviR**1 zR_$9*GOeunQ%o|&HwzN6;PO`sm{i4MDu1|*e#9x+I@IQFg3hD;v&z^jz)oH#jE;O# z_kAqpAn6qzNq-X#3hovW9W)Bf*aYUBC~RPiqXOSuQZ~F%fKVhvB}QO#v=7-{-Dj*| zM@+6s`@=)tXQWc+P~pjJ9>1a0==(@(M&G3rljIC4+uL9IaYIigh-;!;n>S`s(*``C zy2Nj4?!;Ra0TtLQD8K^iD#Ana6;iXf1f$J zK+0U=zT|5@F}WUOvDjVL=m>;0Ev~JeJdIZ(^v9H$Qwb*OuW6Ge8`C6uPsrXHvU?LV zS?!Uj38FzPsgm#enPaw0df3ne6NXd+^>=c;y<}G>sE9Ij%LB`%su6DP{kiCsG0tBL zzh4@>W|sCpJ0yH9F4))DvWzbj1>l+Gv3+nb$ChS5ghbzcU&8`~6pXYD$TKl}u!2M} z?-mvhFt8MLoeNYiM6E+9or}SRu93ugyE$ku7pH>*CB17*aF_rKTVZ&Qt6sK~xe!$4 zR>KwKt|e$=c#C@~xU&lzLfh3TMA?Ti3`wY2HKWDBN9|d2MO+%zIVoxXjv>o~%U772 zrQZ5HEo%LEV~?p5{Ldkko<5OdU$JvH~@T9l05rbWLTJY8hIn5F6^1%HA9X# zIo0XnC5^8Q4s(fJD2KPDRk7n&oghT7HXs;BEbAW#XNG=byN@0wY)j~)Gh56W{|Axw zllM4s!@%vZr%;7-t9`6QX4XQgD6_cFzcjQWj8A0(Kky*J4y&tO;?%@@tsw>X*?2cM z#)-(-_>!Wqo0_wId9;~qHbo8W-+_3;;zMW2H4Rh@K~W%umINZeIYbec=p5JHY!u%) z99UD^#AS_q59~Wc3-Jzn*OZ$O=6PGQVw<`+9K92yW9a}?T5B&Sm*YY)WDg+%W-G%C za=i^JRFu^-7aHLLc0<_`gn@(AMhGZ4cHHLFFmFahZ)UiB12N_^Z(`TYUKc?MUc?U6 z_@Mf)!}adnu&y`vj={uaK#boq>#=j4Y#^uMUAAA9?(Du&6}5@-=0AR!sLc!*ef z=a$D9>Ac_!Ba?4#!$Z+c0^)AB`O8twP)m}3jBYV+;>r_L_*1W8#zZ=dM_9xu#VRiH zR}J!=-2v{@59+PmwErW&Ns(OL+_6?gfiohkhkPZ&18Ebg53KA4p7Wh?giTx*uG_>b zlf<|Y7HDOi5&+ul9=5Kt!4>dI!qRDA<<|rTxugpVwjvxekipyYV)VN)BmV&(TPM07 z6`zmwL;mM6ieLw~a45EV9%KETyyBhqKbKsHb3^PRV*Fi56j_l(?2U6n4W_rHsIY74 z>rX};rg_oTOncy0(Vy{`MjU0LxR5t@BYLWgMT-3)a*q*XFA;{?m=S3WOVDKvSaW<5 zzI_JO^3s?r^G)5B{)=CE;jE+J=;vLcZvP5DlEH!785eQJmt^AXcZgKCn#xgKWAjIS z>{V7dW@V~qoH(XQ{KcNrjFZbLP^@P9+@~e682(av(I0HKt9AQ;1j2E^gt zKExfMthWa!alTB=36Zwmu6})APp2-Q#&o=~mgYpFG7|vWnmQrj(nP*^qOmz~SvYR(Hr#Izo2&mfVn`)T5r~i}25Uw-P#@;QNXUUCV8up zTN%wn#@onLW$nv|7jt8Ah?+K#fhq0p?OZx6GkYMdd{Qp5k}z-;%?tNLCwj4za^dMd z`o(|UCpc=ai-Q`Ddup(Oeh|Sin_Jt+02eG?p_ClIDes&2LHbw04SD`GZn056W)sn&JJ~X7p8Ve zBUn3-+$l8-WkWn;Ks&)$=)&@}z4;Amkt8g4@`O~oxd}d2yk^YD=<1pQ70ff{PWI+D zX1LlpzR6iXbTr|}SbGy9ws!!`EwlMSpa5=*9k}o*3Of{hU){_jhthOgXQ#i%3A{2G zqUc0_RCKYyVmh*IqWf-Xe|^L_L|SsEtl~{cZewKH8gsc`nZJAtygqa2&jm<={e|&d z6X;da0OJ+M*bLsOjj~9F_M#CW5U3(lMvA(yul5w_Yo8?G==yJg!(TKFBg6B&Ic@GA zC%R9Mi=|?9CAAdHKPDw0$_)iZqqP)8x}=&z?i{7ggJ*UF( z?#Y=MmfE>;@N1XIZLrIep5%VC4Wu&JSRZ7f7E^KufB7EEB>{a_o48d^K4Of52R3n{ z4G?ZI!P5?VAUl{IjtS5m$Z<`$HNz4zTSA*V)*_9l>7BTFO1#}*27%16Q%HDJ#=A?xSr!B0hT`;A7+RRzgr!AK9I$K5bI%wCS^^OqnrtX8ol3 z3n$ghnpHn-%8a_H^)*vwnGA<6^cT?ahD`y%4KTsRNNYMbBKoSygbW*!ZiT`0%0PI~ zyZitym0@8IKUJh>oBF$2&55HdcM=i4`$-Z$D9Yy?R*W1oWC&x+XLg2L?9E+lmss)^ zj;Gmt^TLF2TbXHzdC)D;OY47vBGO9@q7lM zzsk~(6OMzhkAlDU`lTl+Z(_!!G7f)^i}Nc5+D!~(Z&%$Z8; zx}80yv8JoFSz^Q~3(#d2LXYB0+57+2W+zl;t)hUgQS6);wr0B(AJRb+8`6eGu^Qej zK<^L`ao~!yHfmLjUqYIu1x8f(GNRf#-eHXFeRm<`UB1C+dN`wj)0FmWoo;gpUgRRg zkr?0%zurWjim;OQm%Ck*e-*?)$e}fCXo>F;>do!ut-R6?Go9`Jbazu;hZV(l<-?b+ zk`$TtI~|6SAuAS84Us2tW20$`Tc#R=tZC+?PA%CC%u){BIiVoCfE835D@j5&{c3mZ$|fkoQ9%%)RV^$#d0^)qzt4o9mfO?J%5TC@F5DDE4Px znSg0AJ`C|dPk~J&iZoY4vVr1>&JK9N!up~Gwv)ApjCm0s7h8$do3p7hEh6I#3xWV4 zOEY3UG5>UG5zJ`cMh#vBEJRe^yuxN~UlAZpzVm}0?9h|6w5;DB#L3cR-%9LDJTMyU zgLzdd*a3^Qxc})k5te%>uzXBgQW;DAHuEZPCZBS;xgn{GksR(yOFUFH;@`C>j+nM^ z=8UPewF{@tm|r)4^7NUtlV(ksHe<^4sr54!&Yxd1Yr*vS3uo2OterKbe$wR0vu4%I zs;!$=S6er8`m~uflj|psNL3TNF)!%G41^L3<2V%z*xc+z5zI2n`*&9tSu7t0cjj97t^bD5WJiAc8uA~brg-n;Z+#B_@tpro31+0f^+Z2?Z-+BH zZzpdLKJ|Kg@zeK)dI$2`wmF2~Z}r%}DXpjfc-6hFuQM~(vuX@>GWRv}UbZ**s?O#r zTdL%QrctA7lg{?0qYf58QS0D~Qfz3T) zopG%<((`sW!}C7PRFIuOZehdb++Y*R!Cr zb6In?%iF{q7T&)OP~v%SkMdIUqNe}X{pA~a-XBsZ z`)}P2Q~7f>rP0>&vz}6CO_X(9LGiow{9aD2_Z6n;-8;PcFKzoCP+wTz! z)f`lnP?bq8hIF}UQ{zqbCV4gdZz>ge!2wly$I{o;y0g{yBpfsc)f??$Ce74sY6WsL z#M8!ion{s(Gg?RRZI)-P%y|=^yJ(f;^KweoETVm{m)2Chr3ef+R<~)_9~KbQ^JR?D z3e+6y%rR~+Ff=ojeAdtS%8&OB0G3o(79k`B`KBn#4EyZ?U&8{UXeDG2_nI3gh^&rF?Rxs86ezeHyF@vz~XE zHw!pR`BpRUG&ty{){$1%;cZ}jp7$kYJ$QVjX}!`^SQKtj=ATwBh`k11DTDJa)@b)t zB}-Kn&EnkdUxQiSieMh1G|kTKfj(`bmzUW0O6!~efB2p%cI8-mAA65z523{2c-~LF zwMA_UcP73sF@qr>m?mp(dt<%Vot!F*m9-^7sunP+-ayd&O&r%S+K z!HOnbx#CraVM9j-@Ea&{zSMt@ci&QfgB$Xf-jLzD`MXg+&$k9Q54zUzcZxqw;5jf)Di8THhN~`QE8~e+F#^`tKz${@BB> zenB!e`e>G;fsoFCkW$fh6Y|DfcKBz)~N6IbZe-l!5CdXIi$gWHu zm&7|Kq#F8^JAuUSD|Ie;WXp^h&C^<^O~-AYnyHyN>+s^i2zPMB$c;&{2I1x5U=kd%AG>_x^6M`D z+-+N~s6V#h%H~U}|KlgGef6sAZ@=s6w^IjPd&477Ts!p1h1b2d->KLAYWeQhZ@&8M z&#k=s%b&mF;YYr>@|53xdBLhPZr*zNS8l%a#2s(>`618W^5aju@wKgc?fZ=zw(NP^ z@O%Dr+gY2Y-u~ym*57f>`ldT4%|GGJYp=QL&NYueedpR2AN}^tfBe_CPdNUk-}%SF z$@kn|d-**>-g@NT))#(uZ~a5>zweW8f9b(bJ$BneJ^y^q4}SZX7k_ZUM}Pl==U+ME zk?i7&AGzejHy(NXwOxOB)aEmP`0ep0Jbu~NZhd^|C+>Ot!Yvm)Ipf6Ze{%Y&9iRIB z&%gZCkq3BB|6y#^Gi{GQ@yygWrak-P-B&y}zWv7MUj6G=o_pzY{l9pmX3;NyG4s#A zns(jh7f;-_)30~9e&cT@o%QkG)zzH$($6++{r&PM|Nip+N8Rv;8!kF%+fR1<#kRdV zj(+8UXRmqX-g$4la!ITA>fb&;`PIX3TlJ^sMsEI7!^Y?TeAPeR|ChZN|Kl&;T5|Q@ z9)0-p|G4hKr{3J{i$D73=2y4<>-+s5e*4}L559fYLESrk^sp^E{`A}zcRc5!U+*+@ z|Ih4v=ecWlUHAO?LvOnI@}XB8o7wH>d%eBe$n_WRzG%OfcHi>G)M1A&X&JWdFQ@PM z#~Zip_4wXR`#&<-Uuo-V2nHt@&HFfn17YzT|vy(>* zx$Zwlc>CON_|2QgjePXH_ED4OE~wf%ZuXenKY!J@?#$ZphmYB_`oEVwJ@HFBf3GI> z%Cl47`mdv=9rF6=(^hWy+O)k+t)B79<7du(qqXXYi8p1AxV3BgoUN@Z=FGVFfO!wz zv`6i-C+}V~{P^E2{{An|I{J{eUs!rh|Cd+%VUL$rJn{Fd8b5OSnx-k)>zYP>k7iP|$v2({x zPj`20n{(EN)Td@=FTbp9W6P_TY&@?2sf`cM-@R+|#BJSw+c58gzxYN=&(1sC*3PO8{cR z2zV#udC&2$hky5?Jodoc-G74TJwtqB&*7eT7oWdynCIniDug;Z_&ppLt;p{o z$mk9Gdft}9J@4xAj5pHrK2G1o1WA6%zw4_#Z!bQt+0XOZ8G8tA-(o)NnNK(4JVBow z`TJVn3JPzf%D>2w6)8=W$-3@rN$MQXE`Ymk^U=A-Z z{xrrvfOg+x?4JVrJjR#`zOQ4xm(X`BYZwabEqs0k{C;E$U)=U5{&sQ?{2N>^6aQ4#S6j&#JJ;9JIxR80{LMrf}y2-8HMt*kSM9CGF3#kIoZK4csZfP*d zZnbof-`Gs2{@4(CA}19`1zG|PT9`RXI)9d(4u=Uas;`n{KFI)6To zR5e>(xkaSB;*;qpuXrW65Gch`?DK_uw*K}hwCe%o4R^s z+TeUbc!vU{uWSSK@BZ&21_=ttoMJuTfb1&}eNAFVGM@#{36S-!D|Wee*r-Qoc{ z&nf+AJv^21KFA(x^Lonwc&pM>Ra#oXA{V-;L9|U)L{5=I5xk%(GU#H1rK5FqbQ_)Q z*2Id$iK4b9Gsb4~^K6<9t!GFryT`jm14bX$d&PVwQkJahroR|U`*suE<|Vr+7M5X1 z8$%Qy#wA1(M0O0%;+**;K+Vd^qBM-9TN$eO><_dv5|? zRdM!@pM#>?n6 z1j>#qT5YXsYqhRbtCyRE7OP!+wJ!DdeP+&_Gjo=kpuX?h|Nrx%kehpEo_U^Uo_*%b zFhcTB@uMmoN1R^*j`bS$JKcd!HB)tfA`Ebg-vBqEW4T$DB|}?e>XI-u!ZeE63^Khg z2NG1I8x5C|9vSqlnr+L-C>McnQIRDzE;GgDNr3X@+z60GxDjDCxHI5%hx?5_5@V6a z7z3dp?r<#@WRWX!V)FvP4QVSlXJ?s|csS^OkpNKzl4j5eL@;|L03`YWaGN6{rj)Vu zb#xpmcTC4f#5xjil3lL#eDo|lZ_>%oQsVb$Gnsd=rnQwJG=PZ+JB3D7AkiDwZHUofS zu`)oBE$7_z5>PV9O6pqlW3oXA79TkaFP1JC7AfwM`pW9|4fC&Brx}vlwJRZddvKJCvDGZ_* zic1^U`g~|#D!|fD^7S(@2)WUGEeZL6!U=jzND~(1(*@$hNP5CCvt#}}NNf}__5+~m zM+n^%C*s#F12Z$5A)UwUk647`M3No24rxqZnJ<2zyhHCh&|R5IzwRV%C0LRZVfa>b zjF!hGgTzE`B%o(ek4wLO$%et$5NnYPk!q!$5gFmnH{POvk8Ay-*S2Sm{D zHkvA8N`%-=xiF9;$OqhFbgaaBdH^~g#N`a+zAh4iK%|t(dKohpJW8`|Fn5gJGsVrz zaDY|34&mhuHW!2O?5-#BZN;pP1rD^VNH9r=ieSFT$JH@ZYS{kn~)Y6(W6rqSKm zScFETxik);$(OV5Li4P)Hy^?zkK0j7_X^C&o=gUT6jOFLDk(jLRP*$!0F+UvO347l zlJuD*Ex>TviVi@VM=rtu^7M(vF{+X7hziH0G(#0^+Eo_R;o;?e4};oY@wmrOj9UcG ztBDDp^9;Z(!*eoP1^AqWQpgbch3Hc|P_oB85+{n@m(GCYxJceRa+8IDnugX5T5h8~CpIS?~9F?fyk$}=OJ0eqwA$w84hT|$1(4GXi zev!cOS*2V?jYI&!Ke+?I?XXOH>kNyaVjK6E?Wh)vpd6M zI~!5pN#O)}luAoms1iihjN2EAT~!8l1*o5@poHG$oY&svrj!QruYvWKz%}B~k|(mkWM2 zFjxJ}SaiOCI?n<;VMk8k%LL(>sh*@_JsH1~|0e$|o9#1DmLAZyZWQ?uGQ8eKlURV? zUuKyCsKB@iZ>Waq@#1H6s_hJLpJL~3dS zyQXU^B+D$OkcnaqVm~=i!jVGI(Eu_s5=N?c+AYcO1OZe8bE4w%a{wG}H*<@C{mcnM z;3PzjFztpaengJh;DU@OiidbLll0n_Hxir*086G0C5XVd7;vXaMB0X%nmXrjFSpL>LdDaJGA5-wTF=+6!Qxz((F_(Z#}Q@RB&tsD0W^#yV(mJEYS&Yt zFqFRJW0H6fyc1nUM(*NWt(G$bdBAr7@V4&_{l>!b(XVZ5dudBNA8v) z7$QChX;3#TKMnxNZ395UFIU!ofv$0F>pF%h0lBeP&}m4dPQ)}ZZ(Q%oSR9n=u0I1*;+Gm;z5Sc+`;N)y#tKZ!%!sy0>iPF_giF)UYPL9}g@lj9Le zMg}FvR1cteSGBpHxA-lZI%ws7-8*d{5hhnT0~Hlo^b;xcyac#Rc|;Oc`&9s%KJEi$ zgGKGhAij>ymx^DU=KGRfFsgnJZPP6$@06@+u_(m0gT23U8qkthrYw(k-f@a|dI0iB3YD z(jBc4XQ)O@Ar}|FJbXiO01t-5TGn6CeSovOc&N(NP|PSL6r5fg_-)D92o#Bl6c&qx zOEOOskWn&6VibT^D*|TT(K&f-+POgO-@-7reD`65Snxdr+~f$*7pFiBBY_r5OEV|$ zT=+dYPK?x1_WMr5>R!AdW1~8TGy1^yMH)aQOKr&q=Sr!j=9K_z3XNWk5lgA+FQ;S}j0jaZr?`k%1>uSBB1{e^geoH=>NiGv3Aq;NA9F zfGu!Nv&>fK41afvdqIC>6|!Bm9S9jsiUtp%Nu2P^DudC3Z-hkqH`|xHo)_%JQyUgz5!-$b&~Dfd4H?N4EUMKw_j>ds zG{$V(%QNcmb{r;~4Y_?#YXFGh@>fm}RQn<2CB>y`OBPcBu}{xVCrQWxS~I{U`h&BZ zQyX5d;;HIogHY)5@93NwA&;tX^#+jiwgZuc!^VsgQV39eBZ0D+P655MRQWc#N8p1E zn9qzo`oGZeYUg9s(ql)S*2A`()Heuj@^ySN0#1z{feu6L4l2*oIFx%9vh)PdPJAhy z3~Gx3C_&w*N#$PIvGT4YFyyjG?eeuB4}m3fc2m8HFpG8tJ%COFPq&lPCOicoeNPvN zoqb-H29x6MUIFG%hAk*`2D04mPDW=J)xvP75wUIde}_)}oFfjyMA&N|>huY}NlSv{ zUk9YaBsc+(ppzq}uDtri_o9lmb}EHZfJ`J+Y9#=X**XYPNw<-r{Vx>RI`{texyget zH(uUAMeLkVXb!OL+Q74_476kOl{tP*AX87yX0? z%|XYZPFnfwaZ|)Meo|M?Y1x{%wm)Q`g!V%LbMj{dfM@+sz-`X$1X!1PdozHPo>pb@ zwZEW~o*AmWKU7;As@WZ?Jrb%pXt0Y2j8Ju5sCH$j<}v!Vb`ieC9T51ozUiS&_&2=s z;qaPmq3VZ1Rn^UNH-;A<3RQ0nRj&?J)#x{UPl(mWOh7ZD%uDAmaLu7m&1S;0wi>84 z)h!4&JQAt~f_0(lEureUA?o#LsOAA+z;5vdu8l?%KO>PL-g5kCc<~0aPgQmJ_`H)F z7TQ>Y4}i9jjZ!a|P_x1!oDqwmRBnM40`5Z$k!7K3Hb(VaJuOtTCseyGREvL!fpLt1 zz_&)=3#?71oj7{D`N3`BWsNPhM?=-y!&RI68sIo*XU+Cd?L(oO1EJcj0LCHF{G2AB z8^cFdHm%$h0#U^{I2#NC%hrQU%}*Q#9mMpSC86pELe-$Pk)gI!W~-hTsyWVt;zQNt-7vd3ozUqs$LSRS*+mJ0nLr*%ATXT8MTJV1pqY@<7>|>vn)4$*n1Zfet)X=h}x@k8i zwyqf;c0xrY(X_Z)v!Z!3SdFp4dHlN`^UD!!*|VT&!;z-idEpJazYCU$g4ba8fwltE z6TaKvPy$$r{rES$d?{FTBNLhiS=@;u+jlfQe4k5Mg_@G&n=H^TW5NwJSre+QS6Hl{ z6RO!3s(k`u>Cs$%En#Sc3WM#W1`c6(pxm;vlY*nRc zrCDA}WY)q&NK;w8o$2oSP)#+a{=}1yg=&_u(P1+3OUjD_<#b*h0Tjw*M9mE5RumTH z8!_d5#b7JAz6e&-8>(T7M^<1ydsH2&eJuRw;ifg9 z3_Nr9f>=QwmRM3^8rYUCV2-o35q)y)x(Gvb!!*|0IQme>ETs_T>EU%tj0_wy9(ya} zNvwR1mbe@@Gp7v41_LpgPjA^*^-0v}rfGz~U zZ;bw-nxo9^JWd*Id44%W@zXU58E%7EhZ~QzJb{>wJ1%Go7}pq{_jq{mqfB$EXf{Z@ z;4vsBXxX@pIj7cwfYn~Y!US1NnL|#lF$pV0J>{^p- z32t?r@IK%x*-3WqiPyM_{pCUq(JLsN%EBiRae^!(3a6=-+&K)#pX26}02<+LHg-}p z5wyaXKvsGZAq2mQxoUm{@ne09X|ZV-ff5T1O`k}(wLGz>dG9Vp31TPoLc~noK-{|( z@p2RzTPQx~lO5xdur;}vPPDLa_x~9E12TH78Pn1PKEe_o#KI!P#fKW$%Uz+`jV$4S zb*&eP+4iPY3&J}wcZysHr#zV=XC)#aSoMg+LddmhA8SFXxA5`sPH0}8fN07*H;`}! zaAORi^+;SuTSN*#?eu}7*LK^L@*u^IAsiW<86FE1Q!EI6cG2nCMb9KDG-2*%56cG@ zvOo!nEr)g@GKEa1TZ#-NMS6_*6KsV+Vfjzw{8M&GOLZ@!NM7IT`|n~bZI)toTXV5` zp&6-DstcMh!&O8UGjXs7@h~o%PdxefiGxV4AOvmjLE+`#;xRV)RvOO2#sH3iqU(KQ zr{iF7GFmO07f`rKaNCeYlFL87XQBSUh7)P?G`dA9(b8z(rZWi}2<_#3+u4B%ss+w>k z)I}ykinRK0^MlB}p_G=aBW*^_fTqS#g@6lPu%&tV{1XQekANp;LKFead+oTVx1wOD z<|>(sr6X;aaA9whOq#qVT*0jrML}U~3MeonCyWWFB1nkG8>nHZ!lCqle8|aHU3k%A zHXVS9nKHSB(L$6g;Qs>{(HW?M+Y~K4j@V{J^L`YHPzHdpgs94HkKMpw-r#s=aZyQ5 zeh~A-Vsh^Ld&MTj&L;mS#SlcR{#CmunSv;w04qVlgAtmS9b>hz1}=$s#xgS7*ij@V zEE-Tks`D6g$%`qIq^xo=6H2h!qhq+CE<6wMb-jx(GzA?ym~|$qTxD_*%?cFY(w^cx zp5ZlbebNkhs$ktstU;rqlM=~1d|^c)%=zXuE!&XOd&B`WGL;AwPeNFD<(6AOCO1V* zPpqes7qor;QY3XKB!=r&aouALKq+P&9OJ1EGcB><<7=9CJ}3*CE$DV-l4OM{g8D#r zdzUY(jv9zrL{w)LHbr-eafU;kAb!jVS~IrZ3v45^MByq(8;0N7vTZktdcw~z*-3PF zv#_^HaTdX5NmIYszZaMc&KntnFpCQzLgCH4H^MY&@?6$ak23X!gaFC)Vz88L-wJf@%2LZmQxPSm0uhin6=_Ovx;|I0tjm=){M-Dy)I+z%2fm3LZZ8M0j2; zC*O9EUe=Ax64gLi*~Z_V*LnFlF*gtRnu!Bpqo@z zM7fL=hAiIHwQOICSWc2ffqz=mkO05?Jg?F`gO7BgUbQ=q|W9TMebUbLs0JXu|X z#b_crV<|2os%q3$kffo({oGP9E|Ky|fvmcW09&UP_Beu6-wbcq8(zK;qKLH~Rt>Z~ z8BH7?vrcf?a?8wAbhq4(DDbP6Ob(8#+A+K0Qv;D@*__}d8^cHTnGS=ZT~w10&NE@D zT}tkkYD(n3-BF&9`t>GlEDn%^KCnZ?eVii34lDLV7_sRv6v*xRjOg$(V1d;Q6c||q zKz@}4B*L(gQoG{IZX`|7-~icf%${el$zp<@KVdwj{%|F84P!zoeO?`^#;P0etVgX! zAZ9igMGc@U_}Kmwv9l7dE&|a zEWLKemJ!B?#~2B`++r@~R5w!E?k_)l;b}0V6xNSY63b*GE#U#0H61RN3u{Qyl%Xp0 zdIcu?#lvN~i4ksLozCNlrd7z=z{RC9niMo|1+sI%U`b?T@$T>=mEnBvVDyYK- zSgOX-A5wE`MjG2hJXbN`obJHTqWv`C-0FbCDYYfXB?^j9<^`efLNL>r8R}oQbAZOY zh=9s!47*{aE!-s&H+cZhtcv1%C)`pP#7)6Qc6miUZc1bm4QpwyX~PuNR?*~31?iKk zU`>|7c202MgSJ>yhrx#@!XgwY4fVf|D-TpnvBnS>fMAk>R^}?%9>wxi)yPSU5yh{= zWhtPQLa|%75fw$uXq?(A_=d!Zr8W|0ctVBH#qy8Lf5tj#6(a7froMYE2u+rb(U2ZW zg^9NU5;pKvP=mgP2sW&F7AZg-Yyp;So@9AEN+sJZ@C}BzxOEu6=b0+jIYnk1X$hnP zQS`7ICGgE9!brgi!uCOAc}nfVcCYTRdnOu}U)2Y28?y}k0 zv=cQ042tL!F&@fwHl9M(Fo+1(lxhtoUX5vcIfRa4A2M6*{8NFpGh#=nYFEF^*gk3+*8Nh_g2DZdwVz_#f=Xzw7(*V8l)=A{OU} zZ@nW7hM3kG%nFfc80y$fAP-jvUNn| zZ3Ps+DT*zBVRl*{+D-sOeabR$N~}i|)$6uC@N%qs2C&0~0t{upm&tp8@LUn0Y}H@j z`?5Q4WAbuv7a**Lup`zNQ*nP+PEm~b9!;<+N7xScoj6Na8<1TzEvf>-NSm6EJt49V z%hU=%?C%Q;eG!SAtVP|_UleW@UI8^_cfh2^F6x{UWA1|}L#QmInEk&+6oFe7Y)9b$ zg}(5D!<44-P@mM$u4Sm%vmjqFhQj2%p|=6B*dgN#FikSO06Pq*5n~P`T#CaqV;W7J zsNUMV=n&6p9mA0Y6Ud^wx$S^0vPxE75EduqKxK`BtIFaS(E~O`iGFx_#bLd?3|fXK8@KB1Zj=l;|#OOaOGbx zF;cS<(355VnQSKnKa$EE&MpGM-T`z5HaUh zS!6Q%6_!`BC|c4eS3GhlOUYCt1EIw7*!8k}34TGi&8@u{*Y*v1nwT*bL%8ffRJ(L7RT!*jnh-$yJ+997pueP$N-P z)P{EEJ-SzT51S;pnJ4wC!I^dXV_5)9#qk;uYAtW2wjGI3w#+zSjT_)T6d_3G)bz(kpf#W+pwN$!XvYS zJiKG!yA7p6L=h21sJcOj5Cr03CY+FHDAs1Co0uEYT<8IGn4^c<$deQWoBmu4>sPoE zd0R%?LcR#aRl*UNC|aNj?teT}u)wYc!6L%h%C``(q8}F`OtP=%nowL3#Er;gNJ+J6 zxkgwtR{3zb7S&7mho=;2_I2ZEzB_=dFMN=yvSKeBkM(Q|q3_X2fLR&|DRC2E>Zs{> z5#^XW;0SBBlf9nN6rtdS_d-+-GWoD8i@;6<4!j6tt{wXo(23#wcX1q7|vcnTwu>(WpaXrFT1Ihma zFKTb1LG_=^|HeV$TS3$PxPOdz_Dk>;fLf#giti}cj{BJ8| zX+>}vl4p5kufd2JgL}zJD$0$1*zQ?UoF5#3CQ4cW=@vExZh*E-SR@&};^K_vtt&_? z5W-OsxFJOTitVe0R+|BYW#*YE&2*GDnzuEa+_(;b1kdqAKup>}`vF9Rfz(6XpO%qR zj5Tsvl()tOYCbA4$8R`p%pmyW&cx^lArXJhN50QrsA1%o{>^$X>g3whwCj~ySAi#{ zKvEhp|A*Q|@l@VagJ60|QGTE-$`IQe8k-+INDF7C9-8_iw%1@Ln0{j6#!MHvF*C8o zi)lp_ROuX2ScXk*C1sW1iK*nC;fd4!GaCe>xslllzTj;x@e09aldhKPB9jDM=WcA- zVr`!hxC#zPxJH)sCv`JfCKgMJ9S|&OX9Xj{e#$A7{D93mOAO7)iSfWlaL@2$Eta?; z9b(sOWbAvIyWAZywJT|6K_Ypda?crK9w!q0)-t(6Z@nq7c6^zFrXO$D8k4yIfNluZ z>e$%WjvYh|13uVHUd)X)z7qu)%$u4i2}DAXW7oorSHf6Z#R|Qf$g|b#!aO4Z!o2f^ zxfRwrjXP})aC$<5NEgNAQ){QWQC$JYTsXMvt;of8)KVC&ED7nNERV3M)fB=ic$iDlBAT}~ zFDAVNTl6s3N`Qjx*G-QiIpLdRgp#I+0OcVIO`0Vu>cFB3FLpScgq|!yYBsCzlvJ!* zVERkAfW=2qqLvm=1!D?DKi}-fL%P8jvwXu-Dr9tR$Xkk99m-)5fUcNRK8|St+B90C z8*huJzO-V&$xB4r(D8_~d;&8Lq$LmUpI=7tz138@<{R9iNLs!_vMDs>^tom24)M@J zPKmwn>#H|co;bLay~PZ}ZO89o``$IuQzdj44Mq$3kn%`ga;=H@4!v(o-J6w0k$mvQ zWwXqHWj@Tx25?wSvTakR$MIe1+}N-s1qR8U6UCg9i@B0HC3E!w_*b#jY!SVP4B26_ z{3Sv!=J2HVVKED;!Efsu~i=oimfZ<=Zo*E<3hjf`U zwqA5{v7?hYl`uT^!oHPtQh28Kr*K|2Qqs;mp>L|mWKpfKlr_#x4W0!Y6a7frl^k5f zkV?(jOfx*aqCSSm6y+nizHd&vTGMB3+1?E&U1}+EJTF>O`Pbg?6sqBcL}bS*q8X>q&Pep0W+s=NGb@l$l8=}Go5e86!tN>Ce#ojk2dBbV zVb;e`egj8|n1^5v*0Km0%)mQoTUlKN$v#ZKX{njPprRS757J%_sHDBUileAZ!fCz! zT2xXz6|4W{)0o3DUwHQbsz0UsBk0_W!s5KxQE6k0^fJ8Me`iVA3?nf%IW~c|q@P^9 z6hh>+l8<4{Y5ngZVV7>ct5j9(PYYyrTw!s3$(=zXJ`VE>Ero`m=p@a6^F2z^%>N#g zr)2QcG-#pPU>Q76vNoeD@(CO=g6t#!u?$5H(yWj;XB42=aXW_ob7wjqHL3(kJF~PL zF-)~=5@89DTl06fY*(vV$WbN5!-~rTWjT10dloI4MmCi1wzF69o!Tol6ys!-otOaj z+U?@Dyl4YPm?aUfNRY`GEnv9qg)>YNYGxVg)g+b&aAL^#JkWCrnK*SNeAVY47-pB9Tvu7|cb5@{? zUQ8r)6X)ot5wNJ|MBaTFQr9rUyVo^UVb73~wzY+H(CvG25~Rz}6WapCW+0C}^to)Y ziqr}PEvL9gc2?jP1&443SpGK;m4R3y_k@@*&P!{$tvIoCIX<#n7t#iK_A$k+Ejdh) z;gFa(062KaDIiJP&5~O9Y4#SRwT*yQ))hyl_dC529KA>L7&jsr64%ViyhB=~`bpnC zxbOajPmbxhDE@x@Ey}J8mIr3Wiq(Rk^(iZmlOHJSVayEXm6R0~=Jqg5liWWsHje)5 zVGOP)D#xi8{fh$?85 z=ZwYAEc&AEt)KT>I`5Kw_YHbt)v|l8?7DpD$>in1dk@~<_1T1#R}U{)Ror*msteA4 zZdI2o{o!}&#y))4qF#@@F!G+Ya(U6w5VAqTNR_uCn z#Gzdy2So3ld;9CVhkf+!?pHRA`Ni5Dx9orWuA&3CzrXRoqA$}A9l5{xQ2V0;p1kK^ z#gTEN-aInq{+>sB%)k5SnO}83Hh)~7V{iQ~``D(LRmX?qUH!|)e)`liuhd-o>{ETN zfA;L)ZO{JlPi4=J`D4bfHhuNsuLh6({Kby-&%d1bvi94Le){$AS{9VP8X9c8cIo?X zzLxRFg4e6>&3^N_`4w;8_2}ns{dUv3cMc3_|6WqvLmz0DHGOb@x2_*9E?@c4>_K&* zAN^rZ__e=vZR$Gmrsk<{HZ+f(G59YXK0fEv3#al=^>}y9sit2xeSF`GKlya+?G>Ny zzIycEUM_p%A1mj4@{hQSrhhi4%eK$EG(Y?K=Vv|mVHAHgmP!|C(p?BYz%q*1Fo4&pz;nPCxqdKa*tp| zvo!G29V^OyI;q`nfA;%|&n}zuVbDn%jCTDg@pY{56@Ab&-TG%`^>f@~Zs10Y{8FkjnjnS7CwnR_-c}BOCIj6eyS)_G; zGPC{lS3Ntv$G@9D=rK91Q_OSsWXAkGb!qIdXKwAe;k;3C7d`(-{N?Rh61rWzD(S1U zF7G`oFroK3Pv4sI@O`uT?7eYGpHS)@{dWhe`j36SA$iPx8`TheKu08x^m)M{~kMe<(T9t>HD*C zR{rDToacYDH20c@gSlzOEqVL)9LYD{IF^6#v$Vj64_{E2^zj3Qhu>OXc+S9C(|1f* zGX2?Z6-Aq;EiDR-{&DfV=iVuMC+V+cua$NTKKgQQ@cf3k<6*s)nZ`P&1K0Iss zxSKhm|y{4_e{cY#rzSpU}HElDlPEEso2$OKR z<&;jE_Q*M0b+`WrVt{9#qg^d-D9OUzL$6(^X%p~w?OB@kCfetmt!Wz#P5U)2Kka>n zrme&U9n)~zQrvmCJ`ZEW<5@h~UX8Xpx@y|XxRc^jz*&g#euuHf1D7bkFY2Iae?{Lg z@F_vxSFhEyE3d@u8t5Ow&kumhN?f)w4DCka*&4KY8+h)y2p8)Cw-?XEC3U#-aD6vT z+XZ;nV2t&bYTCKL^X@}5f7Cv7GT<*9U?QqfP zI?$2cQPYB;>2q9qG7)gJBu)D>V0{c)vZFxv1!xDF4q==Rz@r!Oyc2Ld)I-zW_=%>y zgm&+t%^T?26=OaJKD_|^KLJm!0K6TzgZcx|ayE>35cYBn(T)3tp9Zce!0Q{}@D9Fz z2W>w@+nd3+%fYKk;B|fsV1t)~(4KB}`zvS|0^Xemp1u;TX^p_M5->8(2OaqR3gB}C zX#5g5Y{2(((f%&*>l@(n5%~2d+=`QczxRQL1!#Xc#`_n>_z~#54Q;yN?+xJJySVLS z6KEO@9`yp9E3d18!7`Fv5#$(Qof$sl8 z-<$FLr|5ga(6q(i^MjyqG3NFk8256(*@C{GgLh|vH#v}jw=wSPG}oXr4>-;Syq{w( z9{{{fkjabi^CDo z(NVkZuAiZ6qE}Zs6P({;;@$BPkQN)cBOP5$cevAu*fjP`aJon2F6sqHzeI;ykM2l^4);*+YTyy?!oE-z_)#O!1gic-gqwo z_^j^&o{oQ~TZ_HHo&(j-J$>=SI7QdHi>yAsa`2 z6W9Q52NRcy^9x|TS+xYie1{3PhY|Pvd4qo*;AfxyOxWO^%%U^m2_CF)!;;ok?KY7%r<&c1B{=S^M=x?irERQa-+O?cd0eJtOgL?`p#31$}I zXAY$BX*3_`Zcc`iUsO*sL8sjpG49h9kaWmADIp0nKQ%rHe!u~Q4vwcTbYWE1OIoW3YX5? zrwhCC*ky2kB&X?a;J)y7G(2p&yCI!tUxrH%=mdSbU74&iUFlBj zC$Bpk-1FW5ApQJ-v_7RL0idrx0NW0F6LAc|X<}QtJ7V1UKIq`f8T)>X&5@efv%DY8e5PiyW;Y{| zqf5?+LnjDh@|<+jvdSiJ$@~@_lD#{qBe-qRHAph!TI=fMej_@?w$@1%x6jaJ0Cgcg z^T5JQnR=y@^AeWc$W0Sk1L3Cd9&{TOsT)z32ewy0eFKi`Z@l^aKB_B2B7t;YFXRnq z8dC9yfaFW;co66f=yL#K)9A-u3i>EQ;hu{$JD_bslAjWZ46?6xfG>rtBh44hAHQm? z6xEA}6H0DOzFt|5VaZ@uYTp1vzewY^CcOPnxnOedN3{*WDZ&#B_`tS}tqSs9bQ~C| zqZ)X6PgfIw3=%hLs(QeklL{p0;|{x^!qdYWjQ6-tqeG&12O`PV0_sjKfEY_xB>nVS zgnHf`y(DW4nx}X-Z#}wMh0e*|o$dMbrtc+mjccu|Qxx)fM&e*&0aO zf2u9%0#sQI=qbDF9zY|?NCqT?*ZYVtDuJjTtr80t1JtopAtM!{DYr9vbvlCDuK^BSc(&#eHk;1M&0njaN1td(6D7*%>alnSyS;8!h%Auw4 zt#C*kmO=y`rsV-7kFUM}h;+4?%49Ap6m|3U49bGM^(;1_6>M6M_Kvm=q5{)RYbvvS zu=pAvB3ig?Ofg*>O_8TcI5+a#VNCDUiss8;{5bL9OG!BlRqyovZ0-C>r!x{qZuM8y{G zu6cb~;1Id1GwS&Oonl)XPt}0m-hfzxd?&d*uiBwEdjCeZ=5lBI>xo7Be{NY#q!Aht#r!N5?~(1Y;tG!C^`*@G?Hp-y!_3V z01+Dr1U<`xXq18TJQ|K00FiI4i(d)i&*&Tx<<_0joj1Y^M;gC1nQcF?6``;JI3@R` z)K-MT?yAb~pV2X*{8C4;j%>@+je{T-*?2&;YFhPbi^<)BPO<)-c=AsS(M>2E!~~-_Dgh3XiGAbP9*GPah?9BC(LJT@I5=2%6ZUOn zgLPBk(^bX|24mfua>$&L_orv2_)EXpYm92%n+o7rrvsb}V(Z&Ma)u&9JYBGq6nIPR z0sU z^C|%K!H?2#dy_}&+b?dt8TAKm{4y{bMVJQ)Xvx-lV@SNCI)uHWV`3Kb23|T#A$IFH zLE6dq`2lDv9zPqy?3N!GHM((=@hQ3w@W(>LQoN8tyA8?YjAO2r*73*)Ju}wV61eLP zTRk4QrY+ky5!1!+F9}Ei$Ao7fXqP< z6UW14Qa8$@i`h4%g!G$VX(Da{3s5La-6;xtto&d|QgB_8<%)&92kj*j8;U#JJp&KI)eRX(3hgrnYMy3Vx?E5SIpWo8AB) z0KlTQ1N06qPA984g#ib)eL!!lqw?|QQrm%e<43d5mKe1hgAHx_V3Ae%-vD+R6F8df zp5rp)&5AAoq$h31jw%zh4L8W*ph)4=N&vpK?SLtTwfFmydCtc-ehU}p6~NTA9k{;eYh zoq^hiKS0{;O-mNu1jH1z(wR^%@ae42j5y!2%l0098URG#yT8EhN5=?r-j=ndf3^h# zDY*Yi)3wELxO}WaFsahz?&(DbZ_1CM>qP&q-`_(RB~T^*0lcxQEQLYw?jg=gac^Ivo~5hgC@TuFBofI1CDc;gB3FaJ@ZOt&de#!k0~lF z4#bp+>$9)T$2?p{rd1F4#0QVufdY6`MM{tlJ9gryRN*es)9}&H>!Krg`tCrd5ZU_N*iUTExy@#bKly*;zJp^AEonl3h1DM@SAo zOJ+SkQ%nMHkI4Zu;Wp3RNyi~jx8xL6&>3G_#o=-G`8utS9%B#05XY}T&viD7Ih=lt zucpwQU|?3949}F(TAX5pv&0tRR$lW!{q6jiW=4NMyNAu*27YN*>-Z;PSL&yJ+Rwf8V6;%sJisuB)jsj7yFY1+KhI{w^7z*0*=FyM&ub=fgE zFf`(bAbAq?PtW9%pU3WJiOIuJbU`B(>LSkMHdQ6Pq=df0AzQH%Gb#tuiFE~qdAO-M zwzOcjtw61T8C+6aUJ?|bpnon{G)gdt6VdXG#Dum3X8746nU|9o=$)I8oYXrdt{^X^ zcR{bb+&*!+$+;x%Z04K%#vB4N*#kl z#GHGg)SH9e-x?zegXMbne)MAwglrC{Vh$h3%S;COp`38U1%*>_{ub;V_`fmY+C=!y#?wsb9rH32~|-PIt7= zqb563<~r8X^viU<6CDG#n4MaQ-l z+%ifd%v&;~LuXA_ET0!u-EiXI7Tn?+Ua*A-RZNLQ-&7x9U@oE!(2WBaanuz%(p}s$ zFAO|8oXjs|VJHcHgHw&xd0zIFKw&TX_#rNXMl(2ZV%5}D4m5)Cnhr4hj(q{cl^*1XLJa4gM)=4AY;d9n*$JeC#w%OEncUH z(R=N@(278wdg7TUo$jz3r?XaJq8g1vpd6n-|FBOR*;VG@lL#kQ zLVf0jmqU>80XEHbNL~NVIi-a?XC?GBqvp;KX&kU<@`El3#urwG$I*gC6~`scW#N*9 z5TWRryznuc1Z-U?z^xQOSOEwgwxp91p)Uy)8RMcvYJ{6Se`V{mF6u}f8XQ$p-hfWL6z|dz1i`7@rZn1Wb z@qeuwM&*lBapo-@*=Qa5iF^ITnOupm?e{bt8JqZ#gs zzzwwf5XdYG;qC{mrOJ?GHKrlt9QhRVLOQq_ zBQs&?!@tAjRurmiI^42-DN8019N>ghBy4m_rFrGH8L$Y&{k_inhUe8rvMNS2EC4u( ztI&F~0MD*?AmC%Q26B8RD;!AM#tEqx<#aq=M6k}{MW5RhEXCmxV0x<|fr1S1oH4xi!dAZ{U{>j zh7gbw4L^Q=^WM2eblTXFsogLs4v|=`PZt&pHf6oVe`c~)3a7FMBS31Q9h9=We1f}B zSQFm8l3h2X%KAGxxnXh$4dUG2I_b=RF+4JAcKM3z`;_xkp%6z7gct6oE5_iU%u{7; zqev=OgHgzAl$MoD$HiZ<$Xx^F!Px0RlZA`Q%VZFj7mO& zDw*CD;tqY`mVs3L}3V53L=5(<*1^YWpi5z*t3cBXreG> zhFUY%2l`C9-(*6aV2Kc`BMG95Fed3~R~DF@-q?OCbHRs^NZ$8cR3$IuM+72U9K zwy!#{+yoh1GoV$0(em5uCrg=H)z#0lxOh_cE0li4_qj6k}{gyJc^PL~A*%|t!GO4=CaWPK^~ z4z+Rv6PQ}JfTatVa|G>ktsq7TBBN4^=|X#VX<(b~P(mMx;y%@tBzWm2zhGCO#pgl7 zjEPyq#KUI}Uj(W+eSZgeFZv(6IfU6)ObD^vm4t&Ne(SOvUI>$5M>j0_aChdO$T188 z7U_yni|1xC4gn%9#~lofVtogh6fek7xI_kwFSp3`1MkaKi1GG~*A6u7NU{OZThm;) zOWY{|`-BBH1ap>-BHe@H&S%-5eEF7$@cX;jDCh$}p`T#4a&;j1E;35cvZkGxP2e&l zb3Lm*e0&XyCbxLZhZ&){W=_>HOq&oQ+n%w38kEaRTr;WI7sZi?0EaPk!(J*6S&^C2 zOS*svni#?VQ_09}e^ui%PWY-|c}`K05h$9OBZX~+#r$gr8L}Va4T=kkDV8^4n5%|RSDp;h2mwPZwB(QQGEO7S{ zLj86TA;1cv*uV;pHL$+*Fv93)MNgs>~`E6)F`X1jW|e%Vgcft!@#mga-{WyfJx2fgIF$MY+m+M9El89@fe+!QL%! zTTH2eZB~k8aJc;PcHG34Hv?DB2YD+!UrZS{QQvRYxC#oRCr#YQenizALn2^~;Y4VT z!LXlWlonM?g|Fe1qS(f1MR!KnB!tMLN(M77P?cxxx~BWKg&)VF5dMeAEQX^TRWcN; z5^dljm$Ja5PF#&^*<&WCRW*v2RysR!8pj1de>Sfi_ed9%a932!$b~bRQ(Sn1i1^Jw zT=;$qOO}!N1kVxU(*_U35XbY~fj+5^l2_R^Fcd@l*N$fv=9OVrNTgq;#s#})qhSdz!uI2$ZQ*9RTkaK3XgsQL$F$|fcP;E7t4|RU!`*gjx`=lwQ zBeDXbGFT0JLTg#P!b66c3l0-J6kRuNYu=1YolRxzYcy?J-E?>rt8naNAr`U}Vhb_w zi9OAGcTvdSh~1V`u>eKJ!nQ*6o4E^+7WX_J-)fnVYdfjajSA>ZV7B%jC6S? zj|3~q*tVelSt#LSAD{;)^X|78u>~L@7S(2pzfAtih%?NZVA!3lHD}zJ}lBd*U9)RNe2=rwE|fwWhCp(^;x9VmLp=)C1KC~oW+~^@OnrMmd{bS-(uA-6winZ z4n-rjh^Sa9{S7FpjQmzkX-oYos*tPhl_?+x!gw2*W3~-6IVPYv_yuLzyr602uJEJl z!uR9q(>?5BbJRvzX^)tIdj+vDxXfHhaNy)eSfv<_TOUf;I0yLZ-pSRQ!<$j+Cqj&P zFdGKbDi;ODi>8{3t3RT~y}Pq*E_)4($>)=KoO5sfQ*vKY_`^ zSgtCFQsrS`vgfA8628aPte`=1-NMd_BWi)jq}7QjZK|p|S<4+X?9diRp^3C^lOCS8 z8@ra6wn3V#B`ox!E}kT@TL_eiZ_U5bQO+64Hq*n1Z7$jV)Z&&&@q?9F)6Y! z!x|%geS%3=6(q@sjt$Pj5*n4-p6G_3byhMfQ2Qc2cns z;f3>?)?rhhbyGb!pbRKkDIx0Hb-`zj;CgTK1+^o>Sck+M)^UH2@CHsP?if#Y08BLlLX=`YIzapK>RM{{kYKm}UJu@X+n;%>+ zxgKDr6m&$@8wM6wD}`8+VzMR>E}lKYARhwn#H$<9XceKF11vgP1GNIX712f)A*bAY zz*L#$H!8?>Jw<_w+l;6n4|e#w6ZBn!xj1X@Fl5V^??2%`ty~$apf(Olm7dIU zWQwiP(N2Qd?yxhw_nI4L2TRl~e{p!r8?)Kks^nDPdAjLcHRU ztZeJZ^&yUcv!qGEJW4u%YlG*ZY=T;6>*1E+wResbU+$YAJGZ% z2B{dfGeJxdFFr(~FmDIjRU)a<7p!y>oefV^1LgDUEd-Ej5}}1vD$MXkbn)4gy@f0o zy`F>Ia63=IC>etJIEWkxjI*&YlW%oplv>)i-3tT|L+#s*s=IYnJN9j1TRgeZC)T&r z9%W$RKoE4p2rl8fN$4(Ul-ViRU(7;3HYkIao24kbt6 zrGSgXPH+zBz^;gFakuWAPWR3#7AYW(jD!{`806p(E4E>Zz+;+t`e2JfebWx?DuJ6& zOo!l*i&O@)dH*9V8-cSuY5H5h*#^sYc{^c3s7!TU?p)){O$TWCZ#Z(TvV`xF#= z#l;!;-iS^z5T{X$2=QeidzW+0T2XqgHT)Gi7LYrqoJ|@QScMXA zMC4Tj%S&bk*;|f1{lNNtA8UGizvo8G7+kAeQc-U7N{-WGtnG!gGS=$e!tS2_7_1b_ zppxb+f#x8hqxVo?c9+O}7?hh$yhtJC&3+!(+|TuRXGn7J zK+7A!270lxdTGlsnB3!B=eg%Imk3l*CetJK5*kt1QawCv@R-S2qeqXiHq^wThCYLB zD~yr^J%cG=n^D@^w2GF>#TvcKyiRh1BRfgvOBq9?*Vdj$;^WCjkTFD3Uj4ge00;E3 z=h$mNUbKa(Mg771 zsk-;TV_xgM7V#D5>78D9nllQAYU=P}11qvoq)odbSRG2Z5e|C2TjV6$S5=)MRx1o% zj`mOuvL|LaOT_BtmSn|@BK8$2uw%2C(*a_$%M)xnqGdCfqrH;X*~_palhiPyGP|Ux zg7$#M28)XI>YruqePDI4Kh8h1Xhdz)h~76~-%5W|j@7vPDo} zz3s-bR^(a2{wq=2C$3W}u%t^n;kP%fS`gm(5ZP%K__TRtdfvhv^yi#ibW7XIf+D$LDa_M3cutFDT71Y%Z+|L|^t66=P=2lIDHOo7SyCH67LpoB21B3=u&D>E+CL7$$>Uo_mJRVR_wR75w_c)%$?AV6d9N@ zQaxw$eluXldmDI9aYmrhIK5c?^!i!CPkRXvsRw4~%q%Smu!UzPRt14TzJ5hNq87M# zRan-xQ(tgL1d1J+u;q9c&N>)`*EO;K#N3dBgjg6WQ!nhsU{ChY)R=@MdIgt0^-7^v zb8Qzw-zCK-z#O12Bmj#%uoEbPz+gZW;EdtOOxNDAgALfMt^y14(J|7g5%xg|6vnxp z(5VXScm)S9tO z-x9zV_*{5c>-HP6V;lNfvjU5i&JU#!m-vPO=(bNKCPu z67fs~k_sb*2Dqwu&5H5(fhQqcwdBO%!#1KYENx(UM&AE}y@7)k<%F_z>>#F=EU&PWT!_tMo&;>zwe{i6TX|RuYaHIS z%>0aKkJivwRh)&RR_NQ*_+ZNsyw%IqhRlw5NzjE9odiK|3!XS$!`4ohk&R-n=vmMx zT{6!~adqdf_tl{b;9#j6_5^iHY&jF24wlg5oI^!rEtA}hGc<_keVmQu8517X$LQ;$ z1=oqeHomG-`{a*!)tXsbIat$Ai9n@rYO(!tFkhEbTqrxHX>i|((n&r1lqNH7ZBze= zC_R-=n^<1*ox#YaO+saIfn@SxMpu-VR+JAZEQ6UWDXYZkQbC{>$e-w5+!8Lx;eSH4 z%R-`P4?l@z6a)x+DKM!y9ID-5sRVZzX2_3-2H}oWNjt7bN#TivI?8QMDQI9o>Kru< z8*&>WDO(==Ln>G2fD8ke`<#R?UUVGmOHBt6mf=k-&K~3nR&4>rF=+2xrn+R0 zDE9q62lt5jBMZr-jbr*Zc>7Ek23s#V-e!5h|@dxs8)GCTpgdnMt zg$;9M$(!^7$q%Y*9&`0*yC}8>gx95JmIlf^Ok|^Ka|CVD1ab#w*$gMm^a3TB3TZL$ z5PMM*$tFa=I-4##r_8`BX3znPZ#U+K4t5_e zjm$USYos07_#P9onOtuzqB9femMzQJreJ}zgZS!tcwtO}Igq*8K%_A4PF0d3d@C1@thz9xxQ#&tr`LAUH(vRgTv1j;@61TS7z=0n$q^(6 zG{=~rguQlfO3KRdsUd;6Bo;N> zF)N5eHZQE+B{IflAq)lc5qwvd=|pJ)=~aE8Va~cK5Zn09PLc3)`=*v80qoc9U&)*`|U|*5#m(avRQ?JJ6jPY5yL>rsKjb5#);A+#~moZElV5b zu5;)sMzLl_Y$O7|A7XJK6*xr|8o}ImItDt@crAMtG;KIyy~`zH4<=ru2$&#vb9N%~nBk<%;ZR`Y`W}p1akNlmJR-BVt z6qqxlFvx!L^MG-rI;)sYAfr9WS}~cx_cKlUUSe+-hpAkKX?Ro@A)`G>)+&|QHQCU- zt>NUxbr3uz`jGy0VtY9zUf2+iO+8Y@%G`iyfwI=ZOtGg^pN!m`U}0X5;=t6B@PV6yOS8q{{6+3K|rE$~B2tE*XEJEV1voM{P z%8bxP+oa+(4gS&`LiN~LIOV!zI(;f&FVZnPCtguPA9zx^eR%PR9;FK-PL6snCqH-k!1t|a|~%qp4kiXgo(XxH4Nn2ElR$-$#k z9Q-yjx2Q5Uzo;}^qg5~Iiu|P!v+()6pQNX4ddW`ttUQ7f7MkrmY*UJnbQ76Mmua$~J zy(3z*1#)o#HrRh;;_kxc2GC$B8XB|x_-#xHBG(zaL?Fv{>?Mv?4kL_7FsDW6h-(5- zsosrU-9mm?xJI>uD%dtIMK%+`?FttFR=;x@erpjcyvWQqF$Jw5>n|UJm97*XD9(o2 zTdpM!V8S8B$Twl07KAACMc{RaEZzraaE&zuK86*L%YwF&Iv()r4VZ@s%*IgzC`a@2 zKHJhmXjPyTml$MI7?BqkR-7N0ZRJ%`i`a5xT|H;@lH0XqkerHg1cx|9*C&G*oR%1u zk`kYuo|GP!I;2-xQc9oXq#>yZajAXMlX}OerN<{H#iu4E_vzKEcVbFf{Gjys!F~Fq zCH3k(I591*S08o{fmn7-Et*;+8gl$of5auRkK6Rv5?N=w9ei1I}%*vVzw3Z@T;t% z!}An6Wwm7i-u!@s*SI_D_nqOTny^V$QUl|JdIB8hFGwxuga95cOFkRcj2B)!{UAKH z2x$@Ds3{F4j{JaSo1h7n zc}MJw+s*a+*?vm~Q691y2X4bcVgC%zZeU;V0daAiIDCLP04o>kBqoVNQbH`ad9p~a zKzsnhxtC7Ome^%VCM?hNoK?z9lgGUsg$r=;93Ru9dyb?a1q&^sY^5dVPjlaetD4AsiLEq32b-)z+>7#QC~-sfw}| zgcvwh-sEMZ=*fEQphINsm`m*UXC)X@^LDXaavO;A=Hj$1 zqk4dczHBFr`F>(7Spl?GE0Rdh7P*n2@ZLFmy_jHWn31iaV{WiEpnuJd!wBk>e{hB6vwrb!*tnP10m>Mu{15%V(Aq%Kx_sAf!k=IXu)1l zJevz^$}9mxhtnYy1ElfUE};2(n%j6k)8f{SlQn>veDc1@gX}uhx>2C~-NajOi?}bCg8XRwDeS-Idf9 z;ej;0LTLEzD~Y}Cy~MAY!7D0Kg$1}XibANI@(S#z_bJ8ugoK^HZ%lYYQhG}7#MIRE z#NLAj4@yW*NsaH*E2(#{4OHv^%;^pD7{ZwN@|~8Y4HgOefq@pNgbRtICXGJ za#BiMLRv!S$UB}r2*ef@@v4J+$-#%t?@Oi~eR?3|TKBlc+QNNDMPWcXuFj)#?8NST zxkOLu#~#!5Xjnd&TwYm<*Bl(HAns%Lwg2@$gR+MY(lwpoaUTAim5AFsdd2mM??Yd; z$1SNEZU)x0YwyvtKjLQ{ea0FAHS7r7vf)C z>!4kZpS;ag_<3p`|2%?ytM$t$$*;isCK?aBp41?j+_qN;8ZpeFn_~Mgq-Wa&{AD?Jd8?-K(c4n2P zJ%|Qp5&if*D?2q_JCl>W@9X8w?e3kr;Oaa3E_;7b%5ScEz1`u!A>*nk;|HvIc17Uo zg=?O^@#0JVvhT(n7sfZ7`&zGG{dG#GsWA^-`ses<4M#g}IJ@U{Z{2;k^3ZQSzbP~P(8TK>OJMs#PwVA;@yeAYe*`R6PUZ)uY z-JSpMoBt3F=uC$%h5D)paVrt$@%=dv)S+m6>l9tP7@x(Ti_c8N$6K^)Z8H8%)3Wes zm^NA)h2Mwa-*kMYKS$5}>8@jLybKR;vtLOJl0+XeFr{1}42I-us@vSV?N zI9q2PZ!pUE>6$Ew?Wu3x*C~Z8BNbGJagBXA0lE|lSC z4tQ@DR5pSkK?>qqjtgNt3uESkCQZv^w2U@el7tW@g17V=&AiOZ$TanUT6Uw&Gcp`adqW%VYU`YZWX5?u2iJpN`M@Yn>jk(ukkTA21)me~Ukb1c;NKLj zH>4*Hf8+5v5i^#L|KhYHJdJ1H_GZ83<5?1$*&h}nSoME@{u4QnscYu}uhM@1iOBsQ zPXFy3h}N}?+m>r^bm!OGzT5nhQX`Q=KO08pResb=jBcZ^^q@k8Z)ghNH3vJ zEz6l{V?8oAV3bu98->NjkkQ%3%#!>-Y{!lrx|r}Y)8Gg7Gqm4~)TfCd|D?6+ zbgq7`rd_S`Bm)nw?vKv~J`BW9Z6N=Q;6Q{O;wMW!%%I^6Z6NXjWG49E{%QV7U*XS7 z{d69U0B)uQgg;~dPT@Ktebcs&hJ`w7|0DcPv6A?M<)Yc-%eq`;(nYj7f*nKCro@)v zs28+jxDo9XA{oR3`_Diy1UU=#uxU)6HQ>x(b#=8HWcM;Z5nu6#aBGLWZ(Cno>jFGP z&rAb8=`vmGitnq?rnNsy@6@zcUijn~^}io~i?S=RGCeca&acF$tUykFpsa_%@`R$o z+#UupaJsXuKc%AhuZJ-hSM;Gg(!V%RQC^l))WgWE$So?&8xg1+Q!*n^+&{N>@0_H( zq-0bt5(9B5eXey6R#8m54#k+bsgij7f=NYY>Ja)r|B^w~)mMF0z4f)uHD8RIRI~K$ zp|wjkj+%Ss%S+~8^5}~TKB-t$_rU{i*8S_$$z>y2Zd|_ZmKW|{z5c2d9Zo#^;O}?u zS-Jnyjt_nH@5>(k_Pm7;<7w;m!MIuTHx;^U<7@F>C(t+3vNQ_8(gJt#R3g$6k7E zLxT^m0fb>5~~>*j7AbF}{P124R^biopYE%jnYjON|42KqescD~_(2m6Zdkwe;J8=cIygS`@{t{X z{r8axx4m)f%k+e&_NPAlRJ*TVdOGi;KR%uI{Lh|Q_U#kTJ@CrD=gYo13UV8PjbAEf{tm@w$xqiZ{tA4TT)sah{diB29%U|z3Y11F> z8QcENlke5P+5a-_t-o}OdOQEs*WOP2BI%vqpEvs5>kGEM`^i5V-)-4k`Tk3BH~s1T zl)rtLv~gBw(y6mfoVjU6IKFmK^WeC}Eq|Oj=j5o@|MllfZ+z@8k1fCA)F0Zvf9k@i z!#}?4o%J6-J@AW?_|_(5HV?K>Xsu=?h}x$j;0?YUhh-+$gsm$aNW`-{Ym*AC0=cT=3Uzr!IW; zqU=k5{pD;Us2h5258SY;IJw`PywUx7KYiK2=eD1pn(_M4n>yVV z9{QU<)egVv+mA-hudE;am-GKT`n7+p&8oR)Tz0R*joDrHzBKmG*y3>?gd4`4S2=n7 z#J?AgZ`vLl|H$-xZyLU(~JlD7CyWx ze@5;nD`wnU`R0rl2Ax+ttLLfGe@+{C$NPJ8%g#P?Us*xR-123Cidpx)mNWaTFLGvU z58g9-^2TA6rOl@*hyD4IIXCHx@1Fe3w!4FKhTT(+qDzm?&>ch5-o@WC{CyhE<9r0o zm-f)Kw^7r%pp&K@!tYzI(Xh+M{P{+EeIvWqVD#;zyd+2mL0)KxA}9rRh3N zdmKN1eU7G8T%l?AqRjK?Wt#RYz-jmi>PRT>OvTSmfRP6~-VQc;+QphS`&vy~cRj}I zqG^lJcj$$%Q~0|n200#nA9snS6=3XkX!|wrnGAeNG0tn~(;lBU08S^s*zsdcdrs4| z<2^O4C%zj28gnsLDR3BxXLol5K7ero-@k_B__`}WH`>kx-JeHeY}n&>(QXiMe+Re> z1U*#DxejB04j38e`*C+oTLAil=zlNToWgjGpyeshN`E$jCw1W6m2s$&;QQBs=P~du z1X|9Cf(->N`+>*R`0h~zYzb)d7REgn@cMSc_n@f>Z7u^2A7T6?jDH#09md#i0QO>x zkqCZo1imZKcMfRi0NAtf``h5}7CgHjw7-eJH-T53uLkWH_XRwE2XG2IXxinVzaD?b z^#EKv9|PQfjq$3%^Ls)6tAH^SF8fUMNyXoE;Pzwuz6AKZg8pNEgm!?L33#w2$d%!% zFt6jl$3>vw7SQt`V1I_WI|;n|fX8p4e-L9mMso?=E<)Q{e0MI|{sjC8f?sa}W({QJ zNzj~#d3_!*vH;@%=Jssxed_g)2jDiky{26c9%tdZUHJS0e*O~UMgzCE!H;hMujOpe z3Vz&AZGiuKz%K)Q`aSTy95|F>jI;1LgmLDBPd@|Q1JQ3V#*aq79l+%k@OMXV$U`bi zvP7YPiV^uJf23)%k;n#-0P?~^CepP$Z3h0Gie!w^=UMn%gs1r=8QK7EFlsl-Y$=b; zL8nS-h#>N4O-u0xku4?Qqwu-hOi*XBu_+avht3A?EOOs5XsC|;7v5+l5>HCdD;U(E zRUqMXQ*3i}Dzav^(P(7$qBb)EO}eIn41ysFD^xU~vH+#xY510!S8B7-bkHC?D#W9F z{7V%ao?TA|7bv?P16~j)s>kr>>G+-J?RNsjGO)aXMpWl#Bx@YNJdI-jip|bvW8&ke zDb<;XOtsqxj6;rI^{E+q*?abnfT< zXfmpq0qqV*A5C7l4}iV^;EU>5bfkilC;`nt;{>YM6@Voq@C23D7rFX)SM%;gx2|fp zY*0i>M$(qe#-O5BnRXOSJEY+QkzJ;}L<8EtPDW|WsdpsGWUETipA?Gb?R9h;sCFx7 zkNC`UJ(Dd&HiT>hK+vc{Guy+C$I3tkgXm)X$CZbGh=e4~O^{TyAVE=ZK?y`QgQB9MMg$ENtyWvBR$HsBR$Fbg zR?EGqZLQVTw*J50S@#N`D%y&Gs?n-ZuVzfh zbu9+LWZ&O_dQ5uMGaaMLsFW)(W~~(|#x;sv=ELfxq}+v=GRH>B8FYI9azU!z$D=P= zjA>vRQpeVy<3P>eXfA<$9Mw^DL16z>UI3yY=@2oKNu~=T(Zn7AZz3-hf@F2X$y5un zmI83BA8-tMBI(HBYlZJ%LTo50GYKSV=?Gj^u%G4@u;BZ;*%5& zc5)b86KmjMAXJJIfPk8y9Owu*6Ml*{aL&p23M0X9WEFZ7G65eG-UO*mUZggdk7-EV zdmo&r96uF;$YkGAcKD3DD_;O;Kx!acX6=;q95{W}dfWX4SDB`XQp>4OU)4J+fxyyO z6EGvg=hE~NDOEDlhq|#yV^sP~nRK{nOzMgiBjFg*U%>e?7=cRzk zogcjx968+)uDX`B?*Y)_I9o+s001g|x%yH!a{`?3p>WNSTGSSg{(;{LX;8XB^r0lT zNvWrsUI?`OO>EZ)$7TK_D1-JNKxU28g&Wh zLxZUelbp9yPkgH9S?d%6&6NKf&0Ha^?2d8~b((>;XAtc|j8KX!!vXCybV2xRu_-5F zno4G%At(jjN&wblKRpT=(Cjh<2$H4{!$7J9uh$55A-y!CgQZ~%Un!|xRsgMKbvUkY zw8iL5Mt5f;?zYtGbc})9lf+3Ts(|{UjWvrr4|LPkqq$m!UuBpXxBnRo(3j-j0D%vPUZ^LiDkD*a0ecqn(AT9X~W|_W6^H~C~pTQa?K|IZw;PP zX_evcp-7#K;s?&p24Ju!Es~rhjWwwwRi!=Vm{SErdF>&Rio*`QHvw=m0gXFgk7F!) z)G>JgDhrm8;#w-&eQBg^9Y-p85e?M{AsJ0^EWga6Kq7slkNm9 zO1&p7%mh)JN&Q0K)h6IKk(@lqMy2Jx0gtlnNB)%B#lXv8GKxrEJ3?n6O?j#f;#Zv` z@VGa#U|a*62}pgLE-~kBH18AMd@jtTR9Tcl`kgfx%#Q(3kPd+T%5|QKk=yp>eE=Uz z;KjhF^~hM{hjX=MWDf>xG zjYVIBVWV9(=3?<%(Ea0Jtoa^9O*HU zb|_}h#l)oO56NsM<=XHY8D|QR4In@jJTS=y(r9v$Msc%ga1B=hG}DfA3o&=2X0zOk>Igdzd<9q_Z96w-? zsbrW@gT@SE2vq76^uTN;2_}~5IL=I}MBC_XM8j-*^tN6v1U7~*rC22C1U~?f@##Ur zv(^5{z)!B91L39r8A5+PUg`?78lBHZB#{oio|qOzNl>~%oVbkJ=={pm9}Nu2aRJcI z{5sG|2D6cG2l5NjA-B1Q`huG|75FV(ID3^3?+F(;Jvy3OdN7=P44lMcz-H6%0w5UB zF$83Oe`b9KKyx|$Dk!X6|q}n2Iea5dU)7C7N!zfh>Qw1{ifj)!+M^aDDd}4W;L$@aQUPS^eZg3r$Sr`;FI-- zp!$9Y5A#wzoa@Cg{@|UVN{REX1oT@EovG9V4bNEi#EFGc+l(cXL@qPc2f0fll)KM% z7IzY6Ze!EKcpV)gEJV==3zjTEoRYaX#FOAIY0&{duyZ& zbHsGHB>R=%8~Op#8Pa=MnSJ2i2f_72o3F-VMHx`Y`5NTQfTB1Z5$P7|jZ3O0X%`n^ zLY4TG&O4{o=E()l<2UH$;;-IKa25Ti-_HUYrbDfTmMiB!xe&`nP?8SNZ})?WHF@)f z%EmSfP3T}sx(@7V*)3(~QYh`v+`*Y4#Wa3ZVEEqV>EN{g0`O^eUp9yPpGjq5k5g;> zAW8b*oYLifV0jY6jLZKl6=JE2f*o>zdhbOqXF0k{%Tn!kr$cml6kIxudKXaloGLsz zvc4QEc`m3gV$R>ubb$023n8=RI0?X0GB!p1Po^qCEOk!o2P@PbC`GOeAkvlhBZ@%{ zNtuQ_1XNSB-ztYoRaXOG${*88<{-*+Fsf{05QMkA324iL%j_p>aD~6R#(fT5b@l62 zV?d}hsm?FVg;Lg**(R`mJDYmH4z!V*Y~Kb@Z~egr(yML4rkK;rK7t7~$$iwGRLEMt z8pWRg(6As7vGUD3`r6no0nKATpwnWMXl8|FioeL|-5<6ih=K#;#Et9jx6x$)$xH4E(N)9ckeAK?|j^k$E} ztY%$GgAFUOiEZ3!)9@NJ6r`lHXlLV+_oJK97^`izXYB5cALdsZ{`N8Fw?J`$|EnMh z=6)!=6kKLorU((xn#hEcWU{Dp^Z;1E)aXoewvqWN8LAcnaCmCK;L>P*wAXY#kc{a# z5AeR|vUUwVwyo%NH0fhBCt+itCjBvK z7=jXA4S-Pwz^0k)he`z0^h9U^Uxt%0=Pn@1wGV1C-Rt*BB`6ujBq8t$q>~c_v#p6% zizb6U6F|cc6ey?@wgAcC14Uw@&p!jaAQ`+4#9@T3XBc#3rQJPwP{^#c;;=npqxKg9 zXh^Wfaaahm_hB!ewwv4%q<#G!5DW_k`$9y26_ zUFc-cI!Z~kMUnoeIn8zjz_Ww>(iLK`FCCwfz+O}N(2(Uy4-(|@2Z17qNzTCdzdqD;&0cAwm*6oYmwmGrkmWZq@)V5|v+cn!0_b%Jt zvMiF=vOE58Yb3FCRbunL_A zYivlYYD#QfsfX9ko0NZ^YZ6=bCAKtR-k?J!CfF;!ZL`AUo)f=+M_W^CTjSI|Nvv#2*Elb+r4_^D3@Ym?DnR1>kF>ZVcr4|vbwrx%{ zZ)v-sHGcO#pk9MTZfg|1CbqBj(dNkgyPn+tXftM#bCug0OD|D?Fqwet6-l%_MIt4S z=o^7-J1 zyCkvcmi@c#izFI0$9FYgF8G6-muN<}8`mXvJ%|sH{jICO3ySl$VC@$WY4)#$k$u)%KM1MTPd#5Fq;8}9>VAyM}< zVAex8Dal&?F*j@%gWVlT1SJ6EGjh{qe`^bNIuKg%mvy65=4(OEZpdSo5k;DrBA>?iPsTz00s_q4G&aUELg+S6WqD ziOU2PWYaBV_<4(B#dYNsrID=K!Qz2f7C{^iZ^KT~#~Os-pT@?-V~u9rG?U-n1m2Cr zA6-Uq0-t5zR^Dia5aNdIiDm(zP{wb*J+XDI=xysxJW6b7YHMtkTn*)B2s@B7Yi^9+ z0=Wk>HaDOp-c%N&7Gtr+VauAqay0ICx9xVIZ6)M{sj_H$eBHWu^Yv{FE%AGHa{7=I z2w2<78xy;l;*WxXea``^md(>!k@#cFpoXkwna~nJJiV06sBS}sY35fop{lYfd!`s) zw*Pu7UDSPYQ4NZ;L=u~u+nO2`;`5`mg%uT(aql|{_>Do4YhKVivx|m)A{VwMAi`sUAWoH8>?y!uKhsL{{zz=`h~BJ$~PAi3k!}Synz@Pez)? z3~~4WxXM_x1a~B$MJ1#X3>3>CZ_<3MhI}li#|D3fI?=BZcPAR5=FKp4nZ~r`coh4> zfWd?dpXi$1G-zNJDQ`wOyZ?`%vhL;AM$pO1p@OlK3)#B=@z(f`eemrf@a3?WP4I-s zbJ{zH?b_K&KZY*6#h!wb-Fa|eB5i*2Nh=2!2d6hlQP&{KO%&G7knj5v%%rNbYV)2_r-7r(35mSP7-`nJKJ}lQLU&>S@e((1E zEue(w9!Sfs^Z;WD3#`nqW)#2szd9hpLN7+z7Ui%S>o8n+I5+Q(-?$FF2-8G|AMWmo z%D;y0cotxR$Y)<3tEwdN;Wx$a*vSYWBtoE0XuEl5TjP#!^*W8bAX$su zdv;Ij9N;JaZQLzQP^4`WWqwC|^$tpZTO-_b1Qr3UA~;MDk$zs${Af%U>(bMLq2IF; zJO?kQAV>kECPCgD4_DFhR*@B$# zW3|Vcye>_OY{DZ5gcgyRwC3SfLIO$mn1_Dk*hJjc+ zg76Pt3lryy^1vh7I_-z(UV9@6AuWm4@Zxgs?5djieM_sZsH~_eDvhZn5{n+`pPiq* zL`=qW$Ys3de$Z*2f6((VYUqCWMNLo=J0;I(cUjsf*!{NKH`DCizK%0kFPT^-5!%+@ zO)gUYwTkzemV+IP*<$#^$!l}jrL$OX@SF@w=S`MuH6>bxa&eU<(RtL3@q1T+a#W9q zks(qm;pGb>jR7n9u#7u7ZvU&l!htO+ic4(U4PtYENm1N+J|6aC=o85po&h?@Ffa{* zbD0y^bsu9FYY1Fq=m+T0{d?D--}t?2B5lb0Y(!JNGVVYNM2~t|CWzZhB{jlx8bi|o zg7}My278nkkx%i$CpS;V=^~vF@LFx%qg0K^=C~@Y z1o>TDj(e)0kl9NJ`Z;Yvi>nDMUH<4Em>;fgvfhZx7&w(a#1t*RccD^n+Ej2FNl5#M z_}Z^tbuT^}p_PpA)}A8!B*iRQvNNqjFcu`K;02fjoMkLWXN-zgM^O={vZP*J-jy*p z(hcj0LqfmtvF%?&M@t#npOfup^|-+=6a%G#PRT?P6uY`V2l0`4GtfI1Pt0jA1;Xpd#1J5QHKh@m`!KYN04(_^Ex z7Xb&wN@i|^5^LGSl-h0EjX$N@flrxT6beRA)JFiSRypxUTa`xE!3F3q=H-*wb8Kzu zkq-2WtXLj0Zw`zZ0mZ{d?D9KEvL6EJ3a5z1|$`CYTaJ$Ke6 zb3nyaky(XvL_E3{+{%v=@wLojHf@j08gCwYU!LbKw9?3!A9Lf%N~l;Eah~wCY@7G+ zGOnwVXRoD~v*tvT{L%4g^u*RxG+dT<+7CKf*ly&f{GuAk1RZ+06AugEHjz?s&th_~zw$VKpSK@h2tX49zvN-n(!S zH|%Eq#dNrT_cBBPcp8bXY(=Jtjij0YAF`>f^b!rtZAkf1sDQsEnW&Zej{ zE%GcW+(0uLJ$(d80k1E9Gc1ke_(^qarlKX87CXuBIoI@IPe=ygWJbZ;m^9Udz1?#r zSZ}b;Kufb@ERDMmzItNis(9-<&=M4b4=fCoTJOyfT?2DmWm#3tqM{0A|0QwH^&XqA zbAdtTq8Op>+HENAZgc~BsgJGIlr!UV28{Mp?CH6~me$Ln$u(Pwf}Wmj$DV)}=1N9d z8NaawjEankZ*XV&ylT0tCwBZubB2x?Zjlj@#I$EI!Vw_uM#q=rT<#t|)5I*JIG{HSabthJ# z64pbjU+r5cUSeKXDQ2agw7WsS)}yvJ755k!J??MWjwA$l#=Qu}F7X8N5s*bm7Sd^B z3!E|$1+PIxa`Gm&JUM2S_7EFbse=99L6ky2Uh^RAgmf2t>G#v*coN^eg%Ow6wWLU2 zHP}{KGamgEmYRoD-=LzXuym(7l2cf>RTT z80@Tq4Sk6}KkFzrX|SkMnN?C$vLI?A1pRqFinA0|WNFFk!&3%DnrK2Tfzr=i$E1d1#<^Ct&kt_wR)tt{W_v;xUa$M228}=ed4T*I?A}{KI zVZbIdgu{!idw7d5!M6vgdhlAE7GKp6-|{3K33dL!(^MJm*=o;=f={uL7-=OepKE|e zt(zN=iGkp(#W*PYloWAzfQz2Q(LEI*CKuH%5Hg0@s(gcmf0);=@Gme@nYFaMSd$y1 zZ|B$O?jr2H__Q>bA!rya(?R!2b9gZG7~-TWb~ao&EC8(d(?8m@8d-UUTT~h(f$h|d z$iQxv$5+7JGHnS_XMl0Lb|{Bx$=?jcgL5vbp*Ac!`n5Nq&8W{%>J6UPYVeU3A*JHo zksdeAl6*3U37%NAwl-QLG%8k1#*uowo}imP<|!7-0P7c5#jxQHCj;4RrOdrpGZ|E; zR25`-ZN2cMJoeW{?-*KGDYP;rlPSp+q=Go}WADB8*IdSIi8p_thf*JbNn%ZPxT2%* zGqE46vqWZF6mIXM%a~XXCUC_NJr00}C+897trfS6IYbXjGIt*6Vx0VTP$18C1x(TSQM(;f+b6Wfl`!XE88Yx47v3>%s^%MVYv;WQ1?EIvi$ z_ehQ81S4(mA@KxK1~{=LkIHyGtJ6>-07Fc{6J9stI2Dqcw}^aaKa=Z7+T48ht4%;; zdBkpjHyADd|2Jka+|WYD43rm$9io1E`*O{cH@1J*&L9uF=V_sBhC+)~M@u5gRrf1M zj|IWw!ds_Tq_z}#YAH~@rV!D4aY}8weX?+^%Z9?=_HqlU;(nMEVYO`TRPgj|BkvPxnS%~zBt){#! zEPc?4D%FnL`bvmodm^{)g!Z;9iQFFpL#sTz^vPvwy;##Hj>Nvl9w>We;^@6?*l8aR za365Pw|+XN@YSIIPwm;b)o=>L$qf36az6~p%I}BJN?DEqNp05COW#}TX}CAZPZ$J& z#*qRO&mxbV!fdH38^|2d)tq_LBZPXkH zZo%p5{4F zLMBKb&!(td1RPqfsNMnrSz0sBapvBBGFU7U?zDClT=Mjiw|E?C$)#C(9bdr13vreW*=y@J<< zrijqPum_%wuzRyLJ^c?xNJi)5FS%p~l+z3>TO?g(6YdKJfwX`SLvS~2?$9=Xzup?P*4(=M)&Ie*wJc^!z|5rI(*ryaaeh5RF!002~6b`W*od zNr9efkwIn+)S|on3n!0@_zuA#?csJ3YdTw8!8^f-F`yS}Vy2 zX33pK<*~QRO**Ne^9162W)fBLehSTjmrYQ+A^yC1Bx_7nO-WSf8o$Ad-}n%-*z3G~ znjzE!!g85Zldv$}YacRJ3)Oq>U^xLBib{0UALrh@3@?pLZV`un-0qwAaFv0FzW)|s%% zXNyw$`59g&+j9A3Hlc$)zvS{z0(+Y4JiYK*-C5Moktm{@RQZ_G6SP4xQAIaKx^Mb~ zK=7$dtmvnzbM3%uO9pk1U6kZEfs!MDbEqe&PXbYqX4}FbJXsKzay` zq0@?qJ)8O3hsqInzLa}FTU+HOJ1&v?-cT^^irK9)-@%mZ24gdf^@b&6Gu6^`_mf44 zM8hM9h3s^a+%6DXCXjQ`)6*AzXrGBaMR1Hoa&21r#cj>OJ_7_;kNsXPR^n=Z}) z(cx}tTPHR!XvO$Gp*ZH3e13EaHw>CrF*!p}wSCyKZvRuQ>TNjH49cGRc$|UnM zMXj?kcz!KHE-)z|WC-LTJ1dmY=MFaEP}4fiP6a9im|CT0d4 z_Gx%d@~*w0Kx5*Ol}dQ_w<0_i$!Qh-h-hUKGGo?0wx$-H6__KDL{qN>EEAhgvB7=& zS|{Kusw(GWXSWMaOg<4XrMxk(+1b}&<7+mRE`o;Z41WDE+7Mz5%&}GGs-J*nl={cmm>)n=TR;(YIEA^>l(={wFeZk)gpw@ z*3bG~CzaKO!{|$FdI6;-mRpuFu?oz-sJa#rvN~plVCI1G`V*@v$5mo4p$PAzEXKwF zeWRdy_}JAOO@1*T6Y{VpA+V7}Qz>8AN`Hb|#goS0NSB3#@2#T2&0+3T1nf1kswOIY z_UVO_EtiZVQm215wQwskpqZ7-aP_rg;VIe?QU&_5=bM{hmzlee-wB4hcsfWGWV2v^ZMBQ{2=XGVv}B5Umks%#?2Pxh6j24|!9{GjN83nGAD~!#V)M zeM^QVP?X+E@}0`S6hwnS`XSTA4h*ZFR@C2Srw}%&t1g(R^2U z=l_C&yze{mpDM`vSf`&a8?pYVh99*RHcajE-nG-dJ@UGryjFbOrspF!JbV4;Yu>za z$c-yc+;h{2`_|ul_31rt8T*F;x5U2Fy0+(Yxs6>fs9Ik+c*puJ7Vkh?8mphvhAZg+-s*dANOXg`KNdMp!xBu2W{*5$Md(%c>0fb z{j05bd->&`-%~aB_DZf$wv!o4lyhh;v} zaM|mRjC=3xM_##e+O91-FMjNo->!K4vUj#Ue*Hhj?A^Ea!@Y+*IqZq6TkH1CnE1xN zX>0pF+2`7;o;>)A-cMaSW6)D?{&eb7cP?7?y-_7yzkk7PIS=(_4x-ya!y?c{gfcx}S3 z%3fdoovCj;e{J0x-`@1un?Jd8>syZxJLK2-Yc{;=obvm3*Y@i9+Z$>d-&-jyg=XVLBM&@-QP_wAo{=++f4ANKgq&pLenU-OU1IkWYM`pZ^#e(^VF9=ZFI%Z^+$ z>g^+kHoehh$}{&LGcb44u>+cqJ8t*!XB>C+p9+ur>7mPyJ7vgGCrrBF$P=IKf8~kK z-WWY;=dCp-&FS=$Z~m@lz)^t`^pKu3YI2%4Z7ax z{lw%$&N<_`OZxo#!*~149eq~T^H)#K`b*)B+2fv_*|+(qi8;r=u(98%oj%I#)pdRT z7l)oYa9niOz$2cSS&mA$)oyTdQs*d zet6NOPmh{Xan|3a4%ohzxdS)Kc7+dtHUl?QCdFhyFWgC_M88nKDTk& zfO%scn^M&HNn6niKf1B_%!gWwM@KF$+4JbW(#S8KDsBCAbo94P$Cl@}-B$kHH}5Dv zV))|Acg|XU`E$MMDz-1UvEuzn$5gI-{;is~^8Z-#T6LG$rk9IjM?ch1J9^3kb?3e^ zWbw&A{O;n*@1AqTuzinSdHuX6uDY)IqN`_YnRc~XaQb)dMFqQCaTW4WC!&}tZd}@a zlH-iVeNuB!;$R-`>fU&S6y5s|2OHqoXK1$qcmGYn4W6$Z=Qy+Q-2E_n`d=cvhjmpQ5izFqRD9 zuRt-hKLYjxjHe2)ui`T5(@_ke6!7ok_q!NNBW`mXhjx?jY%|*Y661XIcvL^Y*j_pq zl>~5W{2jP&wFUUjL?3sYjKZ=QOZQVSSG2p_ahz|V;=sjte(aGb8jRb$_X1`y{yrOH z`Bqo7Lj{Vhn8z5@;fi5SpW&{)*}&uEJI;RKYQtQnW?)A2EkfpxsfR=_{EiM2vCP1IGju z+Zuzd_3p%+CV@u%G0(=cv0fO@t-#y00QAIoo=2belXC?5a$Ctn7%U^?N7(}-T@uY#QZ+T_dUQLw}Wqr zfu|JxK7eOkL7yeSb1TMsGUhTFV>tnHz7RCO5cFGz|8B)R>Ot>^Ft@XYAzNdB<3KiFqApwBD7fGGB|zPnZ7hCd;*O?KJBX^mse8Or?{+q7Z6|i z)e)P!=)%X{`5@@ruY#VJx3aWUINB?Lw)nu&2Cs!{kKZ0ZH(y~%*$wXx)n$&u=kSBC zHixOWmyN`U;tF?N`At^o3GZewVY`W1!8N$(@1SP!Wz*oYc4-$(P`4OM2j-#(d z5AS>gIv;=Fof~Zt6#Bb>@PhvYp{){V83uO*UHJ3{0sK^3rTT9a-j2tjl()IE*?-M7 zuTS*X&3p#UhuhfLoQhLc6XZ(pN}QnJc>e{pj)9!Qj*gj2;Z1#4I?1c^v@{uHZHRirVqvvp4

    hcc&>pB`f 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

    5+XD2G2|;z6@1N{!IS=r`elD#8pLYoZZ{q6N zh7Z61{)dfZZ*oD=$pXN6M7yCSPUk`Y1MnO__hUa8M42Hr^Q_IA%7ZK2e0!}u=YxS1 zVlcaS1@;}R^Bu)8kn#{PX`}|0VlUWyHJ)z5*j(o(O2G?>RiZG!a3CI~XBXE|xw848 zgndA80o*r8K%vF^IjTKo^VM5EgYWL(7TO7y8}G(9K8`Uc2_ncwBB2xZOQS%MA^z`0 z0yoGerG4FM>@E2MqfwH%Qkc@Zwc!{ue>}Yl|BTE} z?*Fj&Ch%2N*Z%l9h+4-w*V$v7P?Jl5gh@a`06_^L0TF>>h8u|749!h|SgI%vRTPIh zwGM#VYSmgqP@Je^ZLQj*Y8|VswrZ_bJNWczt^eqtKN(Jgiomk$zQha0gGBO)~n7IyDqv2lF zr)xF(-c1z1dX7mSA(BNIq@r)?y3?zw)=Pq4Uv`=HglZdq121%)i7^jvt@+x&A+(X^x--AZC~QBb~I&s{xOFnjv3N^8^(q78BD|p?TH;pBVmyk z2gZTW4u&S{X`IOphyns7v08`*g@WtwJ@lH&w?SkD#w_JthR7G+e8~c$9w8kXwEk27Kj6>Myv3oUo-bBb~46M*dvFg466Ec3ET!{6S zzEBo2gk8_ZPnbPMu4;zG9g&+pQGAC(XYB{gj7Q~uhKbEQ=D96h+0x7lX>h)h-Oc?lzVh68O$UkE(4M@8^DC*g>jHCXyRd@>0G+cX5bO_suDropOR=m zuqB9dx|jP0<`=w1DCNHS`p1_YBEdn0`pWqGGtA>rOgwF>(@g3%(3tym>8p=D-05u4xVxR`lkEnKI~UXMG*+TdNTHtOpYX_|JTUR5@ci^ z7UV>XG>Imn1Q^?WfbU5?4J*bq<7z+{8Q_H2RS=TN00*sxxMdQosL3<2X^d{RcQDJ8 z`|W>|KT^xQ8G%#EohI1?BOfr}Cn6P~EydGhIHnSTFB!d3KJYs*x|E72HB)Yr2~<+2 zWLuhb{`3Z>ju8QGyd(%5AHXk0h*5^bJkWg>?Jv0?k&_9^Q|{uf8-Y9PLu&tVM!7_S zjOIjNG4{wAV=ZVgipLkin(u)k!tLS5JTh` zy%18frwMu>+DzYS=BKkMx&386eHWBTe6~A9HAXsZ0#H<5U?dWta|T43B^`}&LnBCx zpa5^*B1so8JA@YNjE`ohBzZ?%F;UZm4qr;+BSIfZW)bjEVG)P}0S4J5GUI}K{mH^R zMrFcgc7PhL(1_Sk)~vMADEafDXq`w=qMgEs>o6Xz6tj5asx5KZv{h3Z5ju<$iexm< zr_aAn(FV2w5sB;>zIqdHjKo3%9d0y9;8$>fk;n+-&}?4AtwE{s)Dp@IXeN>>^1z5; zgǬ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 z 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

    d~rq-?d-jY{$d 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 h5(#xaeoRg`*K=~?e1#l0G76KY+-5? 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

    �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ð1O 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

    mOXve)8nv1JahY23=&CuzvfS8EV z!KNTVW)NsH=W+)e1XNiGRcFQSFsv*k-J&BwBbD(4v7xmh%&Djr3qCJtY5!Vi<53bd zjEsA8U|HLr4jMQzBC;5-D0dz9`u$#xVBE1F1%0Oo5%mi{Q((s^s`1ZUaI3cn>R8E zmGfTBOSgXYq3uUyGrJg$tt@*;M?7XcwD-87xp6Hbjbf%j*K>JOiS%BRKz!g8T%;CF z@`4G!ZYB4Faoy?*dFQj~04F%+PFJ`wg^{3;PG^{EFFZP^FNnIZW`+Dlri%qJ7j{h1 z5{Iqi;bQEdy`3t4;B9K8^6D7dK0NAvdG5iAdzD&LK9&Lmp=rdH;l`jR@7#HeMw!oA zMzBWaLQMK>CRE$j7O1?-T6yM{cW8eU&zASQ$h|kl>QJmVNF};*XGqdH{`n3@{1wHA zK%1-ljLcQCp_s$X@o;Dz?wYWh<<3FXid&LjzPaR`V_?Rzh?lu6^Q= zR7HFzG{ky4dt5^J{ufYqygZ`QQZ1-QbR1ZCP=OQxbf3b>*kxnsL?WL*loK_JQBxC+;HG{Ye zO@3K?=89P7aZJ$#64vyUMMqW;cF>c5F}-x*K@1O<@KnQLHyG1)K_zOlZ$*QLfr{wn z#lo)h>%T{Q73EFIE6hc-v+mK^ zbxVu|H8LmV;q@HM`>`iD;)84ZUqi^crWR$wq&0Z zJBJX0J3*!HX5-z__~6bkBkfWWH}OUfpMDh|>m|=iGh4+~k@h^e$UAtQn-Q%I(*A+3 z?K!L*BAYpqDG%v%G#MIQbz(})CV0W@ePTVGAVBAW!B{TUK!AG1FBg#+G+;!>X!EXSHCctyVWLN16gDii=4IPvZjP2{C(BnA9e7?$XvS;P z{(ujN6-}5p7GRnYE@!o*J?mTmaSJfbJ&06f+C{W##H?%85-jm@qVE}}Cn&TpcQ}!QW>wIejm2ouYy&j`| zV+RL@MvZ8Mq_ZH_s1Uf`bO5ie{Vvrv--o5lnJX?>;9yN(u0n#RB1?OQsBhEG@haOM z$Zyj;^TXL)hw{~1RdVE(2%i$UD|Hy+I~-%=yV*6b>aruNr_cKNG#zYBroH~CV-|}3 zOzE>N)uA9p!9#~z_9idYu@+*Zt z!f}3qk=0+bB1BhZ!v(ZK?5;M(=13`g#+~X6&*c8Zt7Q%!Sr#FopB83QLaW zsH?9=K;>8&M>@K_YU!{NZ@{&(;KPYe{SCH zWxKe6+5>r!P2Ab-vyK<kga88V_`NGR8zSTkNd~s>6x}g7sh3T(cwa0 zIxa4%1ZzH+u_7By&FX0A!VPhD)6MwM$0DnUIq}glQts9hPTD~+Iv|Z=5fytjdJzmm zFe8mGcjm-I+y1`t=h2@RY8VVwTUmEH&aNKzKh4B#CK`E${PIq4VYHS7ksDt?j^bOB zsm)`yAKJx6wiD2RfKIc>oVvPFBR`RF(0^1`pId9|r+~MeP zyN`7SwpHTI9iWM<$yBGhv%5RM08iX>CUV}m6(b32*TiJ>&j7Rkge>9D{T3FbUdteU z!?*kx21W-v{UM(MZE*}8+)|%|YZ~!2!mWxa%{%0AnN2US-<2u1kskT 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

    r<}!ngsi*U1 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

    RH2(FBh)Dg2uNnc5MRbLWKZL zCJ^z^JAx2WRtLr`)_`X?B^AgcE@cG!07%V_)1)LBEi;9Qam0LYTw^;d)@&R!L)5WmoKOL9GJtDu{Z7Q?j6eaW# zLebMiuSF2hpkwaX%w_WpD_K!Qy#05LG?=m8OX8JfjHCQ5oFFj}0-kja-Y(#UsLv?iJVpfHR-2l6Typ7&%jgFmO#n!)2-` zD_FoPsJP%y3>dK-0K+EH#D>KU1;d|s4kFT}(!0m1t){=vIlS8$%7qdpmAeLpGhMIq zMaGywm3NzZsi)U-fMAxaG|>WzVbGh3fM>$gn7E-?kZpiD0Ywn~oYWN#$HWe>LSo8- zm|rCB3hM;P!H^SDkiZTuDB)ZU-ptt{mjp*|+S2b30@O5w?(X%!O=lY1T3L>wjF}Ua z#DEAFg4=-P(6#vaBp`&%6)B65zGU5yJJkc?Bc)fP>sx1vMhUJRBV}o)1sR2%!+Dru z>?*+5k&WY49wgr)C=iUHB^kn_+3#bt2dYxwNJTlej*%r}x&#xYT4sXaJO^G`3ADv6 z3=`-gfabY--R=WHFZy_}REZv)$PqPr%0pmf9Wacxu%Hi>XS>yq(Ihf->C7aReCvFPi9 z1TqN)$guYC_=1ual(_4fCrg_L$=zU466!YUjtm2mn0XE+lS~Xr$RJ4=j_!>16~D_iTYTJpGD-Bz7IoKnguVjb@G3(y!8CBq;1r5sv!W5avzp zD!yG5Ztc1OnEKjEmRsr1vuK_me^gdq%5*(E7@f|{7iQ#=-6nHEwA0vto6Z?OOKl-* zW9wKfohKhC`d1Tzqsqq*9a2_NF}`fbn94B&2M?_nFlEQh-=YRgc zG1bS8aUE9>*&aU^mElg-L8XHR4C7Z@IvMyWO4}9j|<{zHpzr_Y4 ze&!SYbe+D-9A_(MNBq_9?2MnTv#ql?eruck@cXO__49G^tuvykrM@GR9_48Cfw!vV zO6085)?vP?)LZf<;p(Q8G{T_<+9uW3kqSB}fSlImi=?)GUzSsW&K92FwBVdKKWX}C z&cyG2u+Is%?6z&Wvuw3faY0apW$?{OFPNksm(tl`ZG~&{?uXc>vU$mHh1SQ|^Du z{C4wj*u&UNrV5u$IZfF*Y_CWgGC0oXhdAEo+|K{^&A*8PuIS{RP{-&1s`aG_-){rK zn}EhAE_0m|@!9@&V&5`+Ji)1U=Hcf!XBs|Dbf!3y@%Kdh9FNcZ?}5JW|C6G-Beb<0 zfAcs0`eMk(ZSMdI-b_x+bo{f0Cppu zHp}z9`mXJ}aSOtz#`kS_((KGnfOMI(DRs)60nRXoziaW+appKh=vxVRP556Lz1uO0 zN}!uDV~(!-za?RITdI%8lQ*v9g4puI~~q3 zfYR=y0Amu2oQ2&g_;;d3jf_L&_eQ_beUFx(rG3W(!W6*R7b6SLBCxA z%Tj18#>hH#GT+t+Jpv3oe^#ViJ(PEnP$$iyox#p_A$TVMUK=0=bRCQt3;>laVhXWVS*##6X2hEQLwM+XB z#6Qm&f}az?i-~wLvhQ(y$w|kVD)XLV@S~IwYs(M*Md=}*0tqNI|6=Hd3#i1k9)0S7 zYgje{IF9pcXI4(zILE^LIubmq2S%mNAi%AGq}Mn@@wp8D8wxDb_&L-W0v#*GzXAAM z2JY75f2B@2o(_<2hsa;`cvdb_@Z~}jqW<5HFP8!c#I^^-)kD8ru>S|`{}Br8>pE2@ zUF^WsiP!x6bH~|eP#@ikDJ(;q9S^v7M`!&uPaU|?o%Ga!a#$DkJ>ag!Ca%gUl&ED84)m~FeeLA(n4%;4V;7=U~zvph@{C<+Vzz+Far_X`gx!XC;F0LjGcyL-N zK705u8o!;<`We9iV+8(Ze@il9f`%=e(PvAh!DNHcjr{h1@k^%^a3u=>++{uxd89v1 zRvig_)4PwJkGiz~3jSuSWdFmW-xB()vvxA`;#_0Fj^{Whr`m8XA=(LUoI69L-AIGr zztJEFauDn?c0`{Y;4HBA&S^I$=%t@dCSCC#aqELTZu^w$91J{c i{4BNqV&MN_Z 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%0Dva%4j})$r4UrvBygcLC{|4v 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

    6;-%B-_%rKo*f4P2VPr^nCgb$Q~9W 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%0A;F#cf~(65U(WswBezFM4!-h9U7SK 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

  • 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¬d=W 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
    5&au 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%0AU}cZ$+DE#nMSf4%uM(j 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 !5Ki7T0W6R9v%G*b!_1f9C}W&1Q8- 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